diff --git a/Cargo.lock b/Cargo.lock index f5eb784dce45f0dcde835c39150678590708649d..3013fbae22c3822c4e080096db6c7633499c5b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,6 +461,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.6.0" @@ -661,6 +667,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.6.0" @@ -761,6 +773,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.2.16" @@ -1013,7 +1034,7 @@ dependencies = [ "enum-map", "log", "mime_guess2", - "resvg", + "resvg 0.37.0", ] [[package]] @@ -1258,6 +1279,29 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.24.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1427,6 +1471,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1804,12 +1858,28 @@ dependencies = [ "png", ] +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "immutable-chunkmap" version = "2.0.6" @@ -1920,6 +1990,16 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "kurbo" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1942,6 +2022,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -2088,6 +2174,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -2194,6 +2286,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2503,7 +2605,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser", + "ttf-parser 0.25.1", ] [[package]] @@ -2669,6 +2771,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.26.0" @@ -2697,6 +2805,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.38" @@ -2825,9 +2943,26 @@ dependencies = [ "log", "pico-args", "rgb", - "svgtypes", + "svgtypes 0.13.0", + "tiny-skia", + "usvg 0.37.0", +] + +[[package]] +name = "resvg" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", "tiny-skia", - "usvg", + "usvg 0.44.0", + "zune-jpeg", ] [[package]] @@ -2859,7 +2994,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.7.0", "serde", "serde_derive", @@ -2871,6 +3006,12 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2902,6 +3043,24 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "rustybuzz" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" +dependencies = [ + "bitflags 2.7.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.24.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.18" @@ -2958,7 +3117,10 @@ dependencies = [ "glam", "mavlink-bindgen", "mint", + "nom", "parking_lot", + "quick-xml 0.37.2", + "resvg 0.44.0", "ring-channel", "serde", "serde_json", @@ -3122,6 +3284,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skyward_mavlink" version = "0.1.0" @@ -3278,8 +3446,18 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" dependencies = [ - "kurbo", - "siphasher", + "kurbo 0.9.5", + "siphasher 0.3.11", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.1", + "siphasher 1.0.1", ] [[package]] @@ -3433,6 +3611,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.43.0" @@ -3526,6 +3719,15 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "ttf-parser" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" +dependencies = [ + "core_maths", +] + [[package]] name = "ttf-parser" version = "0.25.1" @@ -3564,18 +3766,54 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" + +[[package]] +name = "unicode-ccc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" + [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -3605,7 +3843,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" dependencies = [ - "base64", + "base64 0.21.7", "log", "pico-args", "usvg-parser", @@ -3613,6 +3851,33 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "usvg" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.1", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.1", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "usvg-parser" version = "0.37.0" @@ -3621,13 +3886,13 @@ checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" dependencies = [ "data-url", "flate2", - "imagesize", - "kurbo", + "imagesize 0.12.0", + "kurbo 0.9.5", "log", - "roxmltree", + "roxmltree 0.19.0", "simplecss", - "siphasher", - "svgtypes", + "siphasher 0.3.11", + "svgtypes 0.13.0", "usvg-tree", ] @@ -3639,7 +3904,7 @@ checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" dependencies = [ "rctree", "strict-num", - "svgtypes", + "svgtypes 0.13.0", "tiny-skia-path", ] @@ -3907,6 +4172,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" version = "22.1.0" @@ -4672,6 +4943,21 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.2.0" diff --git a/Cargo.toml b/Cargo.toml index e5d25ebd71f6c20bf32f94c14eaf85396cc8f923..2e068e386b9cd705edb66602ed6b1a0c489045a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,6 @@ thiserror = "2.0.7" uuid = { version = "1.12.1", features = ["serde", "v7"] } glam = { version = "0.29", features = ["serde", "mint"] } mint = "0.5.9" +quick-xml = { version = "0.37", features = ["serialize"] } +nom = "7.1" +resvg = "0.44" diff --git a/src/ui/panes.rs b/src/ui/panes.rs index d8e52d1e66f1ae3c67700db06202731c1d0e4b06..1cf0a7f077957d11160fda6005a16c1083df2670 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,7 +1,8 @@ mod default; mod messages_viewer; -mod pid; +pub mod pid; mod pid_drawing_tool; +pub mod pid_new; pub mod plot; use egui_tiles::TileId; @@ -50,11 +51,14 @@ pub enum PaneKind { #[strum(message = "Plot 2D")] Plot2D(plot::Plot2DPane), - #[strum(message = "PID Old")] - PidOld(pid_drawing_tool::PidPane), + #[strum(message = "PID 1")] + PidOld(pid_drawing_tool::Pid1), - #[strum(message = "PID New")] - Pid(pid::Pid), + #[strum(message = "PID 2")] + Pid(pid::Pid2), + + #[strum(message = "PID 3")] + PidNew(pid_new::Pid3), } impl Default for PaneKind { diff --git a/src/ui/panes/pid.rs b/src/ui/panes/pid.rs new file mode 100644 index 0000000000000000000000000000000000000000..12753b66bd267e3424f4d2b3efabc639a4a288e3 --- /dev/null +++ b/src/ui/panes/pid.rs @@ -0,0 +1,208 @@ +mod grid; +mod svg; + +use super::PaneBehavior; +use crate::ui::composable_view::PaneResponse; +use egui::{ + epaint::ImageDelta, Color32, Context, CursorIcon, PointerButton, Pos2, Rect, Sense, + TextureOptions, Theme, Ui, +}; +use egui_tiles::TileId; +use grid::Grid; +use resvg::tiny_skia::IntSize; +use serde::{Deserialize, Serialize}; +use svg::Svg; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] +pub struct Pid2 { + svg: Svg, + grid: Grid, + + #[serde(skip)] + editable: bool, + + center_content: bool, + + cache_valid: bool, + texture_id: Option<egui::epaint::TextureId>, + + selected_element: Option<usize>, +} + +impl PaneBehavior for Pid2 { + fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { + let theme = Self::find_theme(ui.ctx()); + + self.grid.draw(ui, theme); + self.draw_svg(ui); + + let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); + if let Some(pos) = response.hover_pos().map(|p| self.grid.screen_to_grid(p)) { + if let Some((idx, elem)) = self + .svg + .iter_mut_elements() + .enumerate() + .find(|(_, e)| e.hovered(pos)) + { + // Handle hovering + if elem.draggable() { + ui.ctx().output_mut(|o| o.cursor_icon = CursorIcon::Grab); + } + + // Handle drag + if response.drag_started_by(PointerButton::Primary) { + println!("Drag started on {}", elem.who_am_i()); + } else if response.drag_stopped_by(PointerButton::Primary) { + println!("Drag stopped on {}", elem.who_am_i()); + } + + // Handle clicks + if response.clicked_by(PointerButton::Secondary) { + self.selected_element = Some(idx); + } + } + } + + // Handle context menu + if let Some(idx) = self.selected_element { + response.context_menu(|ui| { + if ui.button("Delete").clicked() { + println!("We need to delete {idx}"); + } + }); + } + + PaneResponse::default() + } + + fn contains_pointer(&self) -> bool { + false + } +} + +impl Pid2 { + /// 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, + }, + }) + } + + pub fn from_file() -> Self { + let test = String::from_utf8(std::fs::read("test_assets/simple_pid.svg").unwrap()).unwrap(); + let mut des = quick_xml::de::Deserializer::from_str(&test); + Self { + svg: Svg::deserialize(&mut des).unwrap(), + grid: Grid::from_size(50.0), + editable: false, + center_content: false, + cache_valid: false, + texture_id: None, + selected_element: None, + } + } + + fn draw_svg(&mut self, ui: &mut Ui) { + let texture_id = match self.texture_id { + Some(texture_id) => { + if !self.cache_valid { + let image = self.rasterize_svg().unwrap(); + ui.ctx().tex_manager().write().set( + texture_id, + ImageDelta::full(image, TextureOptions::default()), + ); + self.cache_valid = true; + } + texture_id + } + None => { + let image = self.rasterize_svg().unwrap(); + let texture_id = ui.ctx().tex_manager().write().alloc( + "pid".to_string(), + image.into(), + TextureOptions::default(), + ); + println!( + "Texture meta: {:?}", + ui.ctx().tex_manager().read().meta(texture_id) + ); + self.texture_id = Some(texture_id); + self.cache_valid = true; + texture_id + } + }; + // egui::Image::from_texture(( + // texture_id, + // egui::Vec2::new( + // self.svg.width * self.grid.size(), + // self.svg.height * self.grid.size(), + // ), + // )) + // .paint_at( + // ui, + // Rect::from_min_size( + // Pos2::new(0.0, 0.0), + // egui::Vec2::new( + // self.svg.width * self.grid.size(), + // self.svg.height * self.grid.size(), + // ), + // ), + // ); + + let painter = ui.painter(); + let rect = Rect::from_min_size( + Pos2::new(0.0, 0.0), + egui::Vec2::new( + self.svg.width * self.grid.size(), + self.svg.height * self.grid.size(), + ), + ); + painter.image( + texture_id, + rect, + Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), + Color32::WHITE, + ); + } + + fn rasterize_svg(&self) -> Result<egui::ColorImage, String> { + let mut serialized = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut serialized, Some("svg")).unwrap(); + self.svg.serialize(ser).unwrap(); + let svg_bytes: &[u8] = serialized.as_bytes(); + + use resvg::tiny_skia::{Pixmap, Transform}; + use resvg::usvg::{Options, Tree}; + + let mut opt = Options::default(); + opt.fontdb_mut().load_system_fonts(); + let rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?; + let size = rtree.size().to_int_size(); + println!("Original svg size: {size:?}"); + + let transform = Transform::from_scale(self.grid.size(), self.grid.size()); + let size = IntSize::from_wh( + (transform.sx * size.width() as f32).ceil() as u32, + (transform.sy * size.height() as f32).ceil() as u32, + ) + .ok_or_else(|| format!("Failed to compute SVG size"))?; + println!("Scaled svg size: {size:?}"); + let mut pixmap = Pixmap::new(size.width(), size.height()) + .ok_or_else(|| format!("Failed to create SVG Pixmap of size {size:?}"))?; + resvg::render(&rtree, transform, &mut pixmap.as_mut()); + let image = egui::ColorImage::from_rgba_unmultiplied( + [size.width() as _, size.height() as _], + pixmap.data(), + ); + println!("Rasterized image: {image:?}"); + Ok(image) + } +} diff --git a/src/ui/panes/pid/grid.rs b/src/ui/panes/pid/grid.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa6ffe28c003b042cdb00d14d719ed29060cdfb3 --- /dev/null +++ b/src/ui/panes/pid/grid.rs @@ -0,0 +1,98 @@ +use core::f32; + +use egui::{Color32, Pos2, Theme, Ui, 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 Grid { + pub zero_pos: Vec2, + size: f32, +} + +impl Default for Grid { + fn default() -> Self { + Self { + zero_pos: Vec2::ZERO, + size: DEFAULT_SIZE, + } + } +} + +impl Grid { + pub fn from_size(size: f32) -> Self { + Self { + zero_pos: Vec2::ZERO, + size, + } + } + + /// 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: Pos2) -> Pos2 { + p_g * self.size + self.zero_pos + } + + /// Screen to grid coordinates transformation + pub fn screen_to_grid(&self, p_s: Pos2) -> Pos2 { + (p_s - self.zero_pos) / self.size + } + + fn dots_color(theme: Theme) -> Color32 { + match theme { + Theme::Dark => Color32::DARK_GRAY, + Theme::Light => Color32::BLACK, + } + } + + pub fn draw(&self, ui: &Ui, theme: Theme) { + let painter = ui.painter(); + let window_rect = ui.max_rect(); + let dot_color = Self::dots_color(theme); + + let offset_x = (self.zero_pos.x % self.size()) as i32; + let offset_y = (self.zero_pos.y % self.size()) as i32; + + let start_x = (window_rect.min.x / self.size()) as i32 * self.size() as i32 + offset_x; + let end_x = (window_rect.max.x / self.size() + 2.0) as i32 * self.size() as i32 + offset_x; + let start_y = (window_rect.min.y / self.size()) as i32 * self.size() as i32 + offset_y; + let end_y = (window_rect.max.y / self.size() + 2.0) as i32 * self.size() as i32 + offset_y; + + for x in (start_x..end_x).step_by(self.size() as usize) { + for y in (start_y..end_y).step_by(self.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), + ); + painter.rect_filled(rect, 0.0, dot_color); + } + } + } +} diff --git a/src/ui/panes/pid/svg.rs b/src/ui/panes/pid/svg.rs new file mode 100644 index 0000000000000000000000000000000000000000..91ab21fa82d3df2e3b9c1befa1fad81edacfd056 --- /dev/null +++ b/src/ui/panes/pid/svg.rs @@ -0,0 +1,226 @@ +pub mod attributes; +pub mod elements; +pub mod utils; + +use std::slice::{Iter, IterMut}; + +use elements::{defs::Defs, text::Text, use_node::Use}; +use serde::{Deserialize, Serialize}; +use utils::{is_default, is_zero}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Svg { + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@version")] + pub version: f32, // 1.1 + + #[serde(rename = "@xmlns")] + pub xmlns: String, // http://www.w3.org/2000/svg + + #[serde(rename = "defs")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub defs: Defs, + + #[serde(rename = "use")] + #[serde(default)] + pub uses: Vec<Use>, + + #[serde(rename = "text")] + #[serde(default)] + pub texts: Vec<Text>, +} + +impl Default for Svg { + fn default() -> Self { + Self { + width: 0.0, + height: 0.0, + version: 1.1, + xmlns: "http://www.w3.org/2000/svg".to_string(), + defs: Defs::default(), + uses: Vec::new(), + texts: Vec::new(), + } + } +} + +impl Svg { + pub fn iter_elements(&self) -> Iter<Use> { + self.uses.iter() + } + + pub fn iter_mut_elements(&mut self) -> IterMut<Use> { + self.uses.iter_mut() + } + + pub fn element_at(&self, idx: usize) -> Option<&Use> { + self.uses.get(idx) + } + + pub fn remove_element_at(&mut self, idx: usize) { + self.uses.remove(idx); + } +} + +#[cfg(test)] +mod tests { + use attributes::d::{ + close_path::ClosePath, ellicptical_arc::EllipticalArc, horizonta_line_to::HorizontalLineTo, + line_to::LineTo, move_to::MoveTo, vertical_line_to::VerticalLineTo, D, + }; + use attributes::style::{LineJoin, Style}; + use attributes::transform::{Rotate, Transform, Translate}; + use egui::Color32; + use elements::path::Path; + + use super::*; + + fn test_svg() -> Svg { + Svg { + width: 14.0, + height: 17.196152, + defs: Defs { + paths: vec![ + Path { + id: "arrow".to_string(), + width: 4.0, + height: 4.0, + d: D { + segments: vec![ + MoveTo::abs(0.7, 2.0), + LineTo::rel(2.6, -1.5), + VerticalLineTo::rel(3.0), + ClosePath::token(), + MoveTo::abs(0.0, 2.0), + HorizontalLineTo::rel(4.0), + ], + }, + style: Style { + stroke: Color32::BLACK, + stroke_width: 0.2, + stroke_linejoin: LineJoin::Round, + ..Default::default() + }, + ..Default::default() + }, + Path { + id: "burst_disk".to_string(), + width: 4.0, + height: 6.0, + d: D { + segments: vec![ + MoveTo::abs(0.5, 0.0), + VerticalLineTo::abs(6.0), + MoveTo::abs(1.5, 0.0), + VerticalLineTo::rel(1.0), + EllipticalArc::rel(2.0, 2.0, 0.0, true, true, 0.0, 4.0), + VerticalLineTo::rel(1.0), + MoveTo::abs(0.0, 3.0), + HorizontalLineTo::rel(0.5), + MoveTo::abs(3.5, 3.0), + HorizontalLineTo::rel(0.5), + ], + }, + style: Style { + stroke: Color32::BLACK, + fill: Color32::TRANSPARENT, + stroke_width: 0.2, + stroke_linejoin: LineJoin::Round, + ..Default::default() + }, + ..Default::default() + }, + ], + }, + uses: vec![ + Use { + href: "arrow".to_string(), + width: 4.0, + height: 4.0, + transform: Transform { + rotate: Rotate { + angle: 180.0, + x: 7.0, + y: 8.0, + }, + translate: Translate { x: 5.0, y: 6.0 }, + }, + }, + Use { + href: "burst_disk".to_string(), + width: 4.0, + height: 6.0, + transform: Transform { + translate: Translate { x: 1.0, y: 5.0 }, + ..Default::default() + }, + }, + ], + texts: vec![Text { + font_size: 2.0, + font_family: "monospace".to_string(), + transform: Transform { + translate: Translate { x: 1.0, y: 3.0 }, + ..Default::default() + }, + format: "{:.2f}".to_string(), + text: "Hi mom!".to_string(), + }], + ..Default::default() + } + } + + #[test] + fn it_serialize() { + let test = test_svg(); + let expected = + String::from_utf8(std::fs::read("test_assets/simple_pid.svg").unwrap()).unwrap(); + + let mut serialized = String::new(); + let mut ser = quick_xml::se::Serializer::with_root(&mut serialized, Some("svg")).unwrap(); + ser.indent(' ', 4); + test.serialize(ser).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn it_serialize_default() { + let test = Svg::default(); + let expected = "<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"/>"; + + let mut serialized = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut serialized, Some("svg")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn it_deserializes() { + let test = String::from_utf8(std::fs::read("test_assets/simple_pid.svg").unwrap()).unwrap(); + let expected = test_svg(); + + let mut des = quick_xml::de::Deserializer::from_str(&test); + let deserialized = Svg::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let svg = "<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"/>"; + let expected = Svg::default(); + + let mut des = quick_xml::de::Deserializer::from_str(svg); + let deserialized = Svg::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid/svg/attributes.rs b/src/ui/panes/pid/svg/attributes.rs new file mode 100644 index 0000000000000000000000000000000000000000..d8c84e457e8353871d3be571e8df6756c36abac2 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes.rs @@ -0,0 +1,3 @@ +pub mod d; +pub mod style; +pub mod transform; diff --git a/src/ui/panes/pid/svg/attributes/d.rs b/src/ui/panes/pid/svg/attributes/d.rs new file mode 100644 index 0000000000000000000000000000000000000000..564d5d0c9aef94e0413a3ac70c3ce37f9380b028 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d.rs @@ -0,0 +1,161 @@ +pub mod close_path; +pub mod ellicptical_arc; +pub mod horizonta_line_to; +pub mod line_to; +pub mod move_to; +pub mod vertical_line_to; + +use std::fmt::Display; + +use self::{ + close_path::ClosePath, ellicptical_arc::EllipticalArc, horizonta_line_to::HorizontalLineTo, + line_to::LineTo, move_to::MoveTo, vertical_line_to::VerticalLineTo, +}; +use nom::{branch::alt, character::complete::char, multi::separated_list0, IResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct D { + pub segments: Vec<DToken>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum DToken { + MoveTo(MoveTo), + LineTo(LineTo), + HorizontalLineTo(HorizontalLineTo), + VerticalLineTo(VerticalLineTo), + EllipticalArc(EllipticalArc), + ClosePath(ClosePath), +} + +impl Display for DToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MoveTo(t) => t.fmt(f), + Self::LineTo(t) => t.fmt(f), + Self::HorizontalLineTo(t) => t.fmt(f), + Self::VerticalLineTo(t) => t.fmt(f), + Self::EllipticalArc(t) => t.fmt(f), + Self::ClosePath(t) => t.fmt(f), + } + } +} + +impl Serialize for D { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.segments + .iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for D { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + let d = separated_list0(char(' '), DToken::parse)(input.as_str()) + .map(|(_, segments)| Self { segments }) + .unwrap_or_default(); + Ok(d) + } +} + +impl DToken { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + MoveTo::parse, + LineTo::parse, + HorizontalLineTo::parse, + VerticalLineTo::parse, + ClosePath::parse, + EllipticalArc::parse, + ))(input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@d")] + d: D, + } + + #[test] + fn it_serialize() { + let test = Test { + d: D { + segments: vec![ + MoveTo::abs(-3.12, 1.0), + LineTo::rel(4.0, -4.0), + MoveTo::rel(-1.0, 1.0), + LineTo::abs(5.0, 0.0), + ], + }, + }; + let expected = "<test d=\"M -3.12 1 l 4 -4 m -1 1 L 5 0\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { d: D::default() }; + let expected = "<test d=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = + "<test d=\"M 0.5 0 V 6 M 1.5 0 v 1 a 2 2 0 1 1 0 4 v 1 M 0 3 h 0.5 M 3.5 3 h 0.5\"/>"; + let expected = Test { + d: D { + segments: vec![ + MoveTo::abs(0.5, 0.0), + VerticalLineTo::abs(6.0), + MoveTo::abs(1.5, 0.0), + VerticalLineTo::rel(1.0), + EllipticalArc::rel(2.0, 2.0, 0.0, true, true, 0.0, 4.0), + VerticalLineTo::rel(1.0), + MoveTo::abs(0.0, 3.0), + HorizontalLineTo::rel(0.5), + MoveTo::abs(3.5, 3.0), + HorizontalLineTo::rel(0.5), + ], + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test d=\"\"/>"; + let expected = Test { d: D::default() }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/close_path.rs b/src/ui/panes/pid/svg/attributes/d/close_path.rs new file mode 100644 index 0000000000000000000000000000000000000000..923ee523c0c516c13dac739c44e36a6f4f4a3936 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/close_path.rs @@ -0,0 +1,22 @@ +use super::DToken; +use nom::{branch::alt, character::complete::char, combinator::map, IResult}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct ClosePath {} + +impl ClosePath { + pub fn token() -> DToken { + DToken::ClosePath(Self {}) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map(alt((char('Z'), char('z'))), |_| DToken::ClosePath(Self {}))(input) + } +} + +impl Display for ClosePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Z") + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/ellicptical_arc.rs b/src/ui/panes/pid/svg/attributes/d/ellicptical_arc.rs new file mode 100644 index 0000000000000000000000000000000000000000..3fd93c2e9f8c549c182e5c995e975a975a16c4bd --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/ellicptical_arc.rs @@ -0,0 +1,112 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::anychar, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct EllipticalArc { + abs: bool, + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, +} + +impl EllipticalArc { + pub fn abs( + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ) -> DToken { + DToken::EllipticalArc(Self { + abs: true, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + } + + pub fn rel( + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ) -> DToken { + DToken::EllipticalArc(Self { + abs: false, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('A'), |_| true), map(char('a'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + preceded(char(' '), float), + map(preceded(char(' '), anychar), |c| c == '1'), + map(preceded(char(' '), anychar), |c| c == '1'), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, rx, ry, angle, large_arc, sweep, x, y)| { + DToken::EllipticalArc(Self { + abs, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + }, + )(input) + } +} + +impl Display for EllipticalArc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = if self.abs { 'A' } else { 'a' }; + write!( + f, + "{} {} {} {} {} {} {} {}", + prefix, + self.rx, + self.ry, + self.angle, + self.large_arc as i32, + self.sweep as i32, + self.x, + self.y + ) + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/horizonta_line_to.rs b/src/ui/panes/pid/svg/attributes/d/horizonta_line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..49d9dbca3ed160bdcbf961cdf7123ac33d754048 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/horizonta_line_to.rs @@ -0,0 +1,46 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct HorizontalLineTo { + abs: bool, + x: f32, +} + +impl HorizontalLineTo { + pub fn abs(x: f32) -> DToken { + DToken::HorizontalLineTo(Self { abs: true, x }) + } + + pub fn rel(x: f32) -> DToken { + DToken::HorizontalLineTo(Self { abs: false, x }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('H'), |_| true), map(char('h'), |_| false))), + preceded(char(' '), float), + )), + |(abs, x)| DToken::HorizontalLineTo(Self { abs, x }), + )(input) + } +} + +impl Display for HorizontalLineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "H {}", self.x) + } else { + write!(f, "h {}", self.x) + } + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/line_to.rs b/src/ui/panes/pid/svg/attributes/d/line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2c348f1e97b4d772b3d2c12ee350e011efbece4 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/line_to.rs @@ -0,0 +1,48 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct LineTo { + abs: bool, + x: f32, + y: f32, +} + +impl LineTo { + pub fn abs(x: f32, y: f32) -> DToken { + DToken::LineTo(Self { abs: true, x, y }) + } + + pub fn rel(x: f32, y: f32) -> DToken { + DToken::LineTo(Self { abs: false, x, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('L'), |_| true), map(char('l'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, x, y)| DToken::LineTo(Self { abs, x, y }), + )(input) + } +} + +impl Display for LineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "L {} {}", self.x, self.y) + } else { + write!(f, "l {} {}", self.x, self.y) + } + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/move_to.rs b/src/ui/panes/pid/svg/attributes/d/move_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4d9698a0e40cce9065dc1486de20f0985a4059f --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/move_to.rs @@ -0,0 +1,48 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct MoveTo { + abs: bool, + x: f32, + y: f32, +} + +impl MoveTo { + pub fn abs(x: f32, y: f32) -> DToken { + DToken::MoveTo(Self { abs: true, x, y }) + } + + pub fn rel(x: f32, y: f32) -> DToken { + DToken::MoveTo(Self { abs: false, x, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('M'), |_| true), map(char('m'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, x, y)| DToken::MoveTo(Self { abs, x, y }), + )(input) + } +} + +impl Display for MoveTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "M {} {}", self.x, self.y) + } else { + write!(f, "m {} {}", self.x, self.y) + } + } +} diff --git a/src/ui/panes/pid/svg/attributes/d/vertical_line_to.rs b/src/ui/panes/pid/svg/attributes/d/vertical_line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..0284a2bea4104a975abb9236b2b418188ebadb4c --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/d/vertical_line_to.rs @@ -0,0 +1,46 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct VerticalLineTo { + abs: bool, + y: f32, +} + +impl VerticalLineTo { + pub fn abs(y: f32) -> DToken { + DToken::VerticalLineTo(Self { abs: true, y }) + } + + pub fn rel(y: f32) -> DToken { + DToken::VerticalLineTo(Self { abs: false, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('V'), |_| true), map(char('v'), |_| false))), + preceded(char(' '), float), + )), + |(abs, y)| DToken::VerticalLineTo(Self { abs, y }), + )(input) + } +} + +impl Display for VerticalLineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "V {}", self.y) + } else { + write!(f, "v {}", self.y) + } + } +} diff --git a/src/ui/panes/pid/svg/attributes/style.rs b/src/ui/panes/pid/svg/attributes/style.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc022b95f7392a0704b7b820c3de16e53995aede --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/style.rs @@ -0,0 +1,212 @@ +use std::fmt::Display; + +use egui::Color32; +use nom::{ + branch::alt, + bytes::complete::tag, + bytes::complete::take, + character::complete::char, + combinator::{map, opt}, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Style { + pub fill: Color32, + pub stroke: Color32, + pub stroke_opacity: f32, + pub stroke_width: f32, + pub stroke_linejoin: LineJoin, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum LineJoin { + Bevel, + Miter, + Round, +} + +impl Style { + fn parse(input: &str) -> IResult<&str, Self> { + map( + tuple(( + opt(map(preceded(tag("fill:"), take(9usize)), Color32::from_hex)), + opt(char(';')), + opt(map( + preceded(tag("stroke:"), take(9usize)), + Color32::from_hex, + )), + opt(char(';')), + opt(preceded(tag("stroke-opacity:"), float)), + opt(char(';')), + opt(preceded(tag("stroke-width:"), float)), + opt(char(';')), + opt(preceded(tag("stroke-linejoin:"), LineJoin::parse)), + )), + |(fill, _, stroke, _, stroke_opacity, _, stroke_width, _, stroke_linejoin)| { + let fill = fill.and_then(|c| c.ok()).unwrap_or(Color32::BLACK); + let stroke = stroke.and_then(|c| c.ok()).unwrap_or(Color32::TRANSPARENT); + Self { + fill, + stroke, + stroke_opacity: stroke_opacity.unwrap_or(1.0), + stroke_width: stroke_width.unwrap_or(1.0), + stroke_linejoin: stroke_linejoin.unwrap_or_default(), + } + }, + )(input) + } +} + +impl Default for Style { + fn default() -> Self { + Self { + fill: Color32::BLACK, + stroke: Color32::TRANSPARENT, + stroke_opacity: 1.0, + stroke_width: 1.0, + stroke_linejoin: LineJoin::Miter, + } + } +} + +impl Serialize for Style { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut style = Vec::new(); + + if self.fill != Color32::BLACK { + style.push(format!("fill:{}", self.fill.to_hex())); + } + if self.stroke != Color32::TRANSPARENT { + style.push(format!("stroke:{}", self.stroke.to_hex())); + } + if self.stroke_opacity != 1.0 { + style.push(format!("stroke-opacity:{}", self.stroke_opacity)); + } + if self.stroke_width != 1.0 { + style.push(format!("stroke-width:{}", self.stroke_width)); + } + if self.stroke_linejoin != LineJoin::Miter { + style.push(format!("stroke-linejoin:{}", self.stroke_linejoin)); + } + + style.join(";").serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Style { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + Ok(Style::parse(input.as_str()) + .map(|(_, style)| style) + .unwrap_or_default()) + } +} + +impl LineJoin { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + map(tag("bevel"), |_| Self::Bevel), + map(tag("miter"), |_| Self::Miter), + map(tag("round"), |_| Self::Round), + ))(input) + } +} + +impl Default for LineJoin { + fn default() -> Self { + Self::Miter + } +} + +impl Display for LineJoin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bevel => write!(f, "bevel"), + Self::Miter => write!(f, "miter"), + Self::Round => write!(f, "round"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@style")] + style: Style, + } + + #[test] + fn it_serialize() { + let test = Test { + style: Style { + fill: Color32::from_rgb(23, 45, 76), + stroke: Color32::from_rgb(123, 42, 29), + stroke_opacity: 0.5, + stroke_width: 3.21, + stroke_linejoin: LineJoin::Bevel, + }, + }; + let expected = "<test style=\"fill:#172d4cff;stroke:#7b2a1dff;stroke-opacity:0.5;stroke-width:3.21;stroke-linejoin:bevel\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { + style: Style::default(), + }; + let expected = "<test style=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test style=\"fill:#43516fff;stroke:#001831ff;stroke-opacity:0.741;stroke-width:11.5;stroke-linejoin:round\"/>"; + let expected = Test { + style: Style { + fill: Color32::from_rgb(67, 81, 111), + stroke: Color32::from_rgb(0, 24, 49), + stroke_opacity: 0.741, + stroke_width: 11.5, + stroke_linejoin: LineJoin::Round, + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test style=\"\"/>"; + let expected = Test { + style: Style::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid/svg/attributes/transform.rs b/src/ui/panes/pid/svg/attributes/transform.rs new file mode 100644 index 0000000000000000000000000000000000000000..821a42bdc80f6032a525494fc8655b9e23babac1 --- /dev/null +++ b/src/ui/panes/pid/svg/attributes/transform.rs @@ -0,0 +1,225 @@ +use egui::emath::Rot2; +use egui::{Pos2, Vec2}; +use nom::IResult; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{delimited, preceded, tuple}, +}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::string::ToString; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Transform { + pub rotate: Rotate, + pub translate: Translate, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Rotate { + pub angle: f32, + pub x: f32, + pub y: f32, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Translate { + pub x: f32, + pub y: f32, +} + +impl Transform { + pub fn to_local_frame(&self, pos: Pos2) -> Pos2 { + let pos = pos.to_vec2(); + + // Apply the rotation + let rot = Rot2::from_angle(-self.rotate.angle); + let pos = self.rotate.to_vec2() + rot * (pos - self.rotate.to_vec2()); + + // Apply the translation + let pos = pos - self.translate.to_vec2(); + + pos.to_pos2() + } +} + +impl Serialize for Transform { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut transform = String::new(); + if self.rotate.angle != 0.0 { + transform.push_str(&self.rotate.to_string()); + } + if self.translate.x != 0.0 && self.translate.y != 0.0 { + transform.push_str(&self.translate.to_string()); + } + transform.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Transform { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + + let mut transform = Transform::default(); + let input = if let Ok((input, rotate)) = Rotate::parse(&input) { + transform.rotate = rotate; + input + } else { + &input + }; + if let Ok((_, translate)) = Translate::parse(input) { + transform.translate = translate; + } + + Ok(transform) + } +} + +impl Rotate { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + map( + delimited(tag("rotate("), preceded(char(' '), float), char(')')), + |angle| Self { + angle, + x: 0.0, + y: 0.0, + }, + ), + map( + delimited( + tag("rotate("), + tuple(( + float, + preceded(char(' '), float), + preceded(char(' '), float), + )), + char(')'), + ), + |(angle, x, y)| Self { angle, x, y }, + ), + ))(input) + } + + pub fn to_vec2(&self) -> Vec2 { + Vec2::new(self.x, self.y) + } +} + +impl Display for Rotate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.x == 0.0 && self.y == 0.0 { + write!(f, "rotate({})", self.angle) + } else { + write!(f, "rotate({} {} {})", self.angle, self.x, self.y) + } + } +} + +impl Translate { + fn parse(input: &str) -> IResult<&str, Self> { + map( + delimited( + tag("translate("), + tuple((float, preceded(char(' '), float))), + char(')'), + ), + |(x, y)| Self { x, y }, + )(input) + } + + pub fn to_vec2(&self) -> Vec2 { + Vec2::new(self.x, self.y) + } +} + +impl Display for Translate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "translate({} {})", self.x, self.y) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@transform")] + transform: Transform, + } + + #[test] + fn it_serialize() { + let test = Test { + transform: Transform { + rotate: Rotate { + angle: 3.15, + x: 7.0, + y: -1.23, + }, + translate: Translate { x: 4.0, y: -1.4 }, + }, + }; + let expected = "<test transform=\"rotate(3.15 7 -1.23)translate(4 -1.4)\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { + transform: Transform::default(), + }; + let expected = "<test transform=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test transform=\"rotate(45.7 -43.21 89)translate(4 -1.4)\"/>"; + let expected = Test { + transform: Transform { + rotate: Rotate { + angle: 45.7, + x: -43.21, + y: 89.0, + }, + translate: Translate { x: 4.0, y: -1.4 }, + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test transform=\"\"/>"; + let expected = Test { + transform: Transform::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid/svg/elements.rs b/src/ui/panes/pid/svg/elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b42c4d5a06c1809a74ef7641e6fe1ac9b441d25 --- /dev/null +++ b/src/ui/panes/pid/svg/elements.rs @@ -0,0 +1,4 @@ +pub mod defs; +pub mod path; +pub mod text; +pub mod use_node; diff --git a/src/ui/panes/pid/svg/elements/defs.rs b/src/ui/panes/pid/svg/elements/defs.rs new file mode 100644 index 0000000000000000000000000000000000000000..223a3c9830bf4ea0495273baf97f62eae12bfc0c --- /dev/null +++ b/src/ui/panes/pid/svg/elements/defs.rs @@ -0,0 +1,10 @@ +use super::path::Path; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)] +pub struct Defs { + #[serde(rename = "path")] + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub paths: Vec<Path>, +} diff --git a/src/ui/panes/pid/svg/elements/path.rs b/src/ui/panes/pid/svg/elements/path.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2769f2c96d312ec5308fbe4724c31bba39d28b2 --- /dev/null +++ b/src/ui/panes/pid/svg/elements/path.rs @@ -0,0 +1,119 @@ +use crate::ui::panes::pid::svg::{ + attributes::{ + d::{DToken, D}, + style::Style, + transform::Transform, + }, + utils::{is_default, is_zero}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)] +pub struct Path { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@d")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub d: D, + + #[serde(rename = "@style")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub style: Style, + + #[serde(rename = "@transfrom")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, +} + +impl Path { + pub fn push_segment(&mut self, segment: DToken) { + self.d.segments.push(segment); + // self.update_size(); + } + + // fn update_size(&mut self) { + // let size = self + // .d + // .segments + // .iter() + // .map(|s| s.to_vec2()) + // .fold(Vec2::new(self.width, self.height), Vec2::max); + // self.width = size.x + self.style.stroke_width / 2.0; + // self.height = size.y + self.style.stroke_width / 2.0; + // } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_serialize() { + let test = Path { + id: "paolino".to_string(), + width: 23.4, + height: 5.0, + d: D::default(), + style: Style::default(), + transform: Transform::default(), + }; + let expected = "<path id=\"paolino\" width=\"23.4\" height=\"5\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("path")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Path::default(); + let expected = "<path id=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("path")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test id=\"pepperoncino\" width=\"11\" height=\"24.5\"/>"; + let expected = Path { + id: "pepperoncino".to_string(), + width: 11.0, + height: 24.5, + d: D::default(), + style: Style::default(), + transform: Transform::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Path::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<path id=\"\"/>"; + let expected = Path::default(); + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Path::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid/svg/elements/text.rs b/src/ui/panes/pid/svg/elements/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..8d547ba64d79e92e6d312d2af27a3d98ef6cd902 --- /dev/null +++ b/src/ui/panes/pid/svg/elements/text.rs @@ -0,0 +1,43 @@ +use crate::ui::panes::pid::svg::{ + attributes::transform::Transform, + utils::{is_default, is_zero}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct Text { + #[serde(rename = "@font-size")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub font_size: f32, + + #[serde(rename = "@font-family")] + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub font_family: String, + + #[serde(rename = "@transform")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, + + #[serde(rename = "@segs-format")] + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub format: String, + + #[serde(rename = "$text")] + pub text: String, +} + +impl Text { + pub fn new(text: String, size: f32) -> Self { + Self { + font_size: size, + font_family: "monospace".to_string(), + transform: Transform::default(), + format: "TODO".to_string(), + text, + } + } +} diff --git a/src/ui/panes/pid/svg/elements/use_node.rs b/src/ui/panes/pid/svg/elements/use_node.rs new file mode 100644 index 0000000000000000000000000000000000000000..ad3559076462f18b2fefb5d17b841a09660edc58 --- /dev/null +++ b/src/ui/panes/pid/svg/elements/use_node.rs @@ -0,0 +1,92 @@ +use crate::ui::panes::pid::svg::{ + attributes::transform::Transform, + utils::{is_default, is_zero}, +}; +use egui::{InputState, Pos2}; +use glam::Vec2; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct Use { + #[serde(rename = "@href", with = "href")] + pub href: String, + + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@transform")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, +} + +impl Use { + pub fn handle_click(&mut self, _pos: Pos2) {} + pub fn handle_double_click(&mut self, _pos: Pos2) {} + + /// Consumes any shortcut the element should respond to + pub fn handle_shortcuts(&mut self, _input: &mut InputState) {} + + pub fn hovered(&self, pos: Pos2) -> bool { + let pos = self.transform.to_local_frame(pos).to_vec2(); + + // The bounding box in the elemen's frame is defined by the size. But + // width and height can be negative. This allows to represent where the + // position anchor point is (for paths is the top-left, for texts + // is the bottom-left) + let size = Vec2::new(self.width, self.height); + let min = Vec2::ZERO.min(size); + let max = Vec2::ZERO.max(size); + + // Check if the point is in the bounding box + min.x <= pos.x && pos.x <= max.x && min.y <= pos.y && pos.y <= max.y + } + + /// Whether the elemen can be moved + pub fn draggable(&self) -> bool { + false + } + + /// Whether a window to edit the element's configuration is available + pub fn editable(&self) -> bool { + false + } + + pub fn who_am_i(&self) -> String { + format!( + "{} at x={} y={} width={} height={}", + self.href, + self.transform.translate.x, + self.transform.translate.y, + self.width, + self.height + ) + } +} + +mod href { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize<S>(id: &str, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + format!("#{id}").serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error> + where + D: Deserializer<'de>, + { + let mut id = String::deserialize(deserializer)?; + id.remove(0); + Ok(id) + } +} diff --git a/src/ui/panes/pid/svg/utils.rs b/src/ui/panes/pid/svg/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..16be55525989331c0a3a2c05c4ee320d927020a0 --- /dev/null +++ b/src/ui/panes/pid/svg/utils.rs @@ -0,0 +1,7 @@ +pub fn is_default<T: Default + PartialEq>(x: &T) -> bool { + *x == T::default() +} + +pub fn is_zero(x: &f32) -> bool { + *x == 0.0 +} diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 35a766e5d90c57ee8de6f4e1de77a9c1a02990ff..d9567fc3aad59ce3b9f2afd1fd64306132c1ebac 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -30,7 +30,7 @@ enum Action { /// Piping and instrumentation diagram #[derive(Clone, Serialize, Deserialize, Default, Debug)] -pub struct PidPane { +pub struct Pid1 { elements: Vec<Element>, connections: Vec<Connection>, @@ -45,7 +45,7 @@ pub struct PidPane { center_content: bool, } -impl PartialEq for PidPane { +impl PartialEq for Pid1 { fn eq(&self, other: &Self) -> bool { self.elements == other.elements && self.connections == other.connections @@ -54,9 +54,9 @@ impl PartialEq for PidPane { } } -impl PaneBehavior for PidPane { +impl PaneBehavior for Pid1 { fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { - let theme = PidPane::find_theme(ui.ctx()); + let theme = Pid1::find_theme(ui.ctx()); if self.center_content && !self.editable { self.center(ui); @@ -69,7 +69,9 @@ impl PaneBehavior for PidPane { self.draw_elements(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()); + let (rect, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); + println!("Allocated rectangle: {rect:?}"); + println!("Response: {response:?}"); 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); @@ -101,7 +103,7 @@ impl PaneBehavior for PidPane { } } -impl PidPane { +impl Pid1 { /// Returns the currently used theme fn find_theme(ctx: &Context) -> Theme { // In Egui you can either decide a theme or use the system one. @@ -155,7 +157,7 @@ impl PidPane { 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 dot_color = Pid1::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; diff --git a/src/ui/panes/pid_drawing_tool/connections.rs b/src/ui/panes/pid_drawing_tool/connections.rs index 2a5020f28c79df7b6f4dc09a67118e84fb3553e5..8e8a73324869b92854d04d6da962e5cb0c2d43f5 100644 --- a/src/ui/panes/pid_drawing_tool/connections.rs +++ b/src/ui/panes/pid_drawing_tool/connections.rs @@ -6,7 +6,7 @@ use crate::ui::utils::glam_to_egui; use super::{ grid::{GridInfo, CONNECTION_LINE_THICKNESS, CONNECTION_LINE_THRESHOLD, CONNECTION_POINT_SIZE}, - PidPane, + Pid1, }; #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] @@ -40,7 +40,7 @@ impl Connection { } /// Return the index of the segment the point is on, if any - pub fn contains(&self, pid: &PidPane, p_s: Vec2) -> Option<usize> { + pub fn contains(&self, pid: &Pid1, p_s: Vec2) -> Option<usize> { let p_g = pid.grid.screen_to_grid(p_s); let mut points = Vec::new(); @@ -94,7 +94,7 @@ impl Connection { } } - pub fn draw(&self, pid: &PidPane, painter: &Painter, theme: Theme) { + pub fn draw(&self, pid: &Pid1, painter: &Painter, theme: Theme) { let color = Connection::line_color(theme); let start = pid.elements[self.start].anchor_point(self.start_anchor); diff --git a/src/ui/panes/pid_new/icon.rs b/src/ui/panes/pid_new/icon.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/ui/panes/pid_new/mod.rs b/src/ui/panes/pid_new/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..80d4001409fa7a466f650430dbfa5bb81d309327 --- /dev/null +++ b/src/ui/panes/pid_new/mod.rs @@ -0,0 +1,119 @@ +mod icon; +mod svg; + +use super::{PaneBehavior, PaneResponse}; +use anyhow::{Context, Result}; +use egui::{Pos2, Ui}; +use egui_tiles::TileId; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::string::String; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +struct Grid { + pos: Pos2, + scale: f32, +} + +impl Default for Grid { + fn default() -> Self { + Self { + pos: Pos2::ZERO, + scale: 10.0, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +enum Element { + Icon, + Label, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +struct ElementRef { + id: String, + pos: Pos2, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct Pid3 { + /// Scale and position of where to draw the diagram on screen + grid: Grid, + + /// Elements that can be placed in the diagram + elements: HashMap<String, Element>, + + /// Instances of elements + references: Vec<ElementRef>, +} + +impl PaneBehavior for Pid3 { + fn ui(&mut self, ui: &mut Ui, _: TileId) -> PaneResponse { + self.draw_diagram(ui); + PaneResponse::default() + } + + fn contains_pointer(&self) -> bool { + false + } +} + +impl Pid3 { + fn draw_diagram(&self, ui: &mut Ui) { + let image = self.rasterize_svg().unwrap(); + let texture_id = ui.ctx().tex_manager().write().alloc( + "pid".to_string(), + image.into(), + egui::TextureOptions::default(), + ); + let rect = egui::Rect::from_min_size( + Pos2::new(0.0, 0.0), + egui::Vec2::new(10.0 * self.grid.scale, 10.0 * self.grid.scale), + ); + ui.painter().image( + texture_id, + rect, + egui::Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), + egui::Color32::WHITE, + ); + } + + fn rasterize_svg(&self) -> Result<egui::ColorImage> { + // resvg uses the library roxmltree to represent internally the xml. The + // problem is that roxmltree do not allow to build the document, only to + // parse a text/file. For this reason we have to first serialize the svg + // and then parse it to do the rasterization + + // Serialization + let svg = svg::Svg::from(self); + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("svg"))?; + svg.serialize(ser)?; + + // Parsing with usvg + let mut options = resvg::usvg::Options::default(); + options.fontdb_mut().load_system_fonts(); // TODO: Do it once + let rtree = resvg::usvg::Tree::from_str(buffer.as_str(), &options)?; + let size = rtree.size().to_int_size(); + + // Configure the scaling with the grid setting + let transform = resvg::tiny_skia::Transform::from_scale(self.grid.scale, self.grid.scale); + let size = resvg::tiny_skia::IntSize::from_wh( + (transform.sx * size.width() as f32).ceil() as u32, + (transform.sy * size.height() as f32).ceil() as u32, + ) + .context("Failed to compute SVG size")?; + + // Rasterize + let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height()) + .context("Failed to create SVG Pixmap of size {size:?}")?; + resvg::render(&rtree, transform, &mut pixmap.as_mut()); + Ok(egui::ColorImage::from_rgba_unmultiplied( + [size.width() as _, size.height() as _], + pixmap.data(), + )) + } + + fn load_default_elements(&mut self) {} +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/close_path.rs b/src/ui/panes/pid_new/svg/attributes/data/close_path.rs new file mode 100644 index 0000000000000000000000000000000000000000..923ee523c0c516c13dac739c44e36a6f4f4a3936 --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/close_path.rs @@ -0,0 +1,22 @@ +use super::DToken; +use nom::{branch::alt, character::complete::char, combinator::map, IResult}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct ClosePath {} + +impl ClosePath { + pub fn token() -> DToken { + DToken::ClosePath(Self {}) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map(alt((char('Z'), char('z'))), |_| DToken::ClosePath(Self {}))(input) + } +} + +impl Display for ClosePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Z") + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/ellicptical_arc.rs b/src/ui/panes/pid_new/svg/attributes/data/ellicptical_arc.rs new file mode 100644 index 0000000000000000000000000000000000000000..3fd93c2e9f8c549c182e5c995e975a975a16c4bd --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/ellicptical_arc.rs @@ -0,0 +1,112 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::anychar, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct EllipticalArc { + abs: bool, + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, +} + +impl EllipticalArc { + pub fn abs( + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ) -> DToken { + DToken::EllipticalArc(Self { + abs: true, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + } + + pub fn rel( + rx: f32, + ry: f32, + angle: f32, + large_arc: bool, + sweep: bool, + x: f32, + y: f32, + ) -> DToken { + DToken::EllipticalArc(Self { + abs: false, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('A'), |_| true), map(char('a'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + preceded(char(' '), float), + map(preceded(char(' '), anychar), |c| c == '1'), + map(preceded(char(' '), anychar), |c| c == '1'), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, rx, ry, angle, large_arc, sweep, x, y)| { + DToken::EllipticalArc(Self { + abs, + rx, + ry, + angle, + large_arc, + sweep, + x, + y, + }) + }, + )(input) + } +} + +impl Display for EllipticalArc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = if self.abs { 'A' } else { 'a' }; + write!( + f, + "{} {} {} {} {} {} {} {}", + prefix, + self.rx, + self.ry, + self.angle, + self.large_arc as i32, + self.sweep as i32, + self.x, + self.y + ) + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/horizonta_line_to.rs b/src/ui/panes/pid_new/svg/attributes/data/horizonta_line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..49d9dbca3ed160bdcbf961cdf7123ac33d754048 --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/horizonta_line_to.rs @@ -0,0 +1,46 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct HorizontalLineTo { + abs: bool, + x: f32, +} + +impl HorizontalLineTo { + pub fn abs(x: f32) -> DToken { + DToken::HorizontalLineTo(Self { abs: true, x }) + } + + pub fn rel(x: f32) -> DToken { + DToken::HorizontalLineTo(Self { abs: false, x }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('H'), |_| true), map(char('h'), |_| false))), + preceded(char(' '), float), + )), + |(abs, x)| DToken::HorizontalLineTo(Self { abs, x }), + )(input) + } +} + +impl Display for HorizontalLineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "H {}", self.x) + } else { + write!(f, "h {}", self.x) + } + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/line_to.rs b/src/ui/panes/pid_new/svg/attributes/data/line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2c348f1e97b4d772b3d2c12ee350e011efbece4 --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/line_to.rs @@ -0,0 +1,48 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct LineTo { + abs: bool, + x: f32, + y: f32, +} + +impl LineTo { + pub fn abs(x: f32, y: f32) -> DToken { + DToken::LineTo(Self { abs: true, x, y }) + } + + pub fn rel(x: f32, y: f32) -> DToken { + DToken::LineTo(Self { abs: false, x, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('L'), |_| true), map(char('l'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, x, y)| DToken::LineTo(Self { abs, x, y }), + )(input) + } +} + +impl Display for LineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "L {} {}", self.x, self.y) + } else { + write!(f, "l {} {}", self.x, self.y) + } + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/mod.rs b/src/ui/panes/pid_new/svg/attributes/data/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..f42da87a153d0a2802b71d14a25d613b374bc5e2 --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/mod.rs @@ -0,0 +1,161 @@ +pub mod close_path; +pub mod ellicptical_arc; +pub mod horizonta_line_to; +pub mod line_to; +pub mod move_to; +pub mod vertical_line_to; + +use std::fmt::Display; + +use self::{ + close_path::ClosePath, ellicptical_arc::EllipticalArc, horizonta_line_to::HorizontalLineTo, + line_to::LineTo, move_to::MoveTo, vertical_line_to::VerticalLineTo, +}; +use nom::{branch::alt, character::complete::char, multi::separated_list0, IResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Data { + pub segments: Vec<DToken>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum DToken { + MoveTo(MoveTo), + LineTo(LineTo), + HorizontalLineTo(HorizontalLineTo), + VerticalLineTo(VerticalLineTo), + EllipticalArc(EllipticalArc), + ClosePath(ClosePath), +} + +impl Display for DToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MoveTo(t) => t.fmt(f), + Self::LineTo(t) => t.fmt(f), + Self::HorizontalLineTo(t) => t.fmt(f), + Self::VerticalLineTo(t) => t.fmt(f), + Self::EllipticalArc(t) => t.fmt(f), + Self::ClosePath(t) => t.fmt(f), + } + } +} + +impl Serialize for Data { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.segments + .iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Data { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + let d = separated_list0(char(' '), DToken::parse)(input.as_str()) + .map(|(_, segments)| Self { segments }) + .unwrap_or_default(); + Ok(d) + } +} + +impl DToken { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + MoveTo::parse, + LineTo::parse, + HorizontalLineTo::parse, + VerticalLineTo::parse, + ClosePath::parse, + EllipticalArc::parse, + ))(input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@d")] + d: Data, + } + + #[test] + fn it_serialize() { + let test = Test { + d: Data { + segments: vec![ + MoveTo::abs(-3.12, 1.0), + LineTo::rel(4.0, -4.0), + MoveTo::rel(-1.0, 1.0), + LineTo::abs(5.0, 0.0), + ], + }, + }; + let expected = "<test d=\"M -3.12 1 l 4 -4 m -1 1 L 5 0\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { d: Data::default() }; + let expected = "<test d=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = + "<test d=\"M 0.5 0 V 6 M 1.5 0 v 1 a 2 2 0 1 1 0 4 v 1 M 0 3 h 0.5 M 3.5 3 h 0.5\"/>"; + let expected = Test { + d: Data { + segments: vec![ + MoveTo::abs(0.5, 0.0), + VerticalLineTo::abs(6.0), + MoveTo::abs(1.5, 0.0), + VerticalLineTo::rel(1.0), + EllipticalArc::rel(2.0, 2.0, 0.0, true, true, 0.0, 4.0), + VerticalLineTo::rel(1.0), + MoveTo::abs(0.0, 3.0), + HorizontalLineTo::rel(0.5), + MoveTo::abs(3.5, 3.0), + HorizontalLineTo::rel(0.5), + ], + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test d=\"\"/>"; + let expected = Test { d: Data::default() }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/move_to.rs b/src/ui/panes/pid_new/svg/attributes/data/move_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4d9698a0e40cce9065dc1486de20f0985a4059f --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/move_to.rs @@ -0,0 +1,48 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct MoveTo { + abs: bool, + x: f32, + y: f32, +} + +impl MoveTo { + pub fn abs(x: f32, y: f32) -> DToken { + DToken::MoveTo(Self { abs: true, x, y }) + } + + pub fn rel(x: f32, y: f32) -> DToken { + DToken::MoveTo(Self { abs: false, x, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('M'), |_| true), map(char('m'), |_| false))), + preceded(char(' '), float), + preceded(char(' '), float), + )), + |(abs, x, y)| DToken::MoveTo(Self { abs, x, y }), + )(input) + } +} + +impl Display for MoveTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "M {} {}", self.x, self.y) + } else { + write!(f, "m {} {}", self.x, self.y) + } + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/data/vertical_line_to.rs b/src/ui/panes/pid_new/svg/attributes/data/vertical_line_to.rs new file mode 100644 index 0000000000000000000000000000000000000000..0284a2bea4104a975abb9236b2b418188ebadb4c --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/data/vertical_line_to.rs @@ -0,0 +1,46 @@ +use super::DToken; +use nom::{ + branch::alt, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub struct VerticalLineTo { + abs: bool, + y: f32, +} + +impl VerticalLineTo { + pub fn abs(y: f32) -> DToken { + DToken::VerticalLineTo(Self { abs: true, y }) + } + + pub fn rel(y: f32) -> DToken { + DToken::VerticalLineTo(Self { abs: false, y }) + } + + pub(super) fn parse(input: &str) -> IResult<&str, DToken> { + map( + tuple(( + alt((map(char('V'), |_| true), map(char('v'), |_| false))), + preceded(char(' '), float), + )), + |(abs, y)| DToken::VerticalLineTo(Self { abs, y }), + )(input) + } +} + +impl Display for VerticalLineTo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs { + write!(f, "V {}", self.y) + } else { + write!(f, "v {}", self.y) + } + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/mod.rs b/src/ui/panes/pid_new/svg/attributes/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..cda81f58f16faa19c8561b8eb53820735e98017f --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/mod.rs @@ -0,0 +1,3 @@ +pub mod data; +pub mod style; +pub mod transform; diff --git a/src/ui/panes/pid_new/svg/attributes/style.rs b/src/ui/panes/pid_new/svg/attributes/style.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc022b95f7392a0704b7b820c3de16e53995aede --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/style.rs @@ -0,0 +1,212 @@ +use std::fmt::Display; + +use egui::Color32; +use nom::{ + branch::alt, + bytes::complete::tag, + bytes::complete::take, + character::complete::char, + combinator::{map, opt}, + number::complete::float, + sequence::{preceded, tuple}, + IResult, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Style { + pub fill: Color32, + pub stroke: Color32, + pub stroke_opacity: f32, + pub stroke_width: f32, + pub stroke_linejoin: LineJoin, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum LineJoin { + Bevel, + Miter, + Round, +} + +impl Style { + fn parse(input: &str) -> IResult<&str, Self> { + map( + tuple(( + opt(map(preceded(tag("fill:"), take(9usize)), Color32::from_hex)), + opt(char(';')), + opt(map( + preceded(tag("stroke:"), take(9usize)), + Color32::from_hex, + )), + opt(char(';')), + opt(preceded(tag("stroke-opacity:"), float)), + opt(char(';')), + opt(preceded(tag("stroke-width:"), float)), + opt(char(';')), + opt(preceded(tag("stroke-linejoin:"), LineJoin::parse)), + )), + |(fill, _, stroke, _, stroke_opacity, _, stroke_width, _, stroke_linejoin)| { + let fill = fill.and_then(|c| c.ok()).unwrap_or(Color32::BLACK); + let stroke = stroke.and_then(|c| c.ok()).unwrap_or(Color32::TRANSPARENT); + Self { + fill, + stroke, + stroke_opacity: stroke_opacity.unwrap_or(1.0), + stroke_width: stroke_width.unwrap_or(1.0), + stroke_linejoin: stroke_linejoin.unwrap_or_default(), + } + }, + )(input) + } +} + +impl Default for Style { + fn default() -> Self { + Self { + fill: Color32::BLACK, + stroke: Color32::TRANSPARENT, + stroke_opacity: 1.0, + stroke_width: 1.0, + stroke_linejoin: LineJoin::Miter, + } + } +} + +impl Serialize for Style { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut style = Vec::new(); + + if self.fill != Color32::BLACK { + style.push(format!("fill:{}", self.fill.to_hex())); + } + if self.stroke != Color32::TRANSPARENT { + style.push(format!("stroke:{}", self.stroke.to_hex())); + } + if self.stroke_opacity != 1.0 { + style.push(format!("stroke-opacity:{}", self.stroke_opacity)); + } + if self.stroke_width != 1.0 { + style.push(format!("stroke-width:{}", self.stroke_width)); + } + if self.stroke_linejoin != LineJoin::Miter { + style.push(format!("stroke-linejoin:{}", self.stroke_linejoin)); + } + + style.join(";").serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Style { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + Ok(Style::parse(input.as_str()) + .map(|(_, style)| style) + .unwrap_or_default()) + } +} + +impl LineJoin { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + map(tag("bevel"), |_| Self::Bevel), + map(tag("miter"), |_| Self::Miter), + map(tag("round"), |_| Self::Round), + ))(input) + } +} + +impl Default for LineJoin { + fn default() -> Self { + Self::Miter + } +} + +impl Display for LineJoin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bevel => write!(f, "bevel"), + Self::Miter => write!(f, "miter"), + Self::Round => write!(f, "round"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@style")] + style: Style, + } + + #[test] + fn it_serialize() { + let test = Test { + style: Style { + fill: Color32::from_rgb(23, 45, 76), + stroke: Color32::from_rgb(123, 42, 29), + stroke_opacity: 0.5, + stroke_width: 3.21, + stroke_linejoin: LineJoin::Bevel, + }, + }; + let expected = "<test style=\"fill:#172d4cff;stroke:#7b2a1dff;stroke-opacity:0.5;stroke-width:3.21;stroke-linejoin:bevel\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { + style: Style::default(), + }; + let expected = "<test style=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test style=\"fill:#43516fff;stroke:#001831ff;stroke-opacity:0.741;stroke-width:11.5;stroke-linejoin:round\"/>"; + let expected = Test { + style: Style { + fill: Color32::from_rgb(67, 81, 111), + stroke: Color32::from_rgb(0, 24, 49), + stroke_opacity: 0.741, + stroke_width: 11.5, + stroke_linejoin: LineJoin::Round, + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test style=\"\"/>"; + let expected = Test { + style: Style::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid_new/svg/attributes/transform.rs b/src/ui/panes/pid_new/svg/attributes/transform.rs new file mode 100644 index 0000000000000000000000000000000000000000..821a42bdc80f6032a525494fc8655b9e23babac1 --- /dev/null +++ b/src/ui/panes/pid_new/svg/attributes/transform.rs @@ -0,0 +1,225 @@ +use egui::emath::Rot2; +use egui::{Pos2, Vec2}; +use nom::IResult; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::char, + combinator::map, + number::complete::float, + sequence::{delimited, preceded, tuple}, +}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::string::ToString; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Transform { + pub rotate: Rotate, + pub translate: Translate, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Rotate { + pub angle: f32, + pub x: f32, + pub y: f32, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Translate { + pub x: f32, + pub y: f32, +} + +impl Transform { + pub fn to_local_frame(&self, pos: Pos2) -> Pos2 { + let pos = pos.to_vec2(); + + // Apply the rotation + let rot = Rot2::from_angle(-self.rotate.angle); + let pos = self.rotate.to_vec2() + rot * (pos - self.rotate.to_vec2()); + + // Apply the translation + let pos = pos - self.translate.to_vec2(); + + pos.to_pos2() + } +} + +impl Serialize for Transform { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut transform = String::new(); + if self.rotate.angle != 0.0 { + transform.push_str(&self.rotate.to_string()); + } + if self.translate.x != 0.0 && self.translate.y != 0.0 { + transform.push_str(&self.translate.to_string()); + } + transform.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Transform { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let input = String::deserialize(deserializer)?; + + let mut transform = Transform::default(); + let input = if let Ok((input, rotate)) = Rotate::parse(&input) { + transform.rotate = rotate; + input + } else { + &input + }; + if let Ok((_, translate)) = Translate::parse(input) { + transform.translate = translate; + } + + Ok(transform) + } +} + +impl Rotate { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + map( + delimited(tag("rotate("), preceded(char(' '), float), char(')')), + |angle| Self { + angle, + x: 0.0, + y: 0.0, + }, + ), + map( + delimited( + tag("rotate("), + tuple(( + float, + preceded(char(' '), float), + preceded(char(' '), float), + )), + char(')'), + ), + |(angle, x, y)| Self { angle, x, y }, + ), + ))(input) + } + + pub fn to_vec2(&self) -> Vec2 { + Vec2::new(self.x, self.y) + } +} + +impl Display for Rotate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.x == 0.0 && self.y == 0.0 { + write!(f, "rotate({})", self.angle) + } else { + write!(f, "rotate({} {} {})", self.angle, self.x, self.y) + } + } +} + +impl Translate { + fn parse(input: &str) -> IResult<&str, Self> { + map( + delimited( + tag("translate("), + tuple((float, preceded(char(' '), float))), + char(')'), + ), + |(x, y)| Self { x, y }, + )(input) + } + + pub fn to_vec2(&self) -> Vec2 { + Vec2::new(self.x, self.y) + } +} + +impl Display for Translate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "translate({} {})", self.x, self.y) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Test { + #[serde(rename = "@transform")] + transform: Transform, + } + + #[test] + fn it_serialize() { + let test = Test { + transform: Transform { + rotate: Rotate { + angle: 3.15, + x: 7.0, + y: -1.23, + }, + translate: Translate { x: 4.0, y: -1.4 }, + }, + }; + let expected = "<test transform=\"rotate(3.15 7 -1.23)translate(4 -1.4)\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Test { + transform: Transform::default(), + }; + let expected = "<test transform=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("test")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test transform=\"rotate(45.7 -43.21 89)translate(4 -1.4)\"/>"; + let expected = Test { + transform: Transform { + rotate: Rotate { + angle: 45.7, + x: -43.21, + y: 89.0, + }, + translate: Translate { x: 4.0, y: -1.4 }, + }, + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<test transform=\"\"/>"; + let expected = Test { + transform: Transform::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Test::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid_new/svg/elements.rs b/src/ui/panes/pid_new/svg/elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b42c4d5a06c1809a74ef7641e6fe1ac9b441d25 --- /dev/null +++ b/src/ui/panes/pid_new/svg/elements.rs @@ -0,0 +1,4 @@ +pub mod defs; +pub mod path; +pub mod text; +pub mod use_node; diff --git a/src/ui/panes/pid_new/svg/elements/defs.rs b/src/ui/panes/pid_new/svg/elements/defs.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b5b26100679d428e2abb18fb1446b17e67ceb55 --- /dev/null +++ b/src/ui/panes/pid_new/svg/elements/defs.rs @@ -0,0 +1,14 @@ +use super::{path::Path, text::Text}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq)] +pub struct Defs { + #[serde(rename = "path")] + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub paths: Vec<Path>, + + #[serde(rename = "text")] + #[serde(default)] + pub texts: Vec<Text>, +} diff --git a/src/ui/panes/pid_new/svg/elements/path.rs b/src/ui/panes/pid_new/svg/elements/path.rs new file mode 100644 index 0000000000000000000000000000000000000000..7bbc52d95353c85a3be937cedd5b41c195bbbda7 --- /dev/null +++ b/src/ui/panes/pid_new/svg/elements/path.rs @@ -0,0 +1,97 @@ +use super::super::{ + attributes::{data::Data, style::Style, transform::Transform}, + utils::{is_default, is_zero}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone)] +pub struct Path { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@d")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub data: Data, + + #[serde(rename = "@style")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub style: Style, + + #[serde(rename = "@transfrom")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_serialize() { + let test = Path { + id: "paolino".to_string(), + width: 23.4, + height: 5.0, + data: Data::default(), + style: Style::default(), + transform: Transform::default(), + }; + let expected = "<path id=\"paolino\" width=\"23.4\" height=\"5\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("path")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_serialize_default() { + let test = Path::default(); + let expected = "<path id=\"\"/>"; + + let mut buffer = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buffer, Some("path")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(buffer, expected); + } + + #[test] + fn it_deserialize() { + let test = "<test id=\"pepperoncino\" width=\"11\" height=\"24.5\"/>"; + let expected = Path { + id: "pepperoncino".to_string(), + width: 11.0, + height: 24.5, + data: Data::default(), + style: Style::default(), + transform: Transform::default(), + }; + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Path::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let test = "<path id=\"\"/>"; + let expected = Path::default(); + + let mut des = quick_xml::de::Deserializer::from_str(test); + let deserialized = Path::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid_new/svg/elements/text.rs b/src/ui/panes/pid_new/svg/elements/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..8181ef9ce5c2cf7051d1312054b2f154557b55dd --- /dev/null +++ b/src/ui/panes/pid_new/svg/elements/text.rs @@ -0,0 +1,43 @@ +use super::super::{ + attributes::transform::Transform, + utils::{is_default, is_zero}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct Text { + #[serde(rename = "@font-size")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub font_size: f32, + + #[serde(rename = "@font-family")] + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub font_family: String, + + #[serde(rename = "@transform")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, + + #[serde(rename = "@segs-format")] + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub format: String, + + #[serde(rename = "$text")] + pub text: String, +} + +impl Text { + pub fn new(text: String, size: f32) -> Self { + Self { + font_size: size, + font_family: "monospace".to_string(), + transform: Transform::default(), + format: "TODO".to_string(), + text, + } + } +} diff --git a/src/ui/panes/pid_new/svg/elements/use_node.rs b/src/ui/panes/pid_new/svg/elements/use_node.rs new file mode 100644 index 0000000000000000000000000000000000000000..14f139b9fe774420d3f5522ae1f610cb797081a6 --- /dev/null +++ b/src/ui/panes/pid_new/svg/elements/use_node.rs @@ -0,0 +1,92 @@ +use super::super::{ + attributes::transform::Transform, + utils::{is_default, is_zero}, +}; +use egui::{InputState, Pos2}; +use glam::Vec2; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct Use { + #[serde(rename = "@href", with = "href")] + pub href: String, + + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@transform")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub transform: Transform, +} + +impl Use { + pub fn handle_click(&mut self, _pos: Pos2) {} + pub fn handle_double_click(&mut self, _pos: Pos2) {} + + /// Consumes any shortcut the element should respond to + pub fn handle_shortcuts(&mut self, _input: &mut InputState) {} + + pub fn hovered(&self, pos: Pos2) -> bool { + let pos = self.transform.to_local_frame(pos).to_vec2(); + + // The bounding box in the elemen's frame is defined by the size. But + // width and height can be negative. This allows to represent where the + // position anchor point is (for paths is the top-left, for texts + // is the bottom-left) + let size = Vec2::new(self.width, self.height); + let min = Vec2::ZERO.min(size); + let max = Vec2::ZERO.max(size); + + // Check if the point is in the bounding box + min.x <= pos.x && pos.x <= max.x && min.y <= pos.y && pos.y <= max.y + } + + /// Whether the elemen can be moved + pub fn draggable(&self) -> bool { + false + } + + /// Whether a window to edit the element's configuration is available + pub fn editable(&self) -> bool { + false + } + + pub fn who_am_i(&self) -> String { + format!( + "{} at x={} y={} width={} height={}", + self.href, + self.transform.translate.x, + self.transform.translate.y, + self.width, + self.height + ) + } +} + +mod href { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize<S>(id: &str, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + format!("#{id}").serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error> + where + D: Deserializer<'de>, + { + let mut id = String::deserialize(deserializer)?; + id.remove(0); + Ok(id) + } +} diff --git a/src/ui/panes/pid_new/svg/mod.rs b/src/ui/panes/pid_new/svg/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8634d3d38bc68886546124c00f66ce6b1964a54 --- /dev/null +++ b/src/ui/panes/pid_new/svg/mod.rs @@ -0,0 +1,263 @@ +pub mod attributes; +pub mod elements; +pub mod utils; + +use attributes::data::{ + close_path::ClosePath, horizonta_line_to::HorizontalLineTo, line_to::LineTo, move_to::MoveTo, + vertical_line_to::VerticalLineTo, Data, +}; +use attributes::style::{LineJoin, Style}; +use attributes::transform::Transform; +use egui::Color32; +use elements::path::Path; +use elements::{defs::Defs, use_node::Use}; +use serde::{Deserialize, Serialize}; +use utils::{is_default, is_zero}; + +use super::{Element, Pid3}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Svg { + #[serde(rename = "@width")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub width: f32, + + #[serde(rename = "@height")] + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub height: f32, + + #[serde(rename = "@version")] + pub version: f32, // 1.1 + + #[serde(rename = "@xmlns")] + pub xmlns: String, // http://www.w3.org/2000/svg + + #[serde(rename = "defs")] + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub defs: Defs, + + #[serde(rename = "use")] + #[serde(default)] + pub uses: Vec<Use>, +} + +impl Default for Svg { + fn default() -> Self { + Self { + width: 0.0, + height: 0.0, + version: 1.1, + xmlns: "http://www.w3.org/2000/svg".to_string(), + defs: Defs::default(), + uses: Vec::new(), + } + } +} + +impl From<&Pid3> for Svg { + fn from(pid: &Pid3) -> Self { + let mut svg = Self { + // TODO: Compute size + width: 10.0, + height: 10.0, + ..Default::default() + }; + + pid.elements.iter().for_each(|(id, element)| match element { + Element::Icon => { + svg.defs.paths.push(Path { + id: id.clone(), + width: 4.0, + height: 4.0, + data: Data { + segments: vec![ + MoveTo::abs(0.7, 2.0), + LineTo::rel(2.6, -1.5), + VerticalLineTo::rel(3.0), + ClosePath::token(), + MoveTo::abs(0.0, 2.0), + HorizontalLineTo::rel(4.0), + ], + }, + style: Style { + stroke: Color32::BLACK, + stroke_width: 0.2, + stroke_linejoin: LineJoin::Round, + ..Default::default() + }, + ..Default::default() + }); + } + Element::Label => {} + }); + + pid.references + .iter() + .flat_map(|r| Some(r).zip(pid.elements.get(&r.id))) + .for_each(|(r, _)| { + svg.uses.push(Use { + href: r.id.clone(), + width: 4.0, + height: 4.0, + transform: Transform::default(), + }); + }); + + svg + } +} + +#[cfg(test)] +mod tests { + + use super::{ + attributes::{ + data::ellicptical_arc::EllipticalArc, + transform::{Rotate, Transform, Translate}, + }, + elements::text::Text, + *, + }; + + fn test_svg() -> Svg { + Svg { + width: 14.0, + height: 17.196152, + defs: Defs { + paths: vec![ + Path { + id: "arrow".to_string(), + width: 4.0, + height: 4.0, + data: Data { + segments: vec![ + MoveTo::abs(0.7, 2.0), + LineTo::rel(2.6, -1.5), + VerticalLineTo::rel(3.0), + ClosePath::token(), + MoveTo::abs(0.0, 2.0), + HorizontalLineTo::rel(4.0), + ], + }, + style: Style { + stroke: Color32::BLACK, + stroke_width: 0.2, + stroke_linejoin: LineJoin::Round, + ..Default::default() + }, + ..Default::default() + }, + Path { + id: "burst_disk".to_string(), + width: 4.0, + height: 6.0, + data: Data { + segments: vec![ + MoveTo::abs(0.5, 0.0), + VerticalLineTo::abs(6.0), + MoveTo::abs(1.5, 0.0), + VerticalLineTo::rel(1.0), + EllipticalArc::rel(2.0, 2.0, 0.0, true, true, 0.0, 4.0), + VerticalLineTo::rel(1.0), + MoveTo::abs(0.0, 3.0), + HorizontalLineTo::rel(0.5), + MoveTo::abs(3.5, 3.0), + HorizontalLineTo::rel(0.5), + ], + }, + style: Style { + stroke: Color32::BLACK, + fill: Color32::TRANSPARENT, + stroke_width: 0.2, + stroke_linejoin: LineJoin::Round, + ..Default::default() + }, + ..Default::default() + }, + ], + texts: vec![Text { + font_size: 2.0, + font_family: "monospace".to_string(), + transform: Transform { + translate: Translate { x: 1.0, y: 3.0 }, + ..Default::default() + }, + format: "{:.2f}".to_string(), + text: "Hi mom!".to_string(), + }], + }, + uses: vec![ + Use { + href: "arrow".to_string(), + width: 4.0, + height: 4.0, + transform: Transform { + rotate: Rotate { + angle: 180.0, + x: 7.0, + y: 8.0, + }, + translate: Translate { x: 5.0, y: 6.0 }, + }, + }, + Use { + href: "burst_disk".to_string(), + width: 4.0, + height: 6.0, + transform: Transform { + translate: Translate { x: 1.0, y: 5.0 }, + ..Default::default() + }, + }, + ], + ..Default::default() + } + } + + #[test] + fn it_serialize() { + let test = test_svg(); + let expected = + String::from_utf8(std::fs::read("test_assets/simple_pid.svg").unwrap()).unwrap(); + + let mut serialized = String::new(); + let mut ser = quick_xml::se::Serializer::with_root(&mut serialized, Some("svg")).unwrap(); + ser.indent(' ', 4); + test.serialize(ser).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn it_serialize_default() { + let test = Svg::default(); + let expected = "<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"/>"; + + let mut serialized = String::new(); + let ser = quick_xml::se::Serializer::with_root(&mut serialized, Some("svg")).unwrap(); + test.serialize(ser).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn it_deserializes() { + let test = String::from_utf8(std::fs::read("test_assets/simple_pid.svg").unwrap()).unwrap(); + let expected = test_svg(); + + let mut des = quick_xml::de::Deserializer::from_str(&test); + let deserialized = Svg::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn it_deserialize_default() { + let svg = "<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"/>"; + let expected = Svg::default(); + + let mut des = quick_xml::de::Deserializer::from_str(svg); + let deserialized = Svg::deserialize(&mut des).unwrap(); + assert_eq!(deserialized, expected); + } +} diff --git a/src/ui/panes/pid_new/svg/utils.rs b/src/ui/panes/pid_new/svg/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..16be55525989331c0a3a2c05c4ee320d927020a0 --- /dev/null +++ b/src/ui/panes/pid_new/svg/utils.rs @@ -0,0 +1,7 @@ +pub fn is_default<T: Default + PartialEq>(x: &T) -> bool { + *x == T::default() +} + +pub fn is_zero(x: &f32) -> bool { + *x == 0.0 +} diff --git a/src/ui/widget_gallery.rs b/src/ui/widget_gallery.rs index a5288191848c04212c0845c403ad12b1c8d1ea31..80ff45596812d5f81fad72ad53fc6dfed891d61d 100644 --- a/src/ui/widget_gallery.rs +++ b/src/ui/widget_gallery.rs @@ -4,7 +4,7 @@ use strum::{EnumMessage, IntoEnumIterator}; use super::{ composable_view::PaneAction, - panes::{Pane, PaneKind}, + panes::{pid::Pid2, Pane, PaneKind}, }; #[derive(Default)] @@ -28,6 +28,16 @@ impl WidgetGallery { for pane in PaneKind::iter() { if let PaneKind::Default(_) = pane { continue; + } else if let PaneKind::Pid(_) = pane { + let pid = Pid2::from_file(); + if ui.button("PID").clicked() { + if let Some(tile_id) = self.tile_id { + return Some(PaneAction::Replace( + tile_id, + Pane::boxed(PaneKind::Pid(pid)), + )); + } + } } else if let Some(message) = pane.get_message() { if ui.button(message).clicked() { if let Some(tile_id) = self.tile_id { diff --git a/test_assets/simple_pid.svg b/test_assets/simple_pid.svg new file mode 100644 index 0000000000000000000000000000000000000000..bb0a40a22f01801300c8d6b6fc3925d779830091 --- /dev/null +++ b/test_assets/simple_pid.svg @@ -0,0 +1,9 @@ +<svg width="14" height="17.196152" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <defs> + <path id="arrow" width="4" height="4" d="M 0.7 2 l 2.6 -1.5 v 3 Z M 0 2 h 4" style="stroke:#000000ff;stroke-width:0.2;stroke-linejoin:round"/> + <path id="burst_disk" width="4" height="6" d="M 0.5 0 V 6 M 1.5 0 v 1 a 2 2 0 1 1 0 4 v 1 M 0 3 h 0.5 M 3.5 3 h 0.5" style="fill:#00000000;stroke:#000000ff;stroke-width:0.2;stroke-linejoin:round"/> + </defs> + <use href="#arrow" width="4" height="4" transform="rotate(180 7 8)translate(5 6)"/> + <use href="#burst_disk" width="4" height="6" transform="translate(1 5)"/> + <text font-size="2" transform="translate(1 3)" segs-format="{:.2f}">Hi mom!</text> +</svg> diff --git a/test_assets/simple_text.svg b/test_assets/simple_text.svg new file mode 100644 index 0000000000000000000000000000000000000000..764e477a65803785edfee24c74390e2461a96050 --- /dev/null +++ b/test_assets/simple_text.svg @@ -0,0 +1,7 @@ +<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <defs> + <path id="line" style="stroke:#000000ff;stroke-width:5" d="M 10 10 L 200 200"/> + </defs> + <use href="#line"/> + <text transform="translate(100 100)" font-size="20" font-family="monospace">Your text here</text> +</svg>