diff --git a/Cargo.lock b/Cargo.lock
index 14c6a43485ce14feaf75e20ba9bc0e9d0cc45445..cfcee9b60b6a6ca085468b80076080ff18249e31 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -62,9 +62,9 @@ dependencies = [
  "accesskit",
  "accesskit_consumer",
  "hashbrown",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -204,9 +204,9 @@ dependencies = [
  "core-graphics",
  "image",
  "log",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "parking_lot",
  "windows-sys 0.48.0",
  "x11rb",
@@ -344,7 +344,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -373,13 +373,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "async-trait"
-version = "0.1.87"
+version = "0.1.88"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -517,7 +517,7 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
 dependencies = [
- "objc2",
+ "objc2 0.5.2",
 ]
 
 [[package]]
@@ -556,7 +556,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -793,6 +793,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
 
+[[package]]
+name = "data-url"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -837,7 +843,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -907,9 +913,9 @@ dependencies = [
  "image",
  "js-sys",
  "log",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "parking_lot",
  "percent-encoding",
  "profiling",
@@ -997,6 +1003,7 @@ dependencies = [
  "log",
  "mime_guess2",
  "profiling",
+ "resvg",
 ]
 
 [[package]]
@@ -1064,6 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b"
 dependencies = [
  "bytemuck",
+ "mint",
  "serde",
 ]
 
@@ -1091,7 +1099,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1103,7 +1111,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1124,7 +1132,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1135,7 +1143,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1231,11 +1239,17 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+
 [[package]]
 name = "foldhash"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
 
 [[package]]
 name = "foreign-types"
@@ -1255,7 +1269,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1342,7 +1356,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1448,6 +1462,16 @@ dependencies = [
  "xml-rs",
 ]
 
+[[package]]
+name = "glam"
+version = "0.29.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677"
+dependencies = [
+ "mint",
+ "serde",
+]
+
 [[package]]
 name = "glow"
 version = "0.16.0"
@@ -1475,9 +1499,9 @@ dependencies = [
  "glutin_glx_sys",
  "glutin_wgl_sys",
  "libloading",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "once_cell",
  "raw-window-handle",
  "wayland-sys",
@@ -1722,7 +1746,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -1759,6 +1783,12 @@ dependencies = [
  "tiff",
 ]
 
+[[package]]
+name = "imagesize"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
+
 [[package]]
 name = "immutable-chunkmap"
 version = "2.0.6"
@@ -1770,9 +1800,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.7.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -1876,6 +1906,15 @@ version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
 
+[[package]]
+name = "kurbo"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b"
+dependencies = [
+ "arrayvec",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -1884,9 +1923,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
-version = "0.2.170"
+version = "0.2.171"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
 
 [[package]]
 name = "libloading"
@@ -1937,9 +1976,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.9.2"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
+checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
 
 [[package]]
 name = "litemap"
@@ -2100,6 +2139,12 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "mint"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
+
 [[package]]
 name = "mio"
 version = "1.0.3"
@@ -2226,7 +2271,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -2256,7 +2301,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -2284,6 +2329,15 @@ dependencies = [
  "objc2-encode",
 ]
 
+[[package]]
+name = "objc2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59"
+dependencies = [
+ "objc2-encode",
+]
+
 [[package]]
 name = "objc2-app-kit"
 version = "0.2.2"
@@ -2293,10 +2347,10 @@ dependencies = [
  "bitflags 2.9.0",
  "block2",
  "libc",
- "objc2",
+ "objc2 0.5.2",
  "objc2-core-data",
  "objc2-core-image",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "objc2-quartz-core",
 ]
 
@@ -2308,9 +2362,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
+ "objc2 0.5.2",
  "objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2320,8 +2374,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
 dependencies = [
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2332,8 +2386,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2343,8 +2397,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
 dependencies = [
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
  "objc2-metal",
 ]
 
@@ -2355,9 +2409,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
 dependencies = [
  "block2",
- "objc2",
+ "objc2 0.5.2",
  "objc2-contacts",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2376,7 +2430,17 @@ dependencies = [
  "block2",
  "dispatch",
  "libc",
- "objc2",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
+dependencies = [
+ "bitflags 2.9.0",
+ "objc2 0.6.0",
 ]
 
 [[package]]
@@ -2386,9 +2450,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
 dependencies = [
  "block2",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2399,8 +2463,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2411,8 +2475,8 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
  "objc2-metal",
 ]
 
@@ -2422,8 +2486,8 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
 dependencies = [
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2434,12 +2498,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
+ "objc2 0.5.2",
  "objc2-cloud-kit",
  "objc2-core-data",
  "objc2-core-image",
  "objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "objc2-link-presentation",
  "objc2-quartz-core",
  "objc2-symbols",
@@ -2454,8 +2518,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
 dependencies = [
  "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2466,9 +2530,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
 dependencies = [
  "bitflags 2.9.0",
  "block2",
- "objc2",
+ "objc2 0.5.2",
  "objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -2482,9 +2546,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.3"
+version = "1.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
 
 [[package]]
 name = "orbclient"
@@ -2570,6 +2634,12 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
 [[package]]
 name = "pin-project"
 version = "1.1.10"
@@ -2587,7 +2657,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -2655,11 +2725,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
 dependencies = [
- "zerocopy 0.7.35",
+ "zerocopy 0.8.23",
 ]
 
 [[package]]
@@ -2697,7 +2767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
 dependencies = [
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -2730,9 +2800,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.39"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
 dependencies = [
  "proc-macro2",
 ]
@@ -2803,6 +2873,12 @@ version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
 
+[[package]]
+name = "rctree"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
+
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
@@ -2871,6 +2947,29 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
 
+[[package]]
+name = "resvg"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4"
+dependencies = [
+ "log",
+ "pico-args",
+ "rgb",
+ "svgtypes",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
+dependencies = [
+ "bytemuck",
+]
+
 [[package]]
 name = "ring-channel"
 version = "0.12.0"
@@ -2897,6 +2996,12 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "roxmltree"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.24"
@@ -2924,14 +3029,14 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657"
+checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
 dependencies = [
  "bitflags 2.9.0",
  "errno",
  "libc",
- "linux-raw-sys 0.9.2",
+ "linux-raw-sys 0.9.3",
  "windows-sys 0.59.0",
 ]
 
@@ -2993,7 +3098,9 @@ dependencies = [
  "egui_plot",
  "egui_tiles",
  "enum_dispatch",
+ "glam",
  "mavlink-bindgen",
+ "mint",
  "profiling",
  "rand 0.9.0",
  "ring-channel",
@@ -3009,13 +3116,14 @@ dependencies = [
  "tracing-appender",
  "tracing-subscriber",
  "tracing-tracy",
+ "uuid",
 ]
 
 [[package]]
 name = "serde"
-version = "1.0.218"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
 dependencies = [
  "serde_derive",
 ]
@@ -3031,13 +3139,13 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.218"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3060,7 +3168,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3165,6 +3273,21 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
 
+[[package]]
+name = "simplecss"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "skyward_mavlink"
 version = "0.1.1"
@@ -3292,6 +3415,9 @@ name = "strict-num"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+dependencies = [
+ "float-cmp",
+]
 
 [[package]]
 name = "strum"
@@ -3312,7 +3438,17 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.99",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70"
+dependencies = [
+ "kurbo",
+ "siphasher",
 ]
 
 [[package]]
@@ -3328,9 +3464,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.99"
+version = "2.0.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3345,20 +3481,19 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
 name = "tempfile"
-version = "3.18.0"
+version = "3.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
+checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
 dependencies = [
- "cfg-if",
  "fastrand",
  "getrandom 0.3.1",
  "once_cell",
- "rustix 1.0.1",
+ "rustix 1.0.2",
  "windows-sys 0.59.0",
 ]
 
@@ -3406,7 +3541,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3417,7 +3552,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3483,6 +3618,7 @@ dependencies = [
  "bytemuck",
  "cfg-if",
  "log",
+ "png",
  "tiny-skia-path",
 ]
 
@@ -3509,9 +3645,9 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.44.0"
+version = "1.44.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
+checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
 dependencies = [
  "backtrace",
  "libc",
@@ -3569,7 +3705,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3749,6 +3885,50 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "usvg"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756"
+dependencies = [
+ "base64",
+ "log",
+ "pico-args",
+ "usvg-parser",
+ "usvg-tree",
+ "xmlwriter",
+]
+
+[[package]]
+name = "usvg-parser"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc"
+dependencies = [
+ "data-url",
+ "flate2",
+ "imagesize",
+ "kurbo",
+ "log",
+ "roxmltree",
+ "simplecss",
+ "siphasher",
+ "svgtypes",
+ "usvg-tree",
+]
+
+[[package]]
+name = "usvg-tree"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3"
+dependencies = [
+ "rctree",
+ "strict-num",
+ "svgtypes",
+ "tiny-skia-path",
+]
+
 [[package]]
 name = "utf16_iter"
 version = "1.0.5"
@@ -3761,6 +3941,16 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
+[[package]]
+name = "uuid"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+dependencies = [
+ "getrandom 0.3.1",
+ "serde",
+]
+
 [[package]]
 name = "valuable"
 version = "0.1.1"
@@ -3820,7 +4010,7 @@ dependencies = [
  "log",
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "wasm-bindgen-shared",
 ]
 
@@ -3855,7 +4045,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -4000,18 +4190,17 @@ dependencies = [
 
 [[package]]
 name = "webbrowser"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535"
+checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e"
 dependencies = [
- "block2",
  "core-foundation 0.10.0",
  "home",
  "jni",
  "log",
  "ndk-context",
- "objc2",
- "objc2-foundation",
+ "objc2 0.6.0",
+ "objc2-foundation 0.3.0",
  "url",
  "web-sys",
 ]
@@ -4187,7 +4376,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -4198,7 +4387,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -4457,9 +4646,9 @@ dependencies = [
  "libc",
  "memmap2",
  "ndk",
- "objc2",
+ "objc2 0.5.2",
  "objc2-app-kit",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
  "objc2-ui-kit",
  "orbclient",
  "percent-encoding",
@@ -4488,9 +4677,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.7.3"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
+checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
 dependencies = [
  "memchr",
 ]
@@ -4589,6 +4778,12 @@ version = "0.8.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
 
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
 [[package]]
 name = "yoke"
 version = "0.7.5"
@@ -4609,7 +4804,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "synstructure",
 ]
 
@@ -4669,7 +4864,7 @@ checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "zbus-lockstep",
  "zbus_xml",
  "zvariant",
@@ -4684,7 +4879,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "zvariant_utils",
 ]
 
@@ -4718,7 +4913,6 @@ version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
- "byteorder",
  "zerocopy-derive 0.7.35",
 ]
 
@@ -4739,7 +4933,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -4750,7 +4944,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -4770,7 +4964,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "synstructure",
 ]
 
@@ -4793,7 +4987,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -4818,7 +5012,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
  "zvariant_utils",
 ]
 
@@ -4830,5 +5024,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.99",
+ "syn 2.0.100",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 0d667b73bd61dccb69a207164a57c19fe0034891..819e571068083cb0aa8e10b32407d5a394cda88c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,8 @@ license = "MIT"
 # ======= GUI & Rendering =======
 egui_tiles = "0.12"
 eframe = { version = "0.31", features = ["persistence"] }
-egui = { version = "0.31", features = ["log"] }
-egui_extras = "0.31"
+egui = { version = "0.31", features = ["log", "mint"] }
+egui_extras = { version = "0.31", features = ["svg"] }
 egui_plot = "0.31"
 egui_file = "0.22"
 # =========== Asynchronous ===========
@@ -36,6 +36,9 @@ strum_macros = "0.26"
 anyhow = "1.0"
 ring-channel = "0.12.0"
 thiserror = "2.0.7"
+uuid = { version = "1.12.1", features = ["serde", "v7"] }
+glam = { version = "0.29", features = ["serde", "mint"] }
+mint = "0.5.9"
 
 [dependencies.skyward_mavlink]
 git = "https://git.skywarder.eu/avn/swd/mavlink/mavlink-skyward-lib.git"
diff --git a/fonts/JetBrainsMono-VariableFont_wght.ttf b/fonts/JetBrainsMono-VariableFont_wght.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..1b3d7f27df2ec21ab07ddb081cd9e384c8a5ef04
Binary files /dev/null and b/fonts/JetBrainsMono-VariableFont_wght.ttf differ
diff --git a/fonts/NotoSans-VariableFont_wdth,wght.ttf b/fonts/NotoSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..9530d84d564033577d4e985b703e5df42e1884ca
Binary files /dev/null and b/fonts/NotoSans-VariableFont_wdth,wght.ttf differ
diff --git a/icons/pid_symbols/dark/arrow.svg b/icons/pid_symbols/dark/arrow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..704c660a9825e3d3db3dd85bca2c95e675ea854f
--- /dev/null
+++ b/icons/pid_symbols/dark/arrow.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="4"
+   height="4"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 0.7,2 l 2.6,-1.5 v 3 z"
+     style="fill:#ffffff;stroke:#ffffff;stroke-opacity:1;stroke-width:0.2;stroke-dasharray:none;stroke-linejoin:round;fill-opacity:1" />
+  <path
+     id="path2"
+     d="M 0,2 h 4"
+     style="stroke:#ffffff;stroke-opacity:1;fill:none;stroke-width:0.2;stroke-dasharray:none" />
+</svg>
diff --git a/icons/pid_symbols/dark/burst_disk.svg b/icons/pid_symbols/dark/burst_disk.svg
new file mode 100644
index 0000000000000000000000000000000000000000..436826c9945a24a0df16a45f377432cf3ebe56e6
--- /dev/null
+++ b/icons/pid_symbols/dark/burst_disk.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="4"
+   height="6"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="M 0.5,0 V 6"
+     id="path1" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="m 1.5,0 v 1 a 2,2 0 1 1 0,4 v 1"
+     id="path2" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="m 0,3 h 0.5"
+     id="path3" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="m 3.5,3 h 0.5"
+     id="path4" />
+</svg>
diff --git a/icons/pid_symbols/dark/check_valve.svg b/icons/pid_symbols/dark/check_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..abe1c9c9e831df5c07b75356da01c79065b88d1e
--- /dev/null
+++ b/icons/pid_symbols/dark/check_valve.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <circle
+     style="fill:#ffffff;stroke:none"
+     id="path2"
+     cx="1"
+     cy="4.5"
+     r="0.5" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,2.5 h 1 z"
+     id="path1-5-2"
+     style="stroke-width:0.2;stroke-dasharray:none" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,2.5 H 1 Z"
+     id="path1-5"
+     style="stroke-width:0.2;stroke-dasharray:none" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 1,0.5 v 4 l 8,-4 v 4"
+     id="path1"
+     style="stroke-width:0.2;stroke-dasharray:none;stroke-linejoin:round" />
+</svg>
diff --git a/icons/pid_symbols/dark/flexible_connection.svg b/icons/pid_symbols/dark/flexible_connection.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3811057224ac66268aea0b1658ac778491224917
--- /dev/null
+++ b/icons/pid_symbols/dark/flexible_connection.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="6"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 0,3 H 1"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="m 9,3 h 1"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path3"
+     d="M 1,0 V 6 M 2,6 V 0"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path4"
+     d="M 8,0 V 6 M 9,0 v 6"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path5"
+     d="M 2,3 H 3 A 2,2 0 1 1 7,3 H 8"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+</svg>
diff --git a/icons/pid_symbols/dark/manual_valve.svg b/icons/pid_symbols/dark/manual_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d942a790529c6d391955a5729c635a4c94617ddf
--- /dev/null
+++ b/icons/pid_symbols/dark/manual_valve.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,2.5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,2.5 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 1,0.5 v 4 l 8,-4 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+</svg>
diff --git a/icons/pid_symbols/dark/motor_valve.svg b/icons/pid_symbols/dark/motor_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..94f0dd9648821b42a0727c77ddb885b2a057f84d
--- /dev/null
+++ b/icons/pid_symbols/dark/motor_valve.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#ffffff;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#ffffff;stroke:#ffffff;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/dark/motor_valve_green.svg b/icons/pid_symbols/dark/motor_valve_green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c5e62636f99c5e7e9c9f0cb42563f167fc4c7d48
--- /dev/null
+++ b/icons/pid_symbols/dark/motor_valve_green.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="fill:#00ff00;stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#ffffff;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#ffffff;stroke:#ffffff;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/dark/motor_valve_red.svg b/icons/pid_symbols/dark/motor_valve_red.svg
new file mode 100644
index 0000000000000000000000000000000000000000..cf1404caf8354abab263d067909b3073768fb5d8
--- /dev/null
+++ b/icons/pid_symbols/dark/motor_valve_red.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="fill:#ff0000;stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#ffffff;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#ffffff;stroke:#ffffff;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/dark/pressure_gauge.svg b/icons/pid_symbols/dark/pressure_gauge.svg
new file mode 100644
index 0000000000000000000000000000000000000000..501d98ce9545e7faf45682009d3d151b4d77937b
--- /dev/null
+++ b/icons/pid_symbols/dark/pressure_gauge.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="7"
+   height="7"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="m 3.5,6.5 v 0.5"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="M 1.73,5.43 2.43,4.56 M 1,3.5 H 2 M 1.73,1.73 2.43,2.43 M 3.5,1 V 2 M 5.27,1.73 4.56,2.43 M 6,3.5 H 5 M 5.27,5.27 4.56,4.56"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     id="circle1"
+     cx="3.5"
+     cy="3.5"
+     r="3" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     d="M 2.7,6.3 3.5,3.5 4.3,6.3"
+     id="path3" />
+</svg>
diff --git a/icons/pid_symbols/dark/pressure_regulator.svg b/icons/pid_symbols/dark/pressure_regulator.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e1e7f414c302c5f89f73e4e5a433d2ce4372293b
--- /dev/null
+++ b/icons/pid_symbols/dark/pressure_regulator.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="10"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,7 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,7 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 1,5 V 9 L 9,5 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,7 V 4"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,7 8,4 V 1 H 5 v 1"
+     id="path5" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     id="path6"
+     d="M 3,4 A 2,2 0 0 1 5,2 2,2 0 0 1 7,4 H 5 Z" />
+</svg>
diff --git a/icons/pid_symbols/dark/pressure_transducer.svg b/icons/pid_symbols/dark/pressure_transducer.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6a9883e49728c75e2318b94017dd3fac458381b9
--- /dev/null
+++ b/icons/pid_symbols/dark/pressure_transducer.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="7"
+   height="7"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 3.5,6.5 V 7"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path3"
+     d="M 2,4.2 V 1.5 H 2.5 A 0.75,0.75 0 1 1 2.5,3 H 2 M 4,4 H 5 M 4.5,4 V 1.5 M 4,1.5 h 1"
+     style="fill:none;stroke:#ffffff;stroke-width:0.4;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="M 0.7,4.5 H 6.3"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     id="circle1"
+     cx="3.5"
+     cy="3.5"
+     r="3" />
+</svg>
diff --git a/icons/pid_symbols/dark/quick_connector.svg b/icons/pid_symbols/dark/quick_connector.svg
new file mode 100644
index 0000000000000000000000000000000000000000..679e517e01f4e54f6d61e79eb68a0cda1899dcf4
--- /dev/null
+++ b/icons/pid_symbols/dark/quick_connector.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="6"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="m 1,0.5 v 4 h 4 v -4 z"
+     id="path1" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="M 0,2.5 H 1"
+     id="path3" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,2.5 H 6"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.4;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 3,0 V 5"
+     id="path5" />
+</svg>
diff --git a/icons/pid_symbols/dark/relief_valve.svg b/icons/pid_symbols/dark/relief_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5b1e2b0bcc294d855300e10e6c2befe97af8aa6f
--- /dev/null
+++ b/icons/pid_symbols/dark/relief_valve.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="6"
+   height="10"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     d="M 3,6 1.5,8.6 h 3 z"
+     stroke-width="0,1"
+     id="path1"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1"
+     d="M 3,8.6 V 10"
+     id="path2" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:1"
+     d="M 4.8,4.4 V 7.6"
+     id="path3" />
+  <path
+     d="M 3,6 V 5 L 2,4.5 4,3.5 2,2.5 4,1.5 3,1 V 0"
+     stroke-width="1.13386"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
+     id="path4" />
+  <path
+     d="m 3,6 2.6,1.5 v -3 z"
+     stroke-width="0,1"
+     id="path5"
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     cx="3"
+     cy="6"
+     fill="#ffffff"
+     id="circle1"
+     r="0.5" />
+</svg>
diff --git a/icons/pid_symbols/dark/three_way_valve.svg b/icons/pid_symbols/dark/three_way_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6b738f7450c050c47baed3a6cbf5359173f477d3
--- /dev/null
+++ b/icons/pid_symbols/dark/three_way_valve.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 0,3 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="m 9,3 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#ffffff"
+     d="M 1,1 V 5 L 9,1 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 5,3 7,7 H 3 Z"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 5,7 v 1"
+     id="path5" />
+</svg>
diff --git a/icons/pid_symbols/dark/vessel.svg b/icons/pid_symbols/dark/vessel.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2034f174f9fb4043cf2b0af80ec37d5b6bab164d
--- /dev/null
+++ b/icons/pid_symbols/dark/vessel.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="8.1999998"
+   height="15.2"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="" />
+  <path
+     style="fill:none;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:round"
+     d="m 0.1,13.6 a 4,1.5 0 0 0 8,0 v -12 a 4,1.5 0 0 0 -8,0 z"
+     id="path2" />
+</svg>
diff --git a/icons/pid_symbols/light/arrow.svg b/icons/pid_symbols/light/arrow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a5a1f22d00b5a055878fd1c45fc486e95d0673aa
--- /dev/null
+++ b/icons/pid_symbols/light/arrow.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="4"
+   height="4"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 0.7,2 l 2.6,-1.5 v 3 z"
+     style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:0.2;stroke-dasharray:none;stroke-linejoin:round;fill-opacity:1" />
+  <path
+     id="path2"
+     d="M 0,2 h 4"
+     style="stroke:#000000;stroke-opacity:1;fill:none;stroke-width:0.2;stroke-dasharray:none" />
+</svg>
diff --git a/icons/pid_symbols/light/burst_disk.svg b/icons/pid_symbols/light/burst_disk.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fc0a6ebf94b9881ec5658e48da6e537844b6f420
--- /dev/null
+++ b/icons/pid_symbols/light/burst_disk.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="4"
+   height="6"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="M 0.5,0 V 6"
+     id="path1" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="m 1.5,0 v 1 a 2,2 0 1 1 0,4 v 1"
+     id="path2" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="m 0,3 h 0.5"
+     id="path3" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="m 3.5,3 h 0.5"
+     id="path4" />
+</svg>
diff --git a/icons/pid_symbols/light/check_valve.svg b/icons/pid_symbols/light/check_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6f5752dc17d8e7e9bfef693156a71b0f5fc6c043
--- /dev/null
+++ b/icons/pid_symbols/light/check_valve.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <circle
+     style="fill:#000000;stroke:none"
+     id="path2"
+     cx="1"
+     cy="4.5"
+     r="0.5" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,2.5 h 1 z"
+     id="path1-5-2"
+     style="stroke-width:0.2;stroke-dasharray:none" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,2.5 H 1 Z"
+     id="path1-5"
+     style="stroke-width:0.2;stroke-dasharray:none" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 1,0.5 v 4 l 8,-4 v 4"
+     id="path1"
+     style="stroke-width:0.2;stroke-dasharray:none;stroke-linejoin:round" />
+</svg>
diff --git a/icons/pid_symbols/light/flexible_connection.svg b/icons/pid_symbols/light/flexible_connection.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4f2330a8d56087043b7c8c611503c5e8e0a62a51
--- /dev/null
+++ b/icons/pid_symbols/light/flexible_connection.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="6"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 0,3 H 1"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="m 9,3 h 1"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path3"
+     d="M 1,0 V 6 M 2,6 V 0"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path4"
+     d="M 8,0 V 6 M 9,0 v 6"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path5"
+     d="M 2,3 H 3 A 2,2 0 1 1 7,3 H 8"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+</svg>
diff --git a/icons/pid_symbols/light/manual_valve.svg b/icons/pid_symbols/light/manual_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..87d477fa760575b9f48056d6cc4e5de233d22642
--- /dev/null
+++ b/icons/pid_symbols/light/manual_valve.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,2.5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,2.5 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 1,0.5 v 4 l 8,-4 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+</svg>
diff --git a/icons/pid_symbols/light/motor_valve.svg b/icons/pid_symbols/light/motor_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e80e4a92860053fa8b8ea8301afd9168aff2cc28
--- /dev/null
+++ b/icons/pid_symbols/light/motor_valve.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/light/motor_valve_green.svg b/icons/pid_symbols/light/motor_valve_green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ef8cea865f8d42cf205e78aad98e3e95a3cf582e
--- /dev/null
+++ b/icons/pid_symbols/light/motor_valve_green.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="fill:#00ff00;stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/light/motor_valve_red.svg b/icons/pid_symbols/light/motor_valve_red.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7e947e9ea86e240dd37554369a789bc28e996529
--- /dev/null
+++ b/icons/pid_symbols/light/motor_valve_red.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 1,3 V 7 L 9,3 v 4 z"
+     id="path1"
+     style="fill:#ff0000;stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,5 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 5,2.5 V 5"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,5 h 1"
+     id="path4"
+     style="stroke-width:0.2" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:0.2"
+     id="circle1"
+     cx="5"
+     cy="1.5"
+     r="1" />
+  <path
+     style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:0.05;stroke-dasharray:none;stroke-linejoin:round"
+     d="M 4.5,2 L 4.5,1 L 4.7,1 L 5,1.9 L 5.3,1 L 5.5,1 L 5.5,2 L 5.4,2 L 5.4,1.1 L 5.1,2 L 4.9,2 L 4.6,1.1 L 4.6,2 Z"
+     id="text1" />
+</svg>
diff --git a/icons/pid_symbols/light/pressure_gauge.svg b/icons/pid_symbols/light/pressure_gauge.svg
new file mode 100644
index 0000000000000000000000000000000000000000..50eb200e9de4ac115c1925f4604b051818cf1b41
--- /dev/null
+++ b/icons/pid_symbols/light/pressure_gauge.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="7"
+   height="7"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="m 3.5,6.5 v 0.5"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="M 1.73,5.43 2.43,4.56 M 1,3.5 H 2 M 1.73,1.73 2.43,2.43 M 3.5,1 V 2 M 5.27,1.73 4.56,2.43 M 6,3.5 H 5 M 5.27,5.27 4.56,4.56"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     id="circle1"
+     cx="3.5"
+     cy="3.5"
+     r="3" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     d="M 2.7,6.3 3.5,3.5 4.3,6.3"
+     id="path3" />
+</svg>
diff --git a/icons/pid_symbols/light/pressure_regulator.svg b/icons/pid_symbols/light/pressure_regulator.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ae77edc28e573ba1ef3d6fab400e7c2719994400
--- /dev/null
+++ b/icons/pid_symbols/light/pressure_regulator.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="10"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,7 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,7 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 1,5 V 9 L 9,5 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,7 V 4"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,7 8,4 V 1 H 5 v 1"
+     id="path5" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     id="path6"
+     d="M 3,4 A 2,2 0 0 1 5,2 2,2 0 0 1 7,4 H 5 Z" />
+</svg>
diff --git a/icons/pid_symbols/light/pressure_transducer.svg b/icons/pid_symbols/light/pressure_transducer.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f7b68083ca5d9acb8c38c80904402e3ab6d75864
--- /dev/null
+++ b/icons/pid_symbols/light/pressure_transducer.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="7"
+   height="7"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="M 3.5,6.5 V 7"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path3"
+     d="M 2,4.2 V 1.5 H 2.5 A 0.75,0.75 0 1 1 2.5,3 H 2 M 4,4 H 5 M 4.5,4 V 1.5 M 4,1.5 h 1"
+     style="fill:none;stroke:#000000;stroke-width:0.4;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     id="path2"
+     d="M 0.7,4.5 H 6.3"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
+     id="circle1"
+     cx="3.5"
+     cy="3.5"
+     r="3" />
+</svg>
diff --git a/icons/pid_symbols/light/quick_connector.svg b/icons/pid_symbols/light/quick_connector.svg
new file mode 100644
index 0000000000000000000000000000000000000000..10b0146baea439c3d304962f1b6b92b3847291c1
--- /dev/null
+++ b/icons/pid_symbols/light/quick_connector.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="6"
+   height="5"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="m 1,0.5 v 4 h 4 v -4 z"
+     id="path1" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="M 0,2.5 H 1"
+     id="path3" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="M 5,2.5 H 6"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.4;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 3,0 V 5"
+     id="path5" />
+</svg>
diff --git a/icons/pid_symbols/light/relief_valve.svg b/icons/pid_symbols/light/relief_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5635bf87134262044dc56243df6547d599856446
--- /dev/null
+++ b/icons/pid_symbols/light/relief_valve.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="6"
+   height="10"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     d="M 3,6 1.5,8.6 h 3 z"
+     stroke-width="0,1"
+     id="path1"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1"
+     d="M 3,8.6 V 10"
+     id="path2" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:1"
+     d="M 4.8,4.4 V 7.6"
+     id="path3" />
+  <path
+     d="M 3,6 V 5 L 2,4.5 4,3.5 2,2.5 4,1.5 3,1 V 0"
+     stroke-width="1.13386"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
+     id="path4" />
+  <path
+     d="m 3,6 2.6,1.5 v -3 z"
+     stroke-width="0,1"
+     id="path5"
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
+  <circle
+     cx="3"
+     cy="6"
+     fill="#000000"
+     id="circle1"
+     r="0.5" />
+</svg>
diff --git a/icons/pid_symbols/light/three_way_valve.svg b/icons/pid_symbols/light/three_way_valve.svg
new file mode 100644
index 0000000000000000000000000000000000000000..18abf1a506d4d99f4afd50d45b84036dc9caff76
--- /dev/null
+++ b/icons/pid_symbols/light/three_way_valve.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="10"
+   height="8"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 0,3 H 1"
+     id="path2"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="m 9,3 h 1"
+     id="path3"
+     style="stroke-width:0.2" />
+  <path
+     fill="none"
+     stroke="#000000"
+     d="M 1,1 V 5 L 9,1 v 4 z"
+     id="path1"
+     style="stroke-width:0.2;stroke-linejoin:round" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 5,3 7,7 H 3 Z"
+     id="path4" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round;stroke-dasharray:none"
+     d="M 5,7 v 1"
+     id="path5" />
+</svg>
diff --git a/icons/pid_symbols/light/vessel.svg b/icons/pid_symbols/light/vessel.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fabb93e5098aaa9d72dc5618943cc980a5188df9
--- /dev/null
+++ b/icons/pid_symbols/light/vessel.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="8.1999998"
+   height="15.2"
+   version="1.1"
+   id="svg1"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1" />
+  <path
+     id="path1"
+     d="" />
+  <path
+     style="fill:none;stroke:#000000;stroke-width:0.2;stroke-linejoin:round"
+     d="m 0.1,13.6 a 4,1.5 0 0 0 8,0 v -12 a 4,1.5 0 0 0 -8,0 z"
+     id="path2" />
+</svg>
diff --git a/justfile b/justfile
index ea6682b72a8b80b385a49c56e482096d98c68fb6..f537b2ebde3647d48e89f8c9074c4958475ef967 100644
--- a/justfile
+++ b/justfile
@@ -9,7 +9,7 @@ test *ARGS:
     cargo nextest run {{ARGS}}
 
 run LEVEL="debug":
-    RUST_LOG=segs={{LEVEL}} cargo r
+    RUST_BACKTRACE=full RUST_LOG=segs={{LEVEL}} cargo r
 
 doc:
     cargo doc --no-deps --open
diff --git a/src/mavlink/reflection.rs b/src/mavlink/reflection.rs
index e3ed70e723913efad04c48e64e1bf43777ec1ad0..4162f24df0f391d151a7c8664e6a747be13bb9cc 100644
--- a/src/mavlink/reflection.rs
+++ b/src/mavlink/reflection.rs
@@ -98,6 +98,21 @@ impl ReflectionContext {
             .map(|f| f.to_mav_field(msg.id, self).ok())
             .collect()
     }
+
+    pub fn get_all_state_fields(
+        &'static self,
+        message_id: impl MessageLike,
+    ) -> Option<Vec<IndexedField>> {
+        let msg = message_id.to_mav_message(self).ok()?;
+        msg.fields
+            .iter()
+            .filter(|f| {
+                f.name.to_lowercase().ends_with("state")
+                    || f.name.to_lowercase().ends_with("status")
+            })
+            .map(|f| f.to_mav_field(msg.id, self).ok())
+            .collect()
+    }
 }
 
 #[derive(Debug, Clone)]
diff --git a/src/ui.rs b/src/ui.rs
index 077abdba17c67d09579d71aa56bf547fbe52910f..a1f7030e2a67392adec41c719f71b3899227d18a 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,5 +1,6 @@
 mod app;
 pub mod cache;
+pub mod font;
 mod panes;
 mod persistency;
 mod shortcuts;
diff --git a/src/ui/app.rs b/src/ui/app.rs
index 632d259eed8f0166222081bbee6eac571732552c..ad8fa9fe0387921d5596236afa2f415efecbe8e5 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -258,6 +258,12 @@ impl eframe::App for App {
 
 impl App {
     pub fn new(app_name: &str, ctx: &CreationContext) -> Self {
+        // Load the image loaders
+        egui_extras::install_image_loaders(&ctx.egui_ctx);
+
+        // Install the fonts
+        super::font::add_font(&ctx.egui_ctx);
+
         let mut layout_manager = if let Some(storage) = ctx.storage {
             LayoutManager::new(app_name, storage)
         } else {
@@ -422,7 +428,7 @@ impl Behavior<Pane> for AppBehavior {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct PaneResponse {
     pub action_called: Option<PaneAction>,
     pub drag_response: egui_tiles::UiResponse,
diff --git a/src/ui/cache.rs b/src/ui/cache.rs
index 031a361506c55edec402f8fc099aae7810d1e494..db6e95f540f6370bf103b2b0dc87eb485e4def49 100644
--- a/src/ui/cache.rs
+++ b/src/ui/cache.rs
@@ -158,7 +158,7 @@ impl ChangeTracker {
     /// ```
     /// let initial_tracker = ChangeTracker::record_initial_state(&state);
     /// ```
-    pub fn record_initial_state<T: Hash>(state: &T) -> Self {
+    pub fn record_initial_state<T: Hash>(state: T) -> Self {
         let mut hasher = DefaultHasher::new();
         state.hash(&mut hasher);
         let integrity_digest = hasher.finish();
@@ -187,7 +187,7 @@ impl ChangeTracker {
     ///     println!("The state has changed.");
     /// }
     /// ```
-    pub fn has_changed<T: Hash>(&self, state: &T) -> bool {
+    pub fn has_changed<T: Hash>(&self, state: T) -> bool {
         let mut hasher = DefaultHasher::new();
         state.hash(&mut hasher);
         self.integrity_digest != hasher.finish()
diff --git a/src/ui/font.rs b/src/ui/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..de5864a2b2da49bc1c0731184931bd235e1b97ea
--- /dev/null
+++ b/src/ui/font.rs
@@ -0,0 +1,30 @@
+use egui::epaint::text::{FontInsert, InsertFontFamily};
+
+pub const NOTO: &[u8] = include_bytes!(concat!(
+    env!("CARGO_MANIFEST_DIR"),
+    "/fonts/NotoSans-VariableFont_wdth,wght.ttf"
+));
+
+pub const JETBRAINS: &[u8] = include_bytes!(concat!(
+    env!("CARGO_MANIFEST_DIR"),
+    "/fonts/JetBrainsMono-VariableFont_wght.ttf"
+));
+
+pub fn add_font(ctx: &egui::Context) {
+    ctx.add_font(FontInsert::new(
+        "noto_sans",
+        egui::FontData::from_static(NOTO),
+        vec![InsertFontFamily {
+            family: egui::FontFamily::Proportional,
+            priority: egui::epaint::text::FontPriority::Highest,
+        }],
+    ));
+    ctx.add_font(FontInsert::new(
+        "jetbrains_mono",
+        egui::FontData::from_static(JETBRAINS),
+        vec![InsertFontFamily {
+            family: egui::FontFamily::Monospace,
+            priority: egui::epaint::text::FontPriority::Highest,
+        }],
+    ));
+}
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 34c56503162878e5c7cffd58321617b7814f1223..977fbda1ee08dd7f9fac4c989573e74c1fbf8e96 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -1,5 +1,6 @@
 mod default;
 mod messages_viewer;
+mod pid_drawing_tool;
 pub mod plot;
 
 use egui_tiles::TileId;
@@ -11,7 +12,7 @@ use crate::mavlink::{MavMessage, TimedMessage};
 
 use super::app::PaneResponse;
 
-#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)]
 pub struct Pane {
     pub pane: PaneKind,
 }
@@ -87,6 +88,9 @@ pub enum PaneKind {
 
     #[strum(message = "Plot 2D")]
     Plot2D(plot::Plot2DPane),
+
+    #[strum(message = "Pid")]
+    PidOld(pid_drawing_tool::PidPane),
 }
 
 impl Default for PaneKind {
diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs
new file mode 100644
index 0000000000000000000000000000000000000000..204b86aed73673678b68daa8ef7961125a1a8032
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool.rs
@@ -0,0 +1,459 @@
+mod connections;
+mod elements;
+mod grid;
+mod symbols;
+
+use connections::Connection;
+use core::f32;
+use egui::{Color32, Context, CursorIcon, PointerButton, Response, Sense, Theme, Ui};
+use egui_tiles::TileId;
+use elements::Element;
+use glam::Vec2;
+use grid::GridInfo;
+use serde::{Deserialize, Serialize};
+use strum::IntoEnumIterator;
+use symbols::{Symbol, icons::Icon};
+
+use crate::{
+    MAVLINK_PROFILE,
+    error::ErrInstrument,
+    mavlink::{GSE_TM_DATA, MessageData, TimedMessage, reflection::MessageLike},
+    ui::{app::PaneResponse, cache::ChangeTracker, utils::egui_to_glam},
+};
+
+use super::PaneBehavior;
+
+#[derive(Clone, Debug)]
+enum Action {
+    Connect(usize),
+    ContextMenu(Vec2),
+    DragElement(usize),
+    DragConnection(usize, usize),
+    DragGrid,
+}
+
+/// Piping and instrumentation diagram
+#[derive(Clone, Serialize, Deserialize, Debug)]
+pub struct PidPane {
+    // Persistent internal state
+    elements: Vec<Element>,
+    connections: Vec<Connection>,
+    grid: GridInfo,
+    message_subscription_id: u32,
+
+    // UI settings
+    center_content: bool,
+
+    // Temporary internal state
+    #[serde(skip)]
+    action: Option<Action>,
+    #[serde(skip)]
+    editable: bool,
+    #[serde(skip)]
+    is_subs_window_visible: bool,
+}
+
+impl Default for PidPane {
+    fn default() -> Self {
+        Self {
+            elements: Vec::new(),
+            connections: Vec::new(),
+            grid: GridInfo::default(),
+            message_subscription_id: GSE_TM_DATA::ID,
+            center_content: false,
+            action: None,
+            editable: false,
+            is_subs_window_visible: false,
+        }
+    }
+}
+
+impl PartialEq for PidPane {
+    fn eq(&self, other: &Self) -> bool {
+        self.elements == other.elements
+            && self.connections == other.connections
+            && self.grid == other.grid
+            && self.center_content == other.center_content
+    }
+}
+
+impl PaneBehavior for PidPane {
+    fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse {
+        let theme = PidPane::find_theme(ui.ctx());
+
+        if self.center_content && !self.editable {
+            self.center(ui);
+        }
+
+        if self.editable {
+            self.draw_grid(ui, theme);
+        }
+        self.draw_connections(ui, theme);
+        self.elements_ui(ui, theme);
+
+        // Handle things that require knowing the position of the pointer
+        let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag());
+        if let Some(pointer_pos) = response.hover_pos().map(|p| egui_to_glam(p.to_vec2())) {
+            if self.editable {
+                self.handle_zoom(ui, theme, pointer_pos);
+            }
+
+            // Set grab icon when hovering something
+            let hovers_element = self.hovers_element(pointer_pos).is_some();
+            let hovers_connection_point = self.hovers_connection_point(pointer_pos).is_some();
+            if self.editable && (hovers_element || hovers_connection_point) {
+                ui.ctx()
+                    .output_mut(|output| output.cursor_icon = CursorIcon::Grab);
+            }
+
+            self.detect_action(&response, pointer_pos);
+            self.handle_actions(&response, pointer_pos);
+        }
+
+        // The context menu does not need the pointer's position.
+        // If active it has to be shown even if the pointer goes off screen.
+        if let Some(Action::ContextMenu(pointer_pos)) = self.action.clone() {
+            response.context_menu(|ui| self.draw_context_menu(ui, pointer_pos));
+        }
+
+        let change_tracker = ChangeTracker::record_initial_state(self.message_subscription_id);
+        egui::Window::new("Subscription")
+            .id(ui.auto_id_with("sub_settings"))
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut self.is_subs_window_visible)
+            .show(ui.ctx(), |ui| {
+                subscription_window(ui, &mut self.message_subscription_id)
+            });
+        if change_tracker.has_changed(self.message_subscription_id) {
+            self.reset_subscriptions();
+        }
+
+        PaneResponse::default()
+    }
+
+    fn contains_pointer(&self) -> bool {
+        false
+    }
+
+    fn update(&mut self, messages: &[TimedMessage]) {
+        if let Some(msg) = messages.last() {
+            for element in &mut self.elements {
+                element.update(&msg.message, self.message_subscription_id);
+            }
+        }
+    }
+
+    fn get_message_subscription(&self) -> Option<u32> {
+        Some(self.message_subscription_id)
+    }
+}
+
+impl PidPane {
+    /// Returns the currently used theme
+    fn find_theme(ctx: &Context) -> Theme {
+        // In Egui you can either decide a theme or use the system one.
+        // If the system theme cannot be determined, a fallback theme can be set.
+        ctx.options(|options| match options.theme_preference {
+            egui::ThemePreference::Light => Theme::Light,
+            egui::ThemePreference::Dark => Theme::Dark,
+            egui::ThemePreference::System => match ctx.system_theme() {
+                Some(Theme::Light) => Theme::Light,
+                Some(Theme::Dark) => Theme::Dark,
+                None => options.fallback_theme,
+            },
+        })
+    }
+
+    fn dots_color(theme: Theme) -> Color32 {
+        match theme {
+            Theme::Dark => Color32::DARK_GRAY,
+            Theme::Light => Color32::BLACK.gamma_multiply(0.2),
+        }
+    }
+
+    /// Returns the index of the element the point is on, if any
+    fn hovers_element(&self, p_s: Vec2) -> Option<usize> {
+        self.elements
+            .iter()
+            .position(|elem| elem.contains(self.grid.screen_to_grid(p_s)))
+    }
+
+    /// Return the connection and segment indexes where the position is on, if any
+    fn hovers_connection(&self, p_s: Vec2) -> Option<(usize, usize)> {
+        self.connections
+            .iter()
+            .enumerate()
+            .find_map(|(conn_idx, conn)| {
+                let segm_idx = conn.contains(self, p_s);
+                Some(conn_idx).zip(segm_idx)
+            })
+    }
+
+    fn hovers_connection_point(&self, p_s: Vec2) -> Option<(usize, usize)> {
+        self.connections
+            .iter()
+            .enumerate()
+            .find_map(|(conn_idx, conn)| {
+                let p_idx = conn.hovers_point(self.grid.screen_to_grid(p_s));
+                Some(conn_idx).zip(p_idx)
+            })
+    }
+
+    fn draw_grid(&self, ui: &Ui, theme: Theme) {
+        let painter = ui.painter();
+        let window_rect = ui.max_rect();
+        let dot_color = PidPane::dots_color(theme);
+
+        let offset_x = (self.grid.zero_pos.x % self.grid.size()) as i32;
+        let offset_y = (self.grid.zero_pos.y % self.grid.size()) as i32;
+
+        let start_x =
+            (window_rect.min.x / self.grid.size()) as i32 * self.grid.size() as i32 + offset_x;
+        let end_x = (window_rect.max.x / self.grid.size() + 2.0) as i32 * self.grid.size() as i32
+            + offset_x;
+        let start_y =
+            (window_rect.min.y / self.grid.size()) as i32 * self.grid.size() as i32 + offset_y;
+        let end_y = (window_rect.max.y / self.grid.size() + 2.0) as i32 * self.grid.size() as i32
+            + offset_y;
+
+        for x in (start_x..end_x).step_by(self.grid.size() as usize) {
+            for y in (start_y..end_y).step_by(self.grid.size() as usize) {
+                let rect = egui::Rect::from_min_size(
+                    egui::Pos2::new(x as f32, y as f32),
+                    egui::Vec2::new(2.0, 2.0),
+                );
+                painter.rect_filled(rect, 0.0, dot_color);
+            }
+        }
+    }
+
+    fn draw_connections(&self, ui: &Ui, theme: Theme) {
+        let painter = ui.painter();
+
+        for conn in &self.connections {
+            conn.draw(self, painter, theme);
+        }
+    }
+
+    fn elements_ui(&mut self, ui: &mut Ui, theme: Theme) {
+        for element in &mut self.elements {
+            ui.scope(|ui| {
+                element.ui(ui, &self.grid, theme, self.message_subscription_id);
+            });
+        }
+    }
+
+    fn draw_context_menu(&mut self, ui: &mut Ui, pointer_pos: Vec2) {
+        ui.set_max_width(180.0); // To make sure we wrap long text
+
+        if !self.editable {
+            if ui.button("Enable editing").clicked() {
+                self.editable = true;
+                self.action.take();
+                ui.close_menu();
+            }
+            ui.checkbox(&mut self.center_content, "Center");
+            return;
+        }
+
+        if let Some(elem_idx) = self.hovers_element(pointer_pos) {
+            if ui.button("Connect").clicked() {
+                self.action = Some(Action::Connect(elem_idx));
+                ui.close_menu();
+            }
+            if ui.button("Delete").clicked() {
+                self.delete_element(elem_idx);
+                self.action.take();
+                ui.close_menu();
+            }
+            self.elements[elem_idx].context_menu(ui);
+        } else if let Some((conn_idx, segm_idx)) = self.hovers_connection(pointer_pos) {
+            if ui.button("Split").clicked() {
+                self.connections[conn_idx].split(segm_idx, self.grid.screen_to_grid(pointer_pos));
+                self.action.take();
+                ui.close_menu();
+            }
+            if ui.button("Change start anchor").clicked() {
+                let conn = &mut self.connections[conn_idx];
+                conn.start_anchor =
+                    (conn.start_anchor + 1) % self.elements[conn.start].anchor_points_len();
+                self.action.take();
+                ui.close_menu();
+            }
+            if ui.button("Change end anchor").clicked() {
+                let conn = &mut self.connections[conn_idx];
+                conn.end_anchor =
+                    (conn.end_anchor + 1) % self.elements[conn.end].anchor_points_len();
+                self.action.take();
+                ui.close_menu();
+            }
+        } else {
+            ui.menu_button("Symbols", |ui| {
+                for symbol in Symbol::iter() {
+                    if let Symbol::Icon(_) = symbol {
+                        ui.menu_button("Icons", |ui| {
+                            for icon in Icon::iter() {
+                                if ui.button(icon.to_string()).clicked() {
+                                    self.elements.push(Element::new(
+                                        self.grid.screen_to_grid(pointer_pos).round(),
+                                        Symbol::Icon(icon),
+                                    ));
+                                    self.action.take();
+                                    ui.close_menu();
+                                }
+                            }
+                        });
+                    } else if ui.button(symbol.to_string()).clicked() {
+                        self.elements.push(Element::new(
+                            self.grid.screen_to_grid(pointer_pos).round(),
+                            symbol,
+                        ));
+                        self.action.take();
+                        ui.close_menu();
+                    }
+                }
+            });
+        }
+
+        if ui.button("Pane subscription settings…").clicked() {
+            self.is_subs_window_visible = true;
+            ui.close_menu();
+        }
+
+        if ui.button("Disable editing").clicked() {
+            self.editable = false;
+            ui.close_menu();
+        }
+    }
+
+    /// Removes an element from the diagram
+    fn delete_element(&mut self, elem_idx: usize) {
+        // First delete connection referencing this element
+        self.connections.retain(|elem| !elem.connected(elem_idx));
+
+        // Then the element
+        self.elements.remove(elem_idx);
+    }
+
+    fn center(&mut self, ui: &Ui) {
+        let ui_center = egui_to_glam(ui.max_rect().center().to_vec2());
+
+        // Chain elements positions and connection mid points
+        let points: Vec<Vec2> = self
+            .elements
+            .iter()
+            .map(|e| e.center())
+            .chain(self.connections.iter().flat_map(|conn| conn.points()))
+            .collect();
+
+        let min_x = points
+            .iter()
+            .map(|p| p.x)
+            .min_by(|a, b| a.total_cmp(b))
+            .log_unwrap();
+        let min_y = points
+            .iter()
+            .map(|p| p.y)
+            .min_by(|a, b| a.total_cmp(b))
+            .log_unwrap();
+        let min = Vec2::new(min_x, min_y);
+
+        let max_x = points
+            .iter()
+            .map(|p| p.x)
+            .max_by(|a, b| a.total_cmp(b))
+            .log_unwrap();
+        let max_y = points
+            .iter()
+            .map(|p| p.y)
+            .max_by(|a, b| a.total_cmp(b))
+            .log_unwrap();
+        let max = Vec2::new(max_x, max_y);
+
+        self.grid.zero_pos = ui_center - min.midpoint(max) * self.grid.size();
+    }
+
+    fn handle_zoom(&mut self, ui: &Ui, theme: Theme, pointer_pos: Vec2) {
+        let scroll_delta = ui.input(|i| i.raw_scroll_delta).y;
+        if scroll_delta != 0.0 {
+            self.grid.apply_scroll_delta(scroll_delta, pointer_pos);
+
+            // Invalidate the cache to redraw the images
+            for icon in Icon::iter() {
+                let img: egui::ImageSource = icon.get_image(theme);
+                ui.ctx().forget_image(img.uri().log_unwrap());
+            }
+        }
+    }
+
+    fn detect_action(&mut self, response: &Response, pointer_pos: Vec2) {
+        if response.clicked_by(PointerButton::Secondary) {
+            self.action = Some(Action::ContextMenu(pointer_pos));
+        } else if self.editable {
+            if response.drag_started() {
+                if response.dragged_by(PointerButton::Middle) {
+                    self.action = Some(Action::DragGrid);
+                } else if let Some(drag_element_action) =
+                    self.hovers_element(pointer_pos).map(Action::DragElement)
+                {
+                    self.action = Some(drag_element_action);
+                } else if let Some(drag_connection_point) = self
+                    .hovers_connection_point(pointer_pos)
+                    .map(|(idx1, idx2)| Action::DragConnection(idx1, idx2))
+                {
+                    self.action = Some(drag_connection_point);
+                }
+            } else if response.drag_stopped() {
+                self.action.take();
+            }
+        }
+    }
+
+    fn handle_actions(&mut self, response: &Response, pointer_pos: Vec2) {
+        match self.action {
+            Some(Action::Connect(start)) => {
+                if response.clicked() {
+                    if let Some(end) = self.hovers_element(pointer_pos) {
+                        if start != end {
+                            self.connections.push(Connection::new(start, 0, end, 0));
+                        }
+                        self.action.take();
+                    }
+                }
+            }
+            Some(Action::DragElement(idx)) => {
+                let pointer_pos_g = self.grid.screen_to_grid(pointer_pos).round();
+                self.elements[idx].set_center_at(pointer_pos_g);
+            }
+            Some(Action::DragConnection(conn_idx, point_idx)) => {
+                let pointer_pos_g = self.grid.screen_to_grid(pointer_pos).round();
+                self.connections[conn_idx].set_point(point_idx, pointer_pos_g);
+            }
+            Some(Action::DragGrid) => {
+                self.grid.zero_pos += egui_to_glam(response.drag_delta());
+            }
+            // Context menu has to be handled outside since it does not reuquire the pointer's position
+            Some(Action::ContextMenu(_)) => {}
+            None => {}
+        }
+    }
+
+    fn reset_subscriptions(&mut self) {
+        for element in &mut self.elements {
+            element.reset_subscriptions();
+        }
+    }
+}
+
+fn subscription_window(ui: &mut Ui, msg_id: &mut u32) {
+    let current_msg = msg_id.to_mav_message(&MAVLINK_PROFILE).log_unwrap();
+    egui::ComboBox::from_label("Message subscription")
+        .selected_text(current_msg.name.as_str())
+        .show_ui(ui, |ui| {
+            for msg in MAVLINK_PROFILE.get_sorted_msgs() {
+                ui.selectable_value(msg_id, msg.id, &msg.name);
+            }
+        });
+}
diff --git a/src/ui/panes/pid_drawing_tool/connections.rs b/src/ui/panes/pid_drawing_tool/connections.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3cde5f8e6a6ac5ecca442e120f25cd9044bdfff7
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/connections.rs
@@ -0,0 +1,166 @@
+use egui::{Color32, CornerRadius, Painter, Rect, Stroke, StrokeKind, Theme};
+use glam::{Mat2, Vec2};
+use serde::{Deserialize, Serialize};
+
+use crate::{error::ErrInstrument, ui::utils::glam_to_egui};
+
+use super::{
+    PidPane,
+    grid::{CONNECTION_LINE_THICKNESS, CONNECTION_LINE_THRESHOLD, CONNECTION_POINT_SIZE, GridInfo},
+};
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
+pub struct Connection {
+    /// Index of the start element
+    pub start: usize,
+    pub start_anchor: usize,
+
+    /// Index of the end element
+    pub end: usize,
+    pub end_anchor: usize,
+
+    /// Mid points in grid coordinates
+    points_g: Vec<Vec2>,
+}
+
+impl Connection {
+    pub fn new(start: usize, start_anchor: usize, end: usize, end_anchor: usize) -> Self {
+        Self {
+            start,
+            start_anchor,
+            end,
+            end_anchor,
+            points_g: Vec::new(),
+        }
+    }
+
+    /// Mid points in grid coordinates
+    pub fn points(&self) -> Vec<Vec2> {
+        self.points_g.clone()
+    }
+
+    /// Return the index of the segment the point is on, if any
+    pub fn contains(&self, pid: &PidPane, p_s: Vec2) -> Option<usize> {
+        let p_g = pid.grid.screen_to_grid(p_s);
+        let mut points = Vec::new();
+
+        // Append start point
+        points.push(pid.elements[self.start].anchor_point(self.start_anchor));
+
+        // Append all midpoints
+        self.points_g.iter().for_each(|&p| points.push(p));
+
+        // Append end point
+        points.push(pid.elements[self.end].anchor_point(self.end_anchor));
+
+        // Check each segment
+        for i in 0..(points.len() - 1) {
+            let a = points[i];
+            let b = points[i + 1];
+            if hovers_segment(&pid.grid, p_g, a, b) {
+                return Some(i);
+            }
+        }
+
+        None
+    }
+
+    /// Checks if the connection references the given element index
+    pub fn connected(&self, elem_idx: usize) -> bool {
+        self.start == elem_idx || self.end == elem_idx
+    }
+
+    /// Returns the index of the point the point is on, if any
+    pub fn hovers_point(&self, p_g: Vec2) -> Option<usize> {
+        self.points_g
+            .iter()
+            .position(|p| p.distance(p_g) < CONNECTION_POINT_SIZE)
+    }
+
+    /// Splits a segment of the connection with a new point
+    pub fn split(&mut self, idx: usize, p_g: Vec2) {
+        self.points_g.insert(idx, p_g);
+    }
+
+    /// Sets the poisition of one of the path points
+    pub fn set_point(&mut self, idx: usize, p_g: Vec2) {
+        self.points_g[idx] = p_g;
+    }
+
+    fn line_color(theme: Theme) -> Color32 {
+        match theme {
+            Theme::Light => Color32::BLACK,
+            Theme::Dark => Color32::WHITE,
+        }
+    }
+
+    pub fn draw(&self, pid: &PidPane, painter: &Painter, theme: Theme) {
+        let color = Connection::line_color(theme);
+
+        let start = pid.elements[self.start].anchor_point(self.start_anchor);
+        let start = pid.grid.grid_to_screen(start);
+        let end = pid.elements[self.end].anchor_point(self.end_anchor);
+        let end = pid.grid.grid_to_screen(end);
+
+        // Draw line segments
+        if self.points_g.is_empty() {
+            Connection::draw_segment(&pid.grid, painter, color, start, end);
+        } else {
+            let points: Vec<Vec2> = self
+                .points_g
+                .iter()
+                .map(|p| pid.grid.grid_to_screen(*p))
+                .collect();
+            Connection::draw_segment(
+                &pid.grid,
+                painter,
+                color,
+                start,
+                *points.first().log_unwrap(),
+            );
+            for i in 0..(points.len() - 1) {
+                Connection::draw_segment(&pid.grid, painter, color, points[i], points[i + 1]);
+            }
+            Connection::draw_segment(&pid.grid, painter, color, *points.last().log_unwrap(), end);
+
+            if pid.editable {
+                for point in points {
+                    painter.rect(
+                        Rect::from_center_size(
+                            glam_to_egui(point).to_pos2(),
+                            egui::Vec2::splat(CONNECTION_POINT_SIZE * pid.grid.size()),
+                        ),
+                        CornerRadius::ZERO,
+                        Color32::DARK_GRAY,
+                        Stroke::NONE,
+                        StrokeKind::Middle,
+                    );
+                }
+            }
+        }
+    }
+
+    fn draw_segment(grid: &GridInfo, painter: &Painter, color: Color32, a: Vec2, b: Vec2) {
+        painter.line_segment(
+            [glam_to_egui(a).to_pos2(), glam_to_egui(b).to_pos2()],
+            (CONNECTION_LINE_THICKNESS * grid.size(), color),
+        );
+    }
+}
+
+/// True if p hovers the segment defined by a and b
+fn hovers_segment(grid: &GridInfo, p_g: Vec2, a_g: Vec2, b_g: Vec2) -> bool {
+    if a_g != b_g {
+        let segment_g = b_g - a_g;
+        let rotm = Mat2::from_angle(-segment_g.to_angle());
+
+        // Rototranslate the point in the segment frame with a as origin
+        let p_s = rotm * (p_g - a_g);
+
+        let y_threshold = CONNECTION_LINE_THRESHOLD / grid.size();
+        0.0 <= p_s.x && p_s.x <= segment_g.length() && p_s.y.abs() <= y_threshold
+    } else {
+        // If a and b are the same point, prevent adding another
+        false
+    }
+}
diff --git a/src/ui/panes/pid_drawing_tool/elements.rs b/src/ui/panes/pid_drawing_tool/elements.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ddcf65e9a0463491e4618710a0deb6b3214d7851
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/elements.rs
@@ -0,0 +1,135 @@
+use std::f32::consts::FRAC_PI_2;
+
+use egui::{Theme, Ui};
+use glam::{Mat2, Vec2};
+use serde::{Deserialize, Serialize};
+
+use crate::mavlink::MavMessage;
+
+use super::{
+    grid::GridInfo,
+    symbols::{Symbol, SymbolBehavior},
+};
+
+#[derive(Clone, Serialize, Deserialize, Debug)]
+pub struct Element {
+    /// Anchor postion in grid coordinates, top-left corner
+    position: glam::Vec2,
+
+    /// Symbol to be displayed
+    symbol: Symbol,
+
+    /// Rotation in radiants
+    rotation: f32,
+}
+
+impl PartialEq for Element {
+    fn eq(&self, other: &Self) -> bool {
+        self.position == other.position
+            && self.symbol == other.symbol
+            && self.rotation == other.rotation
+    }
+}
+
+impl Element {
+    pub fn new(center: Vec2, symbol: Symbol) -> Self {
+        Self {
+            position: center - symbol.size() / 2.0,
+            symbol,
+            rotation: 0.0,
+        }
+    }
+
+    /// Check if the given position is inside the element
+    pub fn contains(&self, p_g: Vec2) -> bool {
+        // First we need to do a rotostranslation from the grid's frame to the element's frame
+        let rotm = Mat2::from_angle(-self.rotation);
+        let p_e = rotm * (p_g - self.position);
+
+        // The bounding box is just the size
+        let min_e = Vec2::ZERO;
+        let max_e = self.symbol.size();
+
+        // Check if the point is in the bounding box
+        min_e.x <= p_e.x && p_e.x <= max_e.x && min_e.y <= p_e.y && p_e.y <= max_e.y
+    }
+
+    /// Moves the element such that its center is at the given position
+    pub fn set_center_at(&mut self, p_g: Vec2) {
+        // Rotation matrix from element's frame to grid's frame
+        let rotm_e_to_g = Mat2::from_angle(self.rotation);
+
+        // Center in grid's frame
+        let center_g = rotm_e_to_g * self.size() / 2.0;
+
+        self.position = p_g - center_g;
+    }
+
+    pub fn context_menu(&mut self, ui: &mut Ui) {
+        if let Symbol::Icon(_) = &mut self.symbol {
+            if ui.button("Rotate 90° ⟲").clicked() {
+                self.rotate(-FRAC_PI_2);
+                ui.close_menu();
+            }
+            if ui.button("Rotate 90° ⟳").clicked() {
+                self.rotate(FRAC_PI_2);
+                ui.close_menu();
+            }
+        }
+        self.symbol.context_menu(ui);
+    }
+
+    /// Rotate the element by its center
+    pub fn rotate(&mut self, rotation: f32) {
+        // Current center position relative to the top-left point in the grid reference frame
+        let center_g = Mat2::from_angle(self.rotation) * self.symbol.size() / 2.0;
+
+        // Rotate the position by the element's center
+        self.position += (Mat2::IDENTITY - Mat2::from_angle(rotation)) * center_g;
+
+        // Update absolute rotation
+        self.rotation += rotation;
+    }
+
+    /// Returns the position of one anchor point in grid coordinates
+    pub fn anchor_point(&self, idx: usize) -> Vec2 {
+        if let Some(anchor_points) = self.symbol.anchor_points() {
+            // Rotation matrix from element's frame to grid's frame
+            let rotm_e_to_g = Mat2::from_angle(self.rotation);
+
+            // Then rotate and translate the anchor points
+            rotm_e_to_g * anchor_points[idx] + self.position
+        } else {
+            Vec2::ZERO
+        }
+    }
+
+    pub fn anchor_points_len(&self) -> usize {
+        self.symbol.anchor_points().map_or(0, |v| v.len())
+    }
+
+    /// Size in grid units
+    pub fn size(&self) -> Vec2 {
+        self.symbol.size()
+    }
+
+    /// Position of the element's center in grid frame
+    pub fn center(&self) -> Vec2 {
+        self.position + Mat2::from_angle(self.rotation) * self.size() * 0.5
+    }
+
+    pub fn ui(&mut self, ui: &mut Ui, grid: &GridInfo, theme: Theme, msg: u32) {
+        let pos = grid.grid_to_screen(self.position);
+        let size = grid.size();
+        self.symbol.paint(ui, theme, pos, size, self.rotation);
+        self.symbol.subscriptions_ui(ui, msg);
+    }
+
+    pub fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) {
+        self.symbol.update(message, subscribed_msg_id);
+    }
+
+    pub fn reset_subscriptions(&mut self) {
+        self.symbol.reset_subscriptions();
+    }
+}
diff --git a/src/ui/panes/pid_drawing_tool/grid.rs b/src/ui/panes/pid_drawing_tool/grid.rs
new file mode 100644
index 0000000000000000000000000000000000000000..aa74330e567662659f3c2a6da2678ff805d043ec
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/grid.rs
@@ -0,0 +1,60 @@
+use core::f32;
+
+use glam::Vec2;
+use serde::{Deserialize, Serialize};
+
+const DEFAULT_SIZE: f32 = 10.0;
+const MIN_SIZE: f32 = 5.0;
+const MAX_SIZE: f32 = 50.0;
+const SCROLL_DELTA: f32 = 1.0;
+
+pub const CONNECTION_LINE_THRESHOLD: f32 = 5.0; // Pixels
+pub const CONNECTION_LINE_THICKNESS: f32 = 0.2; // Grid units
+pub const CONNECTION_POINT_SIZE: f32 = 1.0; // Grid units
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
+pub struct GridInfo {
+    pub zero_pos: Vec2,
+    size: f32,
+}
+
+impl Default for GridInfo {
+    fn default() -> Self {
+        Self {
+            zero_pos: Vec2::ZERO,
+            size: DEFAULT_SIZE,
+        }
+    }
+}
+
+impl GridInfo {
+    /// Returns the grid size
+    pub fn size(&self) -> f32 {
+        self.size
+    }
+
+    /// Applies the scroll delta at the given position (in screen coordinates)
+    pub fn apply_scroll_delta(&mut self, delta: f32, pos_s: Vec2) {
+        if delta == 0.0 || delta == f32::NAN {
+            return;
+        }
+
+        let old_size = self.size;
+        let delta = delta.signum() * SCROLL_DELTA;
+        self.size = (self.size + delta).clamp(MIN_SIZE, MAX_SIZE);
+
+        if self.size != old_size {
+            self.zero_pos += (delta / old_size) * (self.zero_pos - pos_s);
+        }
+    }
+
+    /// Grid to screen coordinates transformation
+    pub fn grid_to_screen(&self, p_g: Vec2) -> Vec2 {
+        p_g * self.size + self.zero_pos
+    }
+
+    /// Screen to grid coordinates transformation
+    pub fn screen_to_grid(&self, p_s: Vec2) -> Vec2 {
+        (p_s - self.zero_pos) / self.size
+    }
+}
diff --git a/src/ui/panes/pid_drawing_tool/symbols.rs b/src/ui/panes/pid_drawing_tool/symbols.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6bbbe4c4557b5afeab304c095ef99615ec251021
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/symbols.rs
@@ -0,0 +1,53 @@
+pub mod icons;
+mod labels;
+
+use egui::{Theme, Ui};
+use enum_dispatch::enum_dispatch;
+use glam::Vec2;
+use icons::Icon;
+use labels::Label;
+use serde::{Deserialize, Serialize};
+use strum_macros::{Display, EnumIter};
+
+use crate::mavlink::MavMessage;
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, EnumIter, Display, Debug)]
+#[enum_dispatch]
+pub enum Symbol {
+    Icon(Icon),
+    Label(Label),
+}
+
+impl Default for Symbol {
+    fn default() -> Self {
+        Symbol::Icon(Icon::default())
+    }
+}
+
+#[enum_dispatch(Symbol)]
+pub trait SymbolBehavior {
+    /// Resets the subscriptions settings.
+    /// IMPORTANT: This method should be called every time the msg_id changes.
+    fn reset_subscriptions(&mut self);
+
+    /// Updates the symbol based on the received message.
+    fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32);
+
+    /// Renders the symbol on the UI.
+    fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: Vec2, size: f32, rotation: f32);
+
+    /// Renders further elements related to the subscriptions settings
+    fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32);
+
+    /// Anchor point in grid coordinates relative to the element's center
+    ///
+    /// These vectors include the current rotation of the element.
+    /// They are cached to avoid recomputing the rotation.
+    fn anchor_points(&self) -> Option<Vec<Vec2>>;
+
+    /// Symbol size in grid coordinates
+    fn size(&self) -> Vec2;
+
+    #[allow(unused_variables)]
+    fn context_menu(&mut self, ui: &mut Ui) {}
+}
diff --git a/src/ui/panes/pid_drawing_tool/symbols/icons.rs b/src/ui/panes/pid_drawing_tool/symbols/icons.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fd3378b72d9db4c98733ace0f8935dfcdeadeeac
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/symbols/icons.rs
@@ -0,0 +1,227 @@
+mod motor_valve;
+
+use egui::{ImageSource, Theme, Ui};
+use glam::Vec2;
+use motor_valve::MotorValve;
+use serde::{Deserialize, Serialize};
+use strum_macros::{Display, EnumIter};
+
+use crate::{mavlink::MavMessage, ui::utils::glam_to_egui};
+
+use super::SymbolBehavior;
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, EnumIter, Display, Debug, Default)]
+pub enum Icon {
+    #[default]
+    Arrow,
+    BurstDisk,
+    CheckValve,
+    FlexibleConnection,
+    ManualValve,
+    MotorValve(MotorValve),
+    PressureGauge,
+    PressureRegulator,
+    PressureTransducer,
+    QuickConnector,
+    ReliefValve,
+    ThreeWayValve,
+    Vessel,
+}
+
+impl Icon {
+    pub fn get_image(&self, theme: Theme) -> ImageSource {
+        match (&self, theme) {
+            (Icon::Arrow, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/arrow.svg")
+            }
+            (Icon::Arrow, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/arrow.svg")
+            }
+            (Icon::BurstDisk, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/burst_disk.svg")
+            }
+            (Icon::BurstDisk, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/burst_disk.svg")
+            }
+            (Icon::ManualValve, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/manual_valve.svg")
+            }
+            (Icon::ManualValve, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/manual_valve.svg")
+            }
+            (Icon::CheckValve, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/check_valve.svg")
+            }
+            (Icon::CheckValve, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/check_valve.svg")
+            }
+            (Icon::ReliefValve, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/relief_valve.svg")
+            }
+            (Icon::ReliefValve, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/relief_valve.svg")
+            }
+            (Icon::MotorValve(state), Theme::Light) => match state.last_value {
+                None => {
+                    egui::include_image!("../../../../../icons/pid_symbols/light/motor_valve.svg")
+                }
+                Some(true) => {
+                    egui::include_image!(
+                        "../../../../../icons/pid_symbols/light/motor_valve_green.svg"
+                    )
+                }
+                Some(false) => {
+                    egui::include_image!(
+                        "../../../../../icons/pid_symbols/light/motor_valve_red.svg"
+                    )
+                }
+            },
+            (Icon::MotorValve(state), Theme::Dark) => match state.last_value {
+                None => {
+                    egui::include_image!("../../../../../icons/pid_symbols/dark/motor_valve.svg")
+                }
+                Some(true) => {
+                    egui::include_image!(
+                        "../../../../../icons/pid_symbols/dark/motor_valve_green.svg"
+                    )
+                }
+                Some(false) => {
+                    egui::include_image!(
+                        "../../../../../icons/pid_symbols/dark/motor_valve_red.svg"
+                    )
+                }
+            },
+            (Icon::ThreeWayValve, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/three_way_valve.svg")
+            }
+            (Icon::ThreeWayValve, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/three_way_valve.svg")
+            }
+            (Icon::PressureRegulator, Theme::Light) => {
+                egui::include_image!(
+                    "../../../../../icons/pid_symbols/light/pressure_regulator.svg"
+                )
+            }
+            (Icon::PressureRegulator, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/pressure_regulator.svg")
+            }
+            (Icon::QuickConnector, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/quick_connector.svg")
+            }
+            (Icon::QuickConnector, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/quick_connector.svg")
+            }
+            (Icon::PressureTransducer, Theme::Light) => {
+                egui::include_image!(
+                    "../../../../../icons/pid_symbols/light/pressure_transducer.svg"
+                )
+            }
+            (Icon::PressureTransducer, Theme::Dark) => {
+                egui::include_image!(
+                    "../../../../../icons/pid_symbols/dark/pressure_transducer.svg"
+                )
+            }
+            (Icon::PressureGauge, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/pressure_gauge.svg")
+            }
+            (Icon::PressureGauge, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/pressure_gauge.svg")
+            }
+            (Icon::FlexibleConnection, Theme::Light) => {
+                egui::include_image!(
+                    "../../../../../icons/pid_symbols/light/flexible_connection.svg"
+                )
+            }
+            (Icon::FlexibleConnection, Theme::Dark) => {
+                egui::include_image!(
+                    "../../../../../icons/pid_symbols/dark/flexible_connection.svg"
+                )
+            }
+            (Icon::Vessel, Theme::Light) => {
+                egui::include_image!("../../../../../icons/pid_symbols/light/vessel.svg")
+            }
+            (Icon::Vessel, Theme::Dark) => {
+                egui::include_image!("../../../../../icons/pid_symbols/dark/vessel.svg")
+            }
+        }
+    }
+}
+
+impl SymbolBehavior for Icon {
+    fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) {
+        if let Icon::MotorValve(state) = self {
+            state.update(message, subscribed_msg_id)
+        }
+    }
+
+    fn reset_subscriptions(&mut self) {
+        if let Icon::MotorValve(state) = self {
+            state.reset_subscriptions()
+        }
+    }
+
+    fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: glam::Vec2, size: f32, rotation: f32) {
+        let center = glam_to_egui(pos).to_pos2();
+        let image_rect = egui::Rect::from_min_size(center, glam_to_egui(self.size() * size));
+        egui::Image::new(self.get_image(theme))
+            .rotate(rotation, egui::Vec2::splat(0.0))
+            .paint_at(ui, image_rect);
+    }
+
+    fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) {
+        if let Icon::MotorValve(state) = self {
+            state.subscriptions_ui(ui, mavlink_id)
+        }
+    }
+
+    fn context_menu(&mut self, ui: &mut Ui) {
+        if let Icon::MotorValve(state) = self {
+            if ui.button("Icon subscription settings…").clicked() {
+                state.is_subs_window_visible = true;
+                ui.close_menu();
+            }
+        }
+    }
+
+    fn anchor_points(&self) -> Option<Vec<glam::Vec2>> {
+        Some(
+            match self {
+                Icon::Arrow => vec![(0.0, 2.0), (4.0, 2.0)],
+                Icon::BurstDisk => vec![(0.0, 3.0), (4.0, 3.0)],
+                Icon::CheckValve => vec![(0.0, 2.5), (10.0, 2.5)],
+                Icon::FlexibleConnection => vec![(0.0, 3.0), (10.0, 3.0)],
+                Icon::ManualValve => vec![(0.0, 2.5), (10.0, 2.5)],
+                Icon::MotorValve(_) => vec![(0.0, 5.0), (10.0, 5.0)],
+                Icon::PressureGauge => vec![(3.5, 7.0)],
+                Icon::PressureRegulator => vec![(0.0, 7.0), (10.0, 7.0)],
+                Icon::PressureTransducer => vec![(3.5, 7.0)],
+                Icon::QuickConnector => vec![(0.0, 2.5), (6.0, 2.5)],
+                Icon::ReliefValve => vec![(3.0, 10.0)],
+                Icon::ThreeWayValve => vec![(0.0, 3.0), (10.0, 3.0), (5.0, 8.0)],
+                Icon::Vessel => vec![(0.0, 7.6), (8.2, 7.6), (4.1, 0.0), (4.1, 15.1)],
+            }
+            .iter()
+            .map(|&p| p.into())
+            .collect(),
+        )
+    }
+
+    fn size(&self) -> Vec2 {
+        match self {
+            Icon::Arrow => (4.0, 4.0),
+            Icon::BurstDisk => (4.0, 6.0),
+            Icon::CheckValve => (10.0, 5.0),
+            Icon::FlexibleConnection => (10.0, 6.0),
+            Icon::ManualValve => (10.0, 5.0),
+            Icon::MotorValve(_) => (10.0, 8.0),
+            Icon::PressureGauge => (7.0, 7.0),
+            Icon::PressureRegulator => (10.0, 10.0),
+            Icon::PressureTransducer => (7.0, 7.0),
+            Icon::QuickConnector => (6.0, 5.0),
+            Icon::ReliefValve => (6.0, 10.0),
+            Icon::ThreeWayValve => (10.0, 8.0),
+            Icon::Vessel => (8.2, 15.2),
+        }
+        .into()
+    }
+}
diff --git a/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs b/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1dd94786496619faae503531191fe0eb9637b770
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/symbols/icons/motor_valve.rs
@@ -0,0 +1,85 @@
+use egui::{RichText, Ui, Window};
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    MAVLINK_PROFILE,
+    error::ErrInstrument,
+    mavlink::{MavMessage, Message, reflection::IndexedField},
+    ui::cache::ChangeTracker,
+};
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, Default, Debug)]
+pub struct MotorValve {
+    subscribed_field: Option<IndexedField>,
+
+    /// false = closed, true = open
+    #[serde(skip)]
+    pub last_value: Option<bool>,
+    #[serde(skip)]
+    pub is_subs_window_visible: bool,
+}
+
+impl MotorValve {
+    pub fn update(&mut self, msg: &MavMessage, subscribed_msg_id: u32) {
+        // Reset field if msg_id has changed
+        if let Some(inner_field) = &self.subscribed_field {
+            if inner_field.msg_id() != subscribed_msg_id {
+                self.subscribed_field = None;
+            }
+        }
+
+        if let Some(field) = &self.subscribed_field {
+            if msg.message_id() == subscribed_msg_id {
+                let value = field.extract_as_f64(msg).log_unwrap();
+                self.last_value = Some(value != 0.0);
+            }
+        }
+    }
+
+    pub fn reset_subscriptions(&mut self) {
+        self.subscribed_field = None;
+        self.last_value = None;
+    }
+
+    pub fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) {
+        let change_tracker = ChangeTracker::record_initial_state(&self.subscribed_field);
+        Window::new("Subscriptions")
+            .id(ui.auto_id_with("subs_settings"))
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut self.is_subs_window_visible)
+            .show(ui.ctx(), |ui| {
+                subscription_window(ui, mavlink_id, &mut self.subscribed_field)
+            });
+        // reset last_value if the subscribed field has changed
+        if change_tracker.has_changed(&self.subscribed_field) {
+            self.last_value = None;
+        }
+    }
+}
+
+fn subscription_window(ui: &mut Ui, msg_id: u32, field: &mut Option<IndexedField>) {
+    // Get all fields available for subscription
+    let fields = MAVLINK_PROFILE.get_all_state_fields(msg_id).log_unwrap();
+
+    // If no fields available for subscription
+    if fields.is_empty() {
+        ui.label(
+            RichText::new("No fields available for subscription")
+                .underline()
+                .strong(),
+        );
+        return;
+    };
+
+    // Otherwise, select the first field available
+    let field = field.get_or_insert(fields[0].to_owned());
+    egui::ComboBox::from_label("field")
+        .selected_text(&field.field().name)
+        .show_ui(ui, |ui| {
+            for msg in fields.iter() {
+                ui.selectable_value(field, msg.to_owned(), &msg.field().name);
+            }
+        });
+}
diff --git a/src/ui/panes/pid_drawing_tool/symbols/labels.rs b/src/ui/panes/pid_drawing_tool/symbols/labels.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e20c55140a091fc3146f4d60a542fa33ae01c1d1
--- /dev/null
+++ b/src/ui/panes/pid_drawing_tool/symbols/labels.rs
@@ -0,0 +1,153 @@
+use serde::{Deserialize, Serialize};
+
+use egui::{
+    Align2, Color32, CornerRadius, FontId, RichText, Stroke, StrokeKind, Theme, Ui, Window,
+};
+use glam::Vec2;
+
+use crate::{
+    MAVLINK_PROFILE,
+    error::ErrInstrument,
+    mavlink::{MavMessage, Message, reflection::IndexedField},
+    ui::{
+        cache::ChangeTracker,
+        utils::{egui_to_glam, glam_to_egui},
+    },
+};
+
+use super::SymbolBehavior;
+
+const FONT_SIZE: f32 = 2.0;
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub struct Label {
+    subscribed_field: Option<IndexedField>,
+    size: Vec2,
+
+    #[serde(skip)]
+    last_value: Option<f32>,
+    #[serde(skip)]
+    is_subs_window_visible: bool,
+}
+
+impl Default for Label {
+    fn default() -> Self {
+        Self {
+            subscribed_field: None,
+            last_value: Some(0.0),
+            size: Vec2::new(FONT_SIZE * 0.6 * 4.0, FONT_SIZE),
+            is_subs_window_visible: false,
+        }
+    }
+}
+
+impl SymbolBehavior for Label {
+    fn update(&mut self, message: &MavMessage, subscribed_msg_id: u32) {
+        if let Some(subscribed_field) = &self.subscribed_field {
+            if message.message_id() == subscribed_msg_id {
+                let value = subscribed_field.extract_as_f64(message).log_unwrap();
+                self.last_value = Some(value as f32);
+            }
+        }
+    }
+
+    fn reset_subscriptions(&mut self) {
+        self.subscribed_field = None;
+        self.last_value = None;
+    }
+
+    fn paint(&mut self, ui: &mut Ui, theme: Theme, pos: Vec2, size: f32, _: f32) {
+        let painter = ui.painter();
+        let color = match theme {
+            Theme::Light => Color32::BLACK,
+            Theme::Dark => Color32::WHITE,
+        };
+
+        let unit = self
+            .subscribed_field
+            .as_ref()
+            .and_then(|f| f.field().unit.as_deref())
+            .unwrap_or("");
+        let text = match self.last_value {
+            Some(value) => format!("{:.2} {}", value, unit),
+            None => "N/A".to_string(),
+        };
+        let text_rect = painter.text(
+            glam_to_egui(pos).to_pos2(),
+            Align2::LEFT_TOP,
+            text,
+            FontId::monospace(FONT_SIZE * size),
+            color,
+        );
+        self.size = egui_to_glam(text_rect.size()) / size;
+        painter.rect(
+            egui::Rect::from_min_size(
+                glam_to_egui(pos).to_pos2(),
+                glam_to_egui(self.size()) * size,
+            ),
+            CornerRadius::ZERO,
+            Color32::TRANSPARENT,
+            Stroke::NONE,
+            StrokeKind::Middle,
+        );
+    }
+
+    fn subscriptions_ui(&mut self, ui: &mut Ui, mavlink_id: u32) {
+        let change_tracker = ChangeTracker::record_initial_state(&self.subscribed_field);
+        Window::new("Subscriptions")
+            .id(ui.auto_id_with("subs_settings"))
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut self.is_subs_window_visible)
+            .show(ui.ctx(), |ui| {
+                subscription_window(ui, mavlink_id, &mut self.subscribed_field)
+            });
+        // reset last_value if the subscribed field has changed
+        if change_tracker.has_changed(&self.subscribed_field) {
+            self.last_value = None;
+        }
+    }
+
+    fn context_menu(&mut self, ui: &mut Ui) {
+        if ui.button("Label subscription settings…").clicked() {
+            self.is_subs_window_visible = true;
+            ui.close_menu();
+        }
+    }
+
+    fn anchor_points(&self) -> Option<Vec<Vec2>> {
+        None
+    }
+
+    fn size(&self) -> Vec2 {
+        self.size
+    }
+}
+
+fn subscription_window(ui: &mut Ui, msg_id: u32, field: &mut Option<IndexedField>) {
+    // Get all fields available for subscription
+    let fields = MAVLINK_PROFILE
+        .get_plottable_fields(msg_id)
+        .log_expect("Invalid message id");
+
+    // If no fields available for subscription
+    if fields.is_empty() {
+        ui.label(
+            RichText::new("No fields available for subscription")
+                .underline()
+                .strong(),
+        );
+        return;
+    }
+
+    // Otherwise, select the first field available
+    let field = field.get_or_insert(fields[0].to_owned());
+    egui::ComboBox::from_label("field")
+        .selected_text(&field.field().name)
+        .show_ui(ui, |ui| {
+            for msg in fields.iter() {
+                ui.selectable_value(field, msg.to_owned(), &msg.field().name);
+            }
+        });
+}
diff --git a/src/ui/utils.rs b/src/ui/utils.rs
index 7ad863175181d3b06766858c500a07707bcefc79..0c2ae3f7257254b414d9974507003ee1443a30bf 100644
--- a/src/ui/utils.rs
+++ b/src/ui/utils.rs
@@ -42,3 +42,15 @@ pub fn vertically_centered(
         .response
     }
 }
+
+#[inline(always)]
+pub fn egui_to_glam(p: egui::Vec2) -> glam::Vec2 {
+    let p: mint::Vector2<f32> = p.into();
+    glam::Vec2::from(p)
+}
+
+#[inline(always)]
+pub fn glam_to_egui(p: glam::Vec2) -> egui::Vec2 {
+    let p: mint::Vector2<f32> = p.into();
+    egui::Vec2::from(p)
+}