diff --git a/Cargo.lock b/Cargo.lock index 16d58e4d9d04a2f92c5a47d836623cc0f916c8d3..cfcee9b60b6a6ca085468b80076080ff18249e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,6 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" dependencies = [ "bytemuck", + "mint", "serde", ] @@ -1461,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" @@ -2128,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" @@ -3081,7 +3098,9 @@ dependencies = [ "egui_plot", "egui_tiles", "enum_dispatch", + "glam", "mavlink-bindgen", + "mint", "profiling", "rand 0.9.0", "ring-channel", @@ -3097,6 +3116,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "tracing-tracy", + "uuid", ] [[package]] @@ -3921,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" diff --git a/Cargo.toml b/Cargo.toml index 5aa24050b589ef3324aa5fefdda46da754dda764..819e571068083cb0aa8e10b32407d5a394cda88c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" # ======= GUI & Rendering ======= egui_tiles = "0.12" eframe = { version = "0.31", features = ["persistence"] } -egui = { version = "0.31", features = ["log"] } +egui = { version = "0.31", features = ["log", "mint"] } egui_extras = { version = "0.31", features = ["svg"] } egui_plot = "0.31" egui_file = "0.22" @@ -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/icons/check_valve_dark.svg b/icons/check_valve_dark.svg deleted file mode 100644 index 6f957f16703f9e9cd8fb7f5ca1c33677deddca85..0000000000000000000000000000000000000000 --- a/icons/check_valve_dark.svg +++ /dev/null @@ -1,56 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="50" - height="50" - version="1.1" - viewBox="0 0 50 50" - xml:space="preserve" - id="svg4" - sodipodi:docname="check_valve_dark.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview - id="namedview4" - pagecolor="#000000" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="true" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="px" - inkscape:zoom="11.313709" - inkscape:cx="3.0935922" - inkscape:cy="30.714951" - inkscape:window-width="1920" - inkscape:window-height="1131" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="svg4" /><defs - id="defs1" /><g - stroke="#000000" - id="g3" - transform="matrix(3.779528,0,0,3.77953,-106.2768,168.61347)" - style="stroke:#ffffff"><circle - cx="29.749052" - cy="-40.397133" - r="1.1778001" - fill-opacity="0.98911" - stroke-linecap="round" - stroke-width="0.3" - id="circle2" - style="stroke:#ffffff;fill:#ffffff" /><path - d="m 29.754599,-40.469906 4.979,2.4722 -4.979,2.4722 z m 9.9581,0 -4.979,2.4722 4.979,2.4722 z" - fill="none" - pointer-events="all" - stroke-linejoin="round" - stroke-miterlimit="10" - stroke-width="0.3" - id="path3" - sodipodi:nodetypes="cccccccc" - style="stroke:#ffffff" /></g></svg> diff --git a/icons/check_valve_light.svg b/icons/check_valve_light.svg deleted file mode 100644 index edbdcbe89ecaa955a91b59154cd7b108595a913a..0000000000000000000000000000000000000000 --- a/icons/check_valve_light.svg +++ /dev/null @@ -1,53 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="50" - height="50" - version="1.1" - viewBox="0 0 50 50" - xml:space="preserve" - id="svg4" - sodipodi:docname="check_valve.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview - id="namedview4" - pagecolor="#000000" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="true" - inkscape:deskcolor="#d1d1d1" - inkscape:document-units="px" - inkscape:zoom="11.313709" - inkscape:cx="3.0935922" - inkscape:cy="30.714951" - inkscape:window-width="1920" - inkscape:window-height="1131" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="svg4" /><defs - id="defs1" /><g - stroke="#000000" - id="g3" - transform="matrix(3.779528,0,0,3.77953,-106.2768,168.61347)"><circle - cx="29.749052" - cy="-40.397133" - r="1.1778001" - fill-opacity="0.98911" - stroke-linecap="round" - stroke-width="0.3" - id="circle2" /><path - d="m 29.754599,-40.469906 4.979,2.4722 -4.979,2.4722 z m 9.9581,0 -4.979,2.4722 4.979,2.4722 z" - fill="none" - pointer-events="all" - stroke-linejoin="round" - stroke-miterlimit="10" - stroke-width="0.3" - id="path3" - sodipodi:nodetypes="cccccccc" /></g></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..f3694e49a605ff5d430e3a971380953739e558c2 --- /dev/null +++ b/icons/pid_symbols/light/check_valve.svg @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="10" + height="5" + version="1.1" + id="svg1" + sodipodi:docname="2_check_valve.svg" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 0 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="10 : 0 : 1" + inkscape:persp3d-origin="5 : -1.6666667 : 1" + id="perspective3" /> + </defs> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="true" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="64" + inkscape:cx="3.7109375" + inkscape:cy="5.2421875" + inkscape:window-width="1996" + inkscape:window-height="1371" + inkscape:window-x="20" + inkscape:window-y="20" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" + showgrid="true"> + <inkscape:grid + id="grid3" + units="px" + originx="0" + originy="0" + spacingx="0.1" + spacingy="0.1" + empcolor="#0099e5" + empopacity="0.30196078" + color="#0099e5" + opacity="0.14901961" + empspacing="5" + enabled="true" + visible="true" /> + </sodipodi:namedview> + <circle + style="fill:#000000;stroke:none" + id="path2" + inkscape:label="path4" + 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" + sodipodi:nodetypes="ccccc" + inkscape:label="path3" /> + <path + fill="none" + stroke="#000000" + d="M 0,2.5 H 1 Z" + id="path1-5" + style="stroke-width:0.2;stroke-dasharray:none" + sodipodi:nodetypes="ccccc" + inkscape:label="path2" /> + <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" + sodipodi:nodetypes="cccc" /> +</svg> diff --git a/icons/pid_symbols/light/empty.svg b/icons/pid_symbols/light/empty.svg new file mode 100644 index 0000000000000000000000000000000000000000..7abdd75191ecfb26c8cc61827f94d06ae1b9ea65 --- /dev/null +++ b/icons/pid_symbols/light/empty.svg @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="10" + height="10" + version="1.1" + id="svg1" + sodipodi:docname="0_empty.svg" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="true" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="93.176471" + inkscape:cx="4.3787879" + inkscape:cy="3.2143308" + inkscape:window-width="1996" + inkscape:window-height="1371" + inkscape:window-x="20" + inkscape:window-y="20" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" + showgrid="true"> + <inkscape:grid + id="grid3" + units="px" + originx="0" + originy="0" + spacingx="0.1" + spacingy="0.1" + empcolor="#0099e5" + empopacity="0.30196078" + color="#0099e5" + opacity="0.14901961" + empspacing="5" + enabled="true" + visible="true" /> + </sodipodi:namedview> +</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..b66100b8b557235ed136cedef7c62d6491378f53 --- /dev/null +++ b/icons/pid_symbols/light/manual_valve.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="10" + height="5" + version="1.1" + id="svg1" + sodipodi:docname="1_manual_valve.svg" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 0 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="10 : 0 : 1" + inkscape:persp3d-origin="5 : -1.6666667 : 1" + id="perspective3" /> + </defs> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="true" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="64" + inkscape:cx="3.7109375" + inkscape:cy="5.2421875" + inkscape:window-width="1996" + inkscape:window-height="1371" + inkscape:window-x="20" + inkscape:window-y="20" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" + showgrid="true"> + <inkscape:grid + id="grid3" + units="px" + originx="0" + originy="0" + spacingx="0.1" + spacingy="0.1" + empcolor="#0099e5" + empopacity="0.30196078" + color="#0099e5" + opacity="0.14901961" + empspacing="5" + enabled="true" + visible="true" /> + </sodipodi:namedview> + <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" + sodipodi:nodetypes="ccccc" + inkscape:label="path3" /> + <path + fill="none" + stroke="#000000" + d="M 0,2.5 H 1 Z" + id="path1-5" + style="stroke-width:0.2;stroke-dasharray:none" + sodipodi:nodetypes="ccccc" + inkscape:label="path2" /> + <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-dasharray:none" + sodipodi:nodetypes="cccc" /> +</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..eae812d1adaf9afedcaf6b36abb28677d8f50b7d --- /dev/null +++ b/icons/pid_symbols/light/motor_valve.svg @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="10" + height="7.5" + version="1.1" + id="svg1" + sodipodi:docname="5_motor_valve.svg" + inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="false" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="90.509668" + inkscape:cx="4.3752232" + inkscape:cy="2.4362038" + inkscape:window-width="1996" + inkscape:window-height="1371" + inkscape:window-x="20" + inkscape:window-y="20" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" + showgrid="true"> + <inkscape:grid + id="grid3" + units="px" + originx="0" + originy="0" + spacingx="0.1" + spacingy="0.1" + empcolor="#0099e5" + empopacity="0.30196078" + color="#0099e5" + opacity="0.14901961" + empspacing="5" + enabled="true" + visible="true" /> + </sodipodi:namedview> + <path + fill="none" + stroke="#000000" + d="M 1,3 V 7 L 9,3 v 4 z" + id="path1" + style="stroke-width:0.225" + sodipodi:nodetypes="ccccc" /> + <path + fill="none" + stroke="#000000" + d="M 0,5 H 1 Z" + id="path2" + style="stroke-width:0.225" + sodipodi:nodetypes="ccccc" /> + <path + fill="none" + stroke="#000000" + d="M 5,2.5 L 5,5 Z" + id="path3" + style="stroke-width:0.225" + sodipodi:nodetypes="ccccc" /> + <path + fill="none" + stroke="#000000" + d="m 9,5 h 1 z" + id="path4" + style="stroke-width:0.225" + sodipodi:nodetypes="ccccc" /> + <circle + style="fill:none;stroke:#000000;stroke-width:0.2;stroke-opacity:1" + id="circle1" + cx="5" + cy="1.5" + r="1" /> + <path + style="font-size:1.33333px;-inkscape-font-specification:'sans-serif, Normal';fill:#000000;stroke:none;stroke-width:0.2;fill-opacity:1" + d="M 4.9446681,1.9759989 4.6313347,1.1413322 h -0.00533 q 0.00267,0.026667 0.004,0.068 0.00267,0.041333 0.004,0.090667 0.00133,0.048 0.00133,0.098667 V 1.9759989 H 4.5246681 v -0.952 h 0.1773333 l 0.2933333,0.78 h 0.00533 l 0.2986666,-0.78 h 0.1760001 v 0.952 H 5.3566681 V 1.3906655 q 0,-0.046667 0.00133,-0.092 0.00133,-0.046667 0.004,-0.086667 0.00267,-0.041333 0.004,-0.069333 h -0.00533 L 5.0433347,1.9759989 Z" + id="text1" + inkscape:transform-center-x="0.034572172" + inkscape:transform-center-y="-0.11235956" + aria-label="M" /> +</svg> diff --git a/icons/pressurized_vessel_dark.svg b/icons/pressurized_vessel_dark.svg deleted file mode 100644 index 4b5d69bd5b4ce2091ae06120f15ab57991e68423..0000000000000000000000000000000000000000 --- a/icons/pressurized_vessel_dark.svg +++ /dev/null @@ -1,111 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="50mm" - height="50mm" - id="svg2" - sodipodi:version="0.32" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" - version="1.0" - sodipodi:docname="pressurized_vessel_dark.svg" - inkscape:output_extension="org.inkscape.output.svg.inkscape" - viewBox="0 0 50 50" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:dc="http://purl.org/dc/elements/1.1/"> - <defs - id="defs4" /> - <sodipodi:namedview - id="base" - pagecolor="#000000" - bordercolor="#000000" - borderopacity="0.10196078" - gridtolerance="10000" - guidetolerance="10" - objecttolerance="10" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="2.8" - inkscape:cx="54.821429" - inkscape:cy="74.464286" - inkscape:document-units="mm" - inkscape:current-layer="layer1" - width="20mm" - height="20mm" - units="mm" - inkscape:window-width="1920" - inkscape:window-height="1131" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:showpageshadow="2" - inkscape:pagecheckerboard="true" - inkscape:deskcolor="#d1d1d1" - inkscape:window-maximized="1" /> - <metadata - id="metadata7"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1" - style="stroke:#ffffff"> - <g - id="g2774" - transform="matrix(0.78244262,0,0,0.78244262,-2.7241867,-2.3262084)" - style="fill:none;stroke:#ffffff"> - <path - sodipodi:open="true" - sodipodi:end="3.1415927" - sodipodi:start="0.017453293" - transform="matrix(1.4090631,0,0,1.7600641,-3.2596449,30.294269)" - d="m 37.203241,13.364701 a 9.7440948,4.4291339 0 0 1 -9.827643,4.351666 9.7440948,4.4291339 0 0 1 -9.659062,-4.428965" - sodipodi:ry="4.4291339" - sodipodi:rx="9.7440948" - sodipodi:cy="13.287402" - sodipodi:cx="27.46063" - id="path2865" - style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.674995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - sodipodi:type="arc" - sodipodi:arc-type="arc" /> - <path - sodipodi:open="true" - sodipodi:end="3.1415927" - sodipodi:start="0.017453293" - transform="matrix(1.4089528,0,0,-1.3269204,-3.2566161,31.802164)" - d="m 37.203241,13.364701 a 9.7440948,4.4291339 0 0 1 -9.827643,4.351666 9.7440948,4.4291339 0 0 1 -9.659062,-4.428965" - sodipodi:ry="4.4291339" - sodipodi:rx="9.7440948" - sodipodi:cy="13.287402" - sodipodi:cx="27.46063" - id="path3248" - style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.777427;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - sodipodi:type="arc" - sodipodi:arc-type="arc" /> - <path - id="path3250" - d="m 21.711705,13.722069 c 0,40.782784 0,40.782784 0,40.782784" - style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1.06299;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;enable-background:accumulate" /> - <path - id="path3252" - d="m 49.163386,13.713031 c 0,40.79173 0,40.79173 0,40.79173" - style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1.06299;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;enable-background:accumulate" /> - <path - id="path2772" - d="m 33.467862,60.827788 c -3.577648,-0.413125 -6.10002,-1.215482 -8.214142,-2.612883 -1.024074,-0.676902 -2.204101,-1.968578 -2.580352,-2.8245 -0.378258,-0.860483 -0.364238,-0.03958 -0.365254,-21.38872 l -9.66e-4,-20.317289 0.233492,-0.468565 c 1.031623,-2.070193 4.817372,-3.6605684 10.034365,-4.2153907 1.367601,-0.1454429 4.725174,-0.1186857 6.160715,0.049095 4.897705,0.5724277 8.598366,2.1804967 9.587937,4.1662967 l 0.233491,0.468564 -9.55e-4,20.317289 c -0.0011,22.298611 0.04107,20.607328 -0.540759,21.698958 -1.275447,2.393011 -4.759848,4.264225 -9.235072,4.959511 -0.934233,0.145142 -4.525825,0.258473 -5.3125,0.167634 z" - style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.09491;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> - </g> - </g> -</svg> diff --git a/icons/pressurized_vessel_light.svg b/icons/pressurized_vessel_light.svg deleted file mode 100644 index 6cb7b41184dbe266427102c6e3e27042a32000c5..0000000000000000000000000000000000000000 --- a/icons/pressurized_vessel_light.svg +++ /dev/null @@ -1,110 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - width="50mm" - height="50mm" - id="svg2" - sodipodi:version="0.32" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" - version="1.0" - sodipodi:docname="pressurized_vessel.svg" - inkscape:output_extension="org.inkscape.output.svg.inkscape" - viewBox="0 0 50 50" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:dc="http://purl.org/dc/elements/1.1/"> - <defs - id="defs4" /> - <sodipodi:namedview - id="base" - pagecolor="#000000" - bordercolor="#000000" - borderopacity="0.10196078" - gridtolerance="10000" - guidetolerance="10" - objecttolerance="10" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="2.8" - inkscape:cx="54.821429" - inkscape:cy="74.464286" - inkscape:document-units="mm" - inkscape:current-layer="layer1" - width="20mm" - height="20mm" - units="mm" - inkscape:window-width="1920" - inkscape:window-height="1131" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:showpageshadow="2" - inkscape:pagecheckerboard="true" - inkscape:deskcolor="#d1d1d1" - inkscape:window-maximized="1" /> - <metadata - id="metadata7"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1"> - <g - id="g2774" - transform="matrix(0.78244262,0,0,0.78244262,-2.7241867,-2.3262084)" - style="fill:none"> - <path - sodipodi:open="true" - sodipodi:end="3.1415927" - sodipodi:start="0.017453293" - transform="matrix(1.4090631,0,0,1.7600641,-3.2596449,30.294269)" - d="m 37.203241,13.364701 a 9.7440948,4.4291339 0 0 1 -9.827643,4.351666 9.7440948,4.4291339 0 0 1 -9.659062,-4.428965" - sodipodi:ry="4.4291339" - sodipodi:rx="9.7440948" - sodipodi:cy="13.287402" - sodipodi:cx="27.46063" - id="path2865" - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.674995;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - sodipodi:type="arc" - sodipodi:arc-type="arc" /> - <path - sodipodi:open="true" - sodipodi:end="3.1415927" - sodipodi:start="0.017453293" - transform="matrix(1.4089528,0,0,-1.3269204,-3.2566161,31.802164)" - d="m 37.203241,13.364701 a 9.7440948,4.4291339 0 0 1 -9.827643,4.351666 9.7440948,4.4291339 0 0 1 -9.659062,-4.428965" - sodipodi:ry="4.4291339" - sodipodi:rx="9.7440948" - sodipodi:cy="13.287402" - sodipodi:cx="27.46063" - id="path3248" - style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.777427;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" - sodipodi:type="arc" - sodipodi:arc-type="arc" /> - <path - id="path3250" - d="m 21.711705,13.722069 c 0,40.782784 0,40.782784 0,40.782784" - style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.06299;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;enable-background:accumulate" /> - <path - id="path3252" - d="m 49.163386,13.713031 c 0,40.79173 0,40.79173 0,40.79173" - style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.06299;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;enable-background:accumulate" /> - <path - id="path2772" - d="m 33.467862,60.827788 c -3.577648,-0.413125 -6.10002,-1.215482 -8.214142,-2.612883 -1.024074,-0.676902 -2.204101,-1.968578 -2.580352,-2.8245 -0.378258,-0.860483 -0.364238,-0.03958 -0.365254,-21.38872 l -9.66e-4,-20.317289 0.233492,-0.468565 c 1.031623,-2.070193 4.817372,-3.6605684 10.034365,-4.2153907 1.367601,-0.1454429 4.725174,-0.1186857 6.160715,0.049095 4.897705,0.5724277 8.598366,2.1804967 9.587937,4.1662967 l 0.233491,0.468564 -9.55e-4,20.317289 c -0.0011,22.298611 0.04107,20.607328 -0.540759,21.698958 -1.275447,2.393011 -4.759848,4.264225 -9.235072,4.959511 -0.934233,0.145142 -4.525825,0.258473 -5.3125,0.167634 z" - style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.09491;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> - </g> - </g> -</svg> diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index a512ce418ae140ab8525720b3e472c22dcb4ea5a..c2ee75c6ec9392fba42e23116bdf2bc219d03777 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -1,31 +1,27 @@ mod connections; mod elements; mod grid; -mod pos; mod symbols; use connections::Connection; use core::f32; -use egui::{ - epaint::PathStroke, Color32, Context, CursorIcon, Painter, PointerButton, Pos2, Rounding, - Sense, Stroke, Theme, Ui, Vec2, -}; +use egui::{Color32, Context, CursorIcon, PointerButton, Response, Sense, Theme, Ui}; use elements::Element; -use grid::{GridInfo, LINE_THICKNESS}; -use pos::Pos; +use glam::Vec2; +use grid::GridInfo; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; use strum::IntoEnumIterator; use symbols::Symbol; -use crate::ui::composable_view::PaneResponse; +use crate::ui::{composable_view::PaneResponse, utils::egui_to_glam}; use super::PaneBehavior; #[derive(Clone, Serialize, Deserialize, PartialEq)] enum Action { Connect(usize), - ContextMenu(Pos2), + ContextMenu(Vec2), DragElement(usize), DragConnection(usize, usize), DragGrid, @@ -59,93 +55,32 @@ impl PaneBehavior for PidPane { if self.editable { self.draw_grid(ui, theme); } - - self.draw_connections(ui, theme, self.editable); + self.draw_connections(ui, theme); self.draw_elements(ui, theme); - // Allocate the space to sense inputs + // Handle things that require knowing the position of the pointer let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); - let pointer_pos = response.hover_pos(); - - if let Some(pointer_pos) = pointer_pos { + 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); + self.handle_zoom(ui, theme, pointer_pos); } - } - // Set grab icon when hovering an element - if let Some(pointer_pos) = &pointer_pos { - if self.editable - && (self.is_hovering_element(pointer_pos) - || self.is_hovering_connection_point(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); } - } - // Detect the action - if let Some(pointer_pos) = &pointer_pos { - if response.clicked_by(PointerButton::Secondary) { - println!("Context menu opened"); - 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); - println!("Grid drag started"); - } else if let Some(drag_element_action) = self - .find_hovered_element_idx(pointer_pos) - .map(Action::DragElement) - { - self.action = Some(drag_element_action); - println!("Element drag started"); - } else if let Some(drag_connection_point) = self - .find_hovered_connection_point(pointer_pos) - .map(|(idx1, idx2)| Action::DragConnection(idx1, idx2)) - { - self.action = Some(drag_connection_point); - println!("Connection point drag started"); - } - } else if response.drag_stopped() { - self.action.take(); - println!("Drag stopped"); - } - } + self.detect_action(&response, pointer_pos); + self.handle_actions(&response, pointer_pos); } - // Context menu + // 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)); - } - - // Connect action - if let Some(pointer_pos) = pointer_pos { - match self.action { - Some(Action::Connect(start)) => { - if let Some(end) = self.find_hovered_element_idx(&pointer_pos) { - if response.clicked() { - if start != end { - self.connections.push(Connection::new(start, 0, end, 0)); - println!("Added connection from {} to {}", start, end); - } - self.action.take(); - println!("Connect action ended"); - } - } - } - Some(Action::DragElement(idx)) => { - self.elements[idx].position = Pos::from_pos2(&self.grid, &pointer_pos) - } - Some(Action::DragConnection(conn_idx, midpoint_idx)) => { - self.connections[conn_idx].middle_points[midpoint_idx] = - Pos::from_pos2(&self.grid, &pointer_pos); - } - Some(Action::DragGrid) => { - self.grid.zero_pos += response.drag_delta(); - } - _ => {} - } + response.context_menu(|ui| self.draw_context_menu(ui, pointer_pos)); } PaneResponse::default() @@ -157,20 +92,6 @@ impl PaneBehavior for PidPane { } impl PidPane { - fn is_hovering_element(&self, pointer_pos: &Pos2) -> bool { - self.elements - .iter() - .any(|element| element.contains(&self.grid, pointer_pos)) - } - - fn is_hovering_connection_point(&self, pointer_pos: &Pos2) -> bool { - self.connections.iter().any(|conn| { - conn.middle_points - .iter() - .any(|p| p.distance(&self.grid, pointer_pos) < 10.0) - }) - } - /// Returns the currently used theme fn find_theme(ctx: &Context) -> Theme { // In Egui you can either decide a theme or use the system one. @@ -193,37 +114,32 @@ impl PidPane { } } - fn find_hovered_element_idx(&self, pos: &Pos2) -> Option<usize> { + /// 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, pos)) - } - - fn find_hovered_element_mut(&mut self, pos: &Pos2) -> Option<&mut Element> { - self.elements - .iter_mut() - .find(|element| element.contains(&self.grid, pos)) + .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 find_hovered_connection_idx(&self, pos: &Pos2) -> Option<(usize, usize)> { + fn hovers_connection(&self, p_s: Vec2) -> Option<(usize, usize)> { self.connections .iter() .enumerate() - .find_map(|(idx, conn)| Some(idx).zip(conn.contains(self, pos))) + .find_map(|(conn_idx, conn)| { + let segm_idx = conn.contains(self, p_s); + Some(conn_idx).zip(segm_idx) + }) } - fn find_hovered_connection_point(&self, pos: &Pos2) -> Option<(usize, usize)> { - let mut midpoint_idx = Some(0); - let connection_idx = self.connections.iter().position(|conn| { - midpoint_idx = conn - .middle_points - .iter() - .position(|p| p.distance(&self.grid, pos) < 12.0); - midpoint_idx.is_some() - }); - - connection_idx.zip(midpoint_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) { @@ -231,24 +147,20 @@ impl PidPane { let window_rect = ui.max_rect(); let dot_color = PidPane::dots_color(theme); - let offset_x = (self.grid.zero_pos.x % self.grid.get_size()) as i32; - let offset_y = (self.grid.zero_pos.y % self.grid.get_size()) as i32; + 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.get_size()) as i32 - * self.grid.get_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 end_x = (window_rect.max.x / self.grid.get_size() + 2.0) as i32 - * self.grid.get_size() as i32 - + offset_x; - let start_y = (window_rect.min.y / self.grid.get_size()) as i32 - * self.grid.get_size() as i32 - + offset_y; - let end_y = (window_rect.max.y / self.grid.get_size() + 2.0) as i32 - * self.grid.get_size() as i32 + 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.get_size() as usize) { - for y in (start_y..end_y).step_by(self.grid.get_size() as usize) { + 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(1.0, 1.0), @@ -258,146 +170,75 @@ impl PidPane { } } - fn draw_connections(&self, ui: &Ui, theme: Theme, draw_handles: bool) { + fn draw_connections(&self, ui: &Ui, theme: Theme) { let painter = ui.painter(); - let color = match theme { - Theme::Light => Color32::BLACK, - Theme::Dark => Color32::WHITE, - }; - // Each connection is composed from multiple lines for conn in &self.connections { - let start = self.elements[conn.start].get_anchor(&self.grid, conn.start_anchor); - let end = self.elements[conn.end].get_anchor(&self.grid, conn.end_anchor); - - let points: Vec<Pos2> = conn - .middle_points - .iter() - .map(|p| p.to_pos2(&self.grid)) - .collect(); - - // Draw line segments - if points.is_empty() { - self.draw_connection_segment(painter, color, start, end); - } else { - self.draw_connection_segment(painter, color, start, *points.first().unwrap()); - for i in 0..(points.len() - 1) { - self.draw_connection_segment(painter, color, points[i], points[i + 1]); - } - self.draw_connection_segment(painter, color, *points.last().unwrap(), end); - } - - // Draw handles (dragging boxes) - if draw_handles { - for point in points { - painter.rect( - egui::Rect::from_center_size( - point, - Vec2::new(self.grid.get_size(), self.grid.get_size()), - ), - Rounding::ZERO, - Color32::DARK_GRAY, - Stroke::NONE, - ); - } - } + conn.draw(self, painter, theme); } } - fn draw_connection_segment(&self, painter: &Painter, color: Color32, a: Pos2, b: Pos2) { - painter.line_segment( - [a, b], - PathStroke::new(LINE_THICKNESS * self.grid.get_size(), color), - ); - } - fn draw_elements(&self, ui: &Ui, theme: Theme) { for element in &self.elements { - let image_rect = egui::Rect::from_center_size( - element.position.to_pos2(&self.grid), - Vec2::splat(element.size as f32 * self.grid.get_size()), - ); - - egui::Image::new(element.symbol.get_image(theme)) - .rotate(element.rotation, Vec2::new(0.5, 0.5)) - .paint_at(ui, image_rect); + element.draw(&self.grid, ui, theme); } } - fn draw_context_menu(&mut self, ui: &mut Ui, pointer_pos: &Pos2) { + fn draw_context_menu(&mut self, ui: &mut Ui, pointer_pos: Vec2) { ui.set_max_width(120.0); // To make sure we wrap long text if !self.editable { if ui.button("Enable editing").clicked() { self.editable = true; - ui.close_menu(); + self.close_context_menu(ui); } ui.checkbox(&mut self.center_content, "Center"); return; } - if self.is_hovering_element(pointer_pos) { - let hovered_element = self.find_hovered_element_idx(pointer_pos); + let elem_idx = self.hovers_element(pointer_pos); + if let Some(elem_idx) = elem_idx { if ui.button("Connect").clicked() { - if let Some(idx) = hovered_element { - println!("Connect action started"); - self.action = Some(Action::Connect(idx)); - } else { - panic!("No element found where the \"Connect\" action was issued"); - } - ui.close_menu(); + self.action = Some(Action::Connect(elem_idx)); + self.close_context_menu(ui); + println!("Connect action started on {}", elem_idx); } if ui.button("Rotate 90° ⟲").clicked() { - if let Some(elem) = self.find_hovered_element_mut(pointer_pos) { - elem.rotation += PI / 2.0; - } else { - panic!("No element found where the \"Rotate 90° ⟲\" action was issued"); - } - ui.close_menu(); + self.elements[elem_idx].rotate(-PI / 2.0); + self.close_context_menu(ui); } if ui.button("Rotate 90° ⟳").clicked() { - if let Some(elem) = self.find_hovered_element_mut(pointer_pos) { - elem.rotation -= PI / 2.0; - } else { - panic!("No element found where the \"Rotate 90° ⟳\" action was issued"); - } - ui.close_menu(); + self.elements[elem_idx].rotate(PI / 2.0); + self.close_context_menu(ui); } if ui.button("Delete").clicked() { - if let Some(idx) = self.find_hovered_element_idx(pointer_pos) { - self.delete_element(idx); - } else { - panic!("No element found where the \"Delete\" action was issued"); - } - ui.close_menu(); + self.delete_element(elem_idx); + self.close_context_menu(ui); } - } else if let Some((conn_idx, segm_idx)) = self.find_hovered_connection_idx(pointer_pos) { + } else if let Some((conn_idx, segm_idx)) = self.hovers_connection(pointer_pos) { if ui.button("Split").clicked() { - println!("Splitting connection line"); - self.connections[conn_idx].split(segm_idx, Pos::from_pos2(&self.grid, pointer_pos)); - ui.close_menu(); + self.connections[conn_idx].split(segm_idx, self.grid.screen_to_grid(pointer_pos)); + self.close_context_menu(ui); } 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].symbol.get_anchor_points().len(); - ui.close_menu(); + conn.start_anchor = + (conn.start_anchor + 1) % self.elements[conn.start].anchor_points_len(); + self.close_context_menu(ui); } 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].symbol.get_anchor_points().len(); - ui.close_menu(); + conn.end_anchor = + (conn.end_anchor + 1) % self.elements[conn.end].anchor_points_len(); + self.close_context_menu(ui); } } else { ui.menu_button("Symbols", |ui| { for symbol in Symbol::iter() { if ui.button(symbol.to_string()).clicked() { - self.elements.push(Element::new( - Pos::from_pos2(&self.grid, pointer_pos), - symbol, - )); - ui.close_menu(); + self.elements + .push(Element::new(self.grid.screen_to_grid(pointer_pos), symbol)); + self.close_context_menu(ui); } } }); @@ -409,41 +250,60 @@ impl PidPane { } } - fn delete_element(&mut self, idx: usize) { + fn close_context_menu(&mut self, ui: &mut Ui) { + ui.close_menu(); + self.action.take(); + } + + /// 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.start != idx && elem.end != idx); + self.connections.retain(|elem| !elem.connected(elem_idx)); // Then the element - self.elements.remove(idx); + self.elements.remove(elem_idx); } fn center(&mut self, ui: &Ui) { - let ui_center = ui.max_rect().center(); + let ui_center = egui_to_glam(ui.max_rect().center().to_vec2()); - let points: Vec<Pos> = self + // Chain elements positions and connection mid points + let points: Vec<Vec2> = self .elements .iter() - .map(|e| e.position.clone()) - .chain( - self.connections - .iter() - .flat_map(|conn| conn.middle_points.clone()), - ) + .map(|e| e.position()) + .chain(self.connections.iter().flat_map(|conn| conn.points())) .collect(); - let min_x = points.iter().map(|p| p.x).min().unwrap(); - let max_x = points.iter().map(|p| p.x).max().unwrap(); - let min_y = points.iter().map(|p| p.y).min().unwrap(); - let max_y = points.iter().map(|p| p.y).max().unwrap(); + let min_x = points + .iter() + .map(|p| p.x) + .min_by(|a, b| a.total_cmp(b)) + .unwrap(); + let min_y = points + .iter() + .map(|p| p.y) + .min_by(|a, b| a.total_cmp(b)) + .unwrap(); + let min = Vec2::new(min_x, min_y); - let pid_center = - Pos::new((max_x + min_x) / 2, (max_y + min_y) / 2).to_relative_pos2(&self.grid); + let max_x = points + .iter() + .map(|p| p.x) + .max_by(|a, b| a.total_cmp(b)) + .unwrap(); + let max_y = points + .iter() + .map(|p| p.y) + .max_by(|a, b| a.total_cmp(b)) + .unwrap(); + let max = Vec2::new(max_x, max_y); - self.grid.zero_pos = ui_center - pid_center.to_vec2(); + let center_g = (min + max) / 2.0; + self.grid.zero_pos = ui_center - center_g * self.grid.size(); } - fn handle_zoom(&mut self, ui: &Ui, theme: Theme, pointer_pos: &Pos2) { + 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); @@ -455,4 +315,63 @@ impl PidPane { } } } + + 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) { + println!("Handling actions"); + match self.action { + Some(Action::Connect(start)) => { + println!("Handling connect action"); + if response.clicked() { + println!("A click occurred"); + if let Some(end) = self.hovers_element(pointer_pos) { + println!("The pointer was hovering the element {}", end); + if start != end { + self.connections.push(Connection::new(start, 0, end, 0)); + println!("Connect action ended on {}", end); + } else { + println!("Connect action onded on the same element") + } + 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 => {} + } + } } diff --git a/src/ui/panes/pid_drawing_tool/connections.rs b/src/ui/panes/pid_drawing_tool/connections.rs index 48ea80775472b991a0674493d16f300f3e98b59a..f64ec3ea5a3e00bfda8f7ebf38d33cdd04de1ae2 100644 --- a/src/ui/panes/pid_drawing_tool/connections.rs +++ b/src/ui/panes/pid_drawing_tool/connections.rs @@ -1,7 +1,13 @@ -use egui::Pos2; +use egui::{epaint::PathStroke, Color32, Painter, Rect, Rounding, Stroke, Theme}; +use glam::Vec2; use serde::{Deserialize, Serialize}; -use super::{grid::LINE_DISTANCE_THRESHOLD, pos::Pos, PidPane}; +use crate::ui::utils::glam_to_egui; + +use super::{ + grid::{GridInfo, CONNECTION_LINE_THICKNESS, CONNECTION_LINE_THRESHOLD, CONNECTION_POINT_SIZE}, + PidPane, +}; #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct Connection { @@ -13,8 +19,8 @@ pub struct Connection { pub end: usize, pub end_anchor: usize, - /// Coordinates of middle points - pub middle_points: Vec<Pos>, + /// Mid points in grid coordinates + points_g: Vec<Vec2>, } impl Connection { @@ -24,31 +30,37 @@ impl Connection { start_anchor, end, end_anchor, - middle_points: Vec::new(), + points_g: Vec::new(), } } - /// Return the index of the segment the position is on, if any - pub fn contains(&self, pid: &PidPane, pos: &Pos2) -> Option<usize> { + /// 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].get_anchor(&pid.grid, self.start_anchor)); + points.push(pid.elements[self.start].anchor_point(self.start_anchor)); // Append all midpoints - self.middle_points + self.points_g .iter() - .map(|p| p.to_pos2(&pid.grid)) + .map(|p| pid.grid.grid_to_screen(*p)) .for_each(|p| points.push(p)); // Append end point - points.push(pid.elements[self.end].get_anchor(&pid.grid, self.end_anchor)); + 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 is_hovering_segment(pos, &a, &b) { + if is_hovering_segment(p_g, a, b) { return Some(i); } } @@ -56,27 +68,95 @@ impl Connection { None } - pub fn split(&mut self, idx: usize, pos: Pos) { - self.middle_points.insert(idx, pos.clone()); + /// 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().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().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()), + ), + Rounding::ZERO, + Color32::DARK_GRAY, + Stroke::NONE, + ); + } + } + } + } + + 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()], + PathStroke::new(CONNECTION_LINE_THICKNESS * grid.size(), color), + ); } } -fn distance(a: &Pos2, b: &Pos2) -> f32 { +fn distance(a: Vec2, b: Vec2) -> f32 { ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt() } /// Distance of a from the line defined by b and c -fn distance_from_line(p: &Pos2, m: f32, q: f32) -> f32 { +fn distance_from_line(p: Vec2, m: f32, q: f32) -> f32 { (p.y - m * p.x - q).abs() / (1.0 + m * m).sqrt() } /// True if p hovers the segment defined by a and b -fn is_hovering_segment(p: &Pos2, a: &Pos2, b: &Pos2) -> bool { +fn is_hovering_segment(p: Vec2, a: Vec2, b: Vec2) -> bool { if a != b { - let midpoint = Pos2 { - x: (a.x + b.x) / 2.0, - y: (a.y + b.y) / 2.0, - }; + let midpoint = (a + b) / 2.0; let m = (a.y - b.y) / (a.x - b.x); let (d1, d2) = if m == 0.0 { @@ -97,7 +177,7 @@ fn is_hovering_segment(p: &Pos2, a: &Pos2, b: &Pos2) -> bool { let length = distance(a, b); - d1 <= LINE_DISTANCE_THRESHOLD && d2 <= length + d1 <= CONNECTION_LINE_THRESHOLD && d2 <= length } else { false } diff --git a/src/ui/panes/pid_drawing_tool/elements.rs b/src/ui/panes/pid_drawing_tool/elements.rs index 446c444e03cda8d3ab653a1f2b66552912108f5a..49a6f6f76f5acc6a10ae8259e6c19e26cff9c490 100644 --- a/src/ui/panes/pid_drawing_tool/elements.rs +++ b/src/ui/panes/pid_drawing_tool/elements.rs @@ -1,44 +1,124 @@ +use crate::ui::utils::glam_to_egui; + +use super::grid::GridInfo; use super::symbols::Symbol; -use super::{grid::GridInfo, pos::Pos}; -use egui::{Pos2, Vec2}; +use egui::{Rect, Theme, Ui}; +use glam::{Mat2, Vec2}; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct Element { - /// Anchor postion in the grid, symbol center - pub position: Pos, + /// Anchor postion in grid coordinates, top-left corner + position: glam::Vec2, - /// Size in grid units - pub size: i32, + /// Symbol to be displayed + symbol: Symbol, /// Rotation in radiants - pub rotation: f32, + rotation: f32, - /// Symbol to be displayed - pub symbol: Symbol, + /// 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. + anchor_points: Vec<Vec2>, } impl Element { - pub fn new(pos: Pos, symbol: Symbol) -> Self { + pub fn new(position: Vec2, symbol: Symbol) -> Self { Self { - position: pos, - size: 10, + position, rotation: 0.0, + anchor_points: symbol.anchor_points(), symbol, } } - pub fn contains(&self, grid: &GridInfo, pos: &Pos2) -> bool { - let start = self.position.add_size(-self.size / 2).to_pos2(grid); - let end = self.position.add_size(self.size / 2).to_pos2(grid); + /// 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.symbol.size() / 2.0; + + self.position = p_g - center_g; + } + + pub fn change_symbol(&mut self, symbol: Symbol) { + self.symbol = symbol; + + // Anchor points can be different between symbols, realod the cache + self.reload_anchor_points(); + } + + /// 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; + + // Recompute anchor points cache + self.reload_anchor_points(); + } + + fn reload_anchor_points(&mut self) { + // Rotation matrix from element's frame to grid's frame + let rotm_e_to_g = Mat2::from_angle(self.rotation); + + // Then rotate the anchor points + self.anchor_points = self + .symbol + .anchor_points() + .iter() + .map(|&p| rotm_e_to_g * p) + .collect(); + } + + /// Returns the position of one anchor point in grid coordinates + pub fn anchor_point(&self, idx: usize) -> Vec2 { + self.anchor_points[idx] + self.position + } + + pub fn anchor_points_len(&self) -> usize { + self.anchor_points.len() + } + + /// Size in grid units + pub fn size(&self) -> Vec2 { + self.symbol.size() + } - (start.x <= pos.x && pos.x < end.x) && (start.y <= pos.y && pos.y < end.y) + /// Position of the element's top-left corner + pub fn position(&self) -> Vec2 { + self.position } - pub fn get_anchor(&self, grid: &GridInfo, idx: usize) -> Pos2 { - let anchor = self.symbol.get_anchor_points()[idx]; - let anchor = Vec2::from(anchor) * self.size as f32 * grid.get_size(); + pub fn draw(&self, grid: &GridInfo, ui: &Ui, theme: Theme) { + let center = glam_to_egui(grid.grid_to_screen(self.position)).to_pos2(); + let image_rect = Rect::from_min_size(center, glam_to_egui(self.size() * grid.size())); - self.position.to_pos2(grid) + anchor + egui::Image::new(self.symbol.get_image(theme)) + .rotate(self.rotation, egui::Vec2::splat(0.0)) + .paint_at(ui, image_rect); } } diff --git a/src/ui/panes/pid_drawing_tool/grid.rs b/src/ui/panes/pid_drawing_tool/grid.rs index 9977c69ca1aea025d095866d51e335208c70c371..735218a87e9408ccba0df9895addd7714a785e21 100644 --- a/src/ui/panes/pid_drawing_tool/grid.rs +++ b/src/ui/panes/pid_drawing_tool/grid.rs @@ -1,4 +1,6 @@ -use egui::Pos2; +use core::f32; + +use glam::Vec2; use serde::{Deserialize, Serialize}; const DEFAULT_SIZE: f32 = 10.0; @@ -6,47 +8,54 @@ const MIN_SIZE: f32 = 5.0; const MAX_SIZE: f32 = 50.0; const SCROLL_DELTA: f32 = 1.0; -pub const LINE_DISTANCE_THRESHOLD: f32 = 5.0; -pub const LINE_THICKNESS: f32 = 0.2; +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)] pub struct GridInfo { - pub zero_pos: Pos2, + /// Grid's zero position on screen + pub zero_pos: Vec2, size: f32, } impl Default for GridInfo { fn default() -> Self { Self { - zero_pos: Pos2::ZERO, + zero_pos: Vec2::ZERO, size: DEFAULT_SIZE, } } } impl GridInfo { - pub fn get_size(&self) -> f32 { + /// Returns the grid size + pub fn size(&self) -> f32 { self.size } - pub fn apply_scroll_delta(&mut self, delta: f32, pos: &Pos2) { - if delta == 0.0 { + /// 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; - if delta > 0.0 { - self.size += SCROLL_DELTA; - } else { - self.size -= SCROLL_DELTA; - }; - - self.size = self.size.clamp(MIN_SIZE, MAX_SIZE); + let delta = delta.signum() * SCROLL_DELTA; + self.size = (self.size + delta).clamp(MIN_SIZE, MAX_SIZE); if self.size != old_size { - let delta_prop = self.size / old_size - 1.0; - let pos_delta = delta_prop * (self.zero_pos - pos.to_vec2()); - self.zero_pos += pos_delta.to_vec2(); + 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/pos.rs b/src/ui/panes/pid_drawing_tool/pos.rs deleted file mode 100644 index bb91852492a61f3f3885b98cb2b846cfae0f641f..0000000000000000000000000000000000000000 --- a/src/ui/panes/pid_drawing_tool/pos.rs +++ /dev/null @@ -1,50 +0,0 @@ -use egui::Pos2; -use serde::{Deserialize, Serialize}; - -use super::grid::GridInfo; - -#[derive(Clone, Serialize, Deserialize, PartialEq, Default)] -pub struct Pos { - pub x: i32, - pub y: i32, -} - -impl Pos { - pub fn new(x: i32, y: i32) -> Self { - Self { x, y } - } - - pub fn add_size(&self, size: i32) -> Self { - Self { - x: self.x + size, - y: self.y + size, - } - } - - pub fn to_pos2(&self, grid: &GridInfo) -> Pos2 { - Pos2 { - x: self.x as f32 * grid.get_size() + grid.zero_pos.x, - y: self.y as f32 * grid.get_size() + grid.zero_pos.y, - } - } - - pub fn to_relative_pos2(&self, grid: &GridInfo) -> Pos2 { - Pos2 { - x: self.x as f32 * grid.get_size(), - y: self.y as f32 * grid.get_size(), - } - } - - pub fn from_pos2(grid: &GridInfo, pos: &Pos2) -> Self { - Self { - x: ((pos.x - grid.zero_pos.x) / grid.get_size()) as i32, - y: ((pos.y - grid.zero_pos.y) / grid.get_size()) as i32, - } - } - - pub fn distance(&self, grid: &GridInfo, pos: &Pos2) -> f32 { - let me = self.to_pos2(grid); - - ((me.x - pos.x).powi(2) + (me.y - pos.y).powi(2)).sqrt() - } -} diff --git a/src/ui/panes/pid_drawing_tool/symbols.rs b/src/ui/panes/pid_drawing_tool/symbols.rs index c0bd2123d2af4bd827623e2c5f53421f90e040ba..7f092870e57a333379ad862b838eddae5cd944b7 100644 --- a/src/ui/panes/pid_drawing_tool/symbols.rs +++ b/src/ui/panes/pid_drawing_tool/symbols.rs @@ -1,4 +1,5 @@ use egui::{ImageSource, Theme}; +use glam::Vec2; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumIter}; @@ -7,46 +8,56 @@ pub enum Symbol { ManualValve, CheckValve, // ReliefValve, - // ControlValve, + MotorValve, + // ThreeWayValve, // PressureRegulator, // BurstDisk, // QuickConnector, // PressureTransducer, // PressureGauge, // FlexibleConnection, - // ThreeWayValve, - PressurizedVessel, + // PressurizedVessel, } impl Symbol { pub fn get_image(&self, theme: Theme) -> ImageSource { match (&self, theme) { (Symbol::ManualValve, Theme::Light) => { - egui::include_image!("../../../../icons/ball_valve_light.svg") + egui::include_image!("../../../../icons/pid_symbols/light/manual_valve.svg") } (Symbol::ManualValve, Theme::Dark) => { - egui::include_image!("../../../../icons/ball_valve_dark.svg") + egui::include_image!("../../../../icons/pid_symbols/light/manual_valve.svg") } (Symbol::CheckValve, Theme::Light) => { - egui::include_image!("../../../../icons/check_valve_light.svg") + egui::include_image!("../../../../icons/pid_symbols/light/check_valve.svg") } (Symbol::CheckValve, Theme::Dark) => { - egui::include_image!("../../../../icons/check_valve_dark.svg") + egui::include_image!("../../../../icons/pid_symbols/light/check_valve.svg") } - (Symbol::PressurizedVessel, Theme::Light) => { - egui::include_image!("../../../../icons/pressurized_vessel_light.svg") + (Symbol::MotorValve, Theme::Light) => { + egui::include_image!("../../../../icons/pid_symbols/light/motor_valve.svg") } - (Symbol::PressurizedVessel, Theme::Dark) => { - egui::include_image!("../../../../icons/pressurized_vessel_dark.svg") + (Symbol::MotorValve, Theme::Dark) => { + egui::include_image!("../../../../icons/pid_symbols/light/motor_valve.svg") } } } - pub fn get_anchor_points(&self) -> Vec<(f32, f32)> { + /// Symbol size in grid coordinates + pub fn size(&self) -> Vec2 { + match self { + Symbol::ManualValve => Vec2::new(10.0, 5.0), + Symbol::CheckValve => Vec2::new(10.0, 5.0), + Symbol::MotorValve => Vec2::new(10.0, 7.5), + } + } + + /// Anchor point position relative to top right corner in grid units + pub fn anchor_points(&self) -> Vec<Vec2> { match self { - Symbol::ManualValve => [(-0.5, 0.0), (0.5, 0.0)].into(), - Symbol::CheckValve => [(-0.5, 0.0), (0.5, 0.0)].into(), - Symbol::PressurizedVessel => [(0.0, -0.5), (0.0, 0.5)].into(), + Symbol::ManualValve => [Vec2::new(0.0, 2.5), Vec2::new(10.0, 2.5)].into(), + Symbol::CheckValve => [Vec2::new(0.0, 2.5), Vec2::new(10.0, 2.5)].into(), + Symbol::MotorValve => [Vec2::new(0.0, 5.0), Vec2::new(10.0, 5.0)].into(), } } } 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) +}