From d9cae597f93e172a1d474bd5fe3a8bb5aa55adc5 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Thu, 16 Jul 2026 05:33:05 +0300 Subject: [PATCH] feat(iced): implement shape_sync bidirectional sync --- hcie-iced-app/crates/hcie-iced-gui/src/app.rs | 14 +- .../crates/hcie-iced-gui/src/main.rs | 1 + .../crates/hcie-iced-gui/src/shape_sync.rs | 161 ++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 7193832..475b412 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -1447,15 +1447,14 @@ impl HcieIcedApp { Message::FgColorChanged(color) => { self.fg_color = color; self.active_document_mut().engine.set_color(color); - // Record the new color in the recent-colors list so the - // picker's recent swatches populate (egui does this on - // drag-stopped/click in color.rs:316/816/915). self.add_recent_color(color); self.colors_dirty = true; + crate::shape_sync::sync_shape_from_tool(self); } Message::BgColorChanged(color) => { self.bg_color = color; + crate::shape_sync::sync_shape_from_tool(self); } Message::SwapColors => { @@ -1464,6 +1463,7 @@ impl HcieIcedApp { self.active_document_mut().engine.set_color(color); self.add_recent_color(color); self.colors_dirty = true; + crate::shape_sync::sync_shape_from_tool(self); } Message::ResetColors => { // Reset fg=black, bg=white (egui: toolbox.rs:351-354). @@ -2725,26 +2725,32 @@ impl HcieIcedApp { } Message::VectorStrokeChanged(s) => { self.settings.tool_settings.vector_stroke_width = s; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::VectorOpacityChanged(o) => { self.settings.tool_settings.vector_opacity = o; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::VectorFillToggled(f) => { self.settings.tool_settings.vector_fill = f; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::VectorRadiusChanged(r) => { self.settings.tool_settings.vector_radius = r; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::VectorPointsChanged(p) => { self.settings.tool_settings.vector_points = p; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::VectorSidesChanged(s) => { self.settings.tool_settings.vector_sides = s; + crate::shape_sync::sync_shape_from_tool(self); let _ = self.settings.save(); } Message::TextFontChanged(font) => { @@ -3487,6 +3493,7 @@ impl HcieIcedApp { doc.vector_draw = None; // clear any preview doc.vector_edit_handle = VEH::Move; self.vector_drag_last = Some((x, y)); + crate::shape_sync::sync_tool_from_shape(self); } else { // No shape under cursor — deselect. let doc = &mut self.documents[self.active_doc]; @@ -3561,6 +3568,7 @@ impl HcieIcedApp { if idx.is_some() { let snapshot = self.documents[self.active_doc].engine.active_vector_shapes(); self.vector_shapes_snapshot = snapshot; + crate::shape_sync::sync_tool_from_shape(self); } else { self.vector_shapes_snapshot = None; } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs index 4be9d1a..2c473a9 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs @@ -16,6 +16,7 @@ mod panels; mod script; mod selection; mod settings; +mod shape_sync; mod sidebar; mod theme; mod viewer; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs b/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs new file mode 100644 index 0000000..cf5de8f --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs @@ -0,0 +1,161 @@ +//! Bidirectional vector shape ↔ tool state synchronization. +//! +//! Purpose: Keep the iced GUI's tool settings (stroke, fill, opacity, colors) +//! and the selected VectorShape in the document engine in sync. When the user +//! changes tool properties while a shape is selected, push them to the shape. +//! When a shape is selected, pull its properties back into the tool state. +//! +//! Logic & Workflow: +//! - `sync_shape_from_tool`: Reads tool settings + fg/bg colors from the app, +//! writes all matching properties into the engine's selected vector shape. +//! - `sync_tool_from_shape`: Reads the engine's selected shape properties, +//! writes them back into tool settings and app color state. +//! +//! Side Effects: Mutates both the engine (via `mark_composite_dirty`) and +//! the app's `settings.tool_settings` / `fg_color` / `bg_color`. + +use crate::app::HcieIcedApp; +use hcie_engine_api::{Tool, VectorShape}; + +/// Push tool state properties (stroke, fill, color, opacity, hardness) +/// into the currently selected vector shape. +/// +/// Called when the user changes a tool property or picks a color while +/// a shape is selected in VectorSelect mode. +pub fn sync_shape_from_tool(app: &mut HcieIcedApp) { + if app.tool_state.active_tool != Tool::VectorSelect { + return; + } + let shape_idx = match app.documents[app.active_doc].selected_vector_shape { + Some(i) => i, + None => return, + }; + + // Snapshot for undo (before first modification in this edit session) + if app.vector_shapes_snapshot.is_none() { + let snapshot = app.documents[app.active_doc].engine.active_vector_shapes(); + app.vector_shapes_snapshot = snapshot; + } + + let layer_id = app.documents[app.active_doc].engine.active_layer_id(); + let ts = &app.settings.tool_settings; + + // Stroke width + app.documents[app.active_doc] + .engine + .set_vector_shape_stroke(layer_id, shape_idx, ts.vector_stroke_width); + + // Fill toggle — only push if shape has a fill or the value changed + if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() { + if let Some(shape) = shapes.get(shape_idx) { + if shape.fill_color().is_some() || shape.is_filled() != ts.vector_fill { + app.documents[app.active_doc] + .engine + .set_vector_shape_fill(layer_id, shape_idx, ts.vector_fill); + } + } + } + + // Opacity + app.documents[app.active_doc] + .engine + .set_vector_shape_opacity(layer_id, shape_idx, ts.vector_opacity); + + // Hardness — use brush_hardness as the shared hardness value + app.documents[app.active_doc] + .engine + .set_vector_shape_hardness(layer_id, shape_idx, app.tool_state.brush_hardness); + + // Primary color (stroke color) + app.documents[app.active_doc] + .engine + .set_vector_shape_color(layer_id, shape_idx, app.fg_color); + + // Fill color — only push if shape already has a fill color + if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() { + if let Some(shape) = shapes.get(shape_idx) { + if shape.fill_color().is_some() { + app.documents[app.active_doc] + .engine + .set_vector_shape_fill_color(layer_id, shape_idx, app.bg_color); + } + } + } + + // Shape-specific properties (radius, points, sides) + if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() { + if let Some(shape) = shapes.get(shape_idx) { + match shape { + VectorShape::Rect { .. } => { + // round_rect_radius is set directly via engine if needed + } + VectorShape::Star { .. } => { + // star_points / inner_radius via engine when available + } + VectorShape::Polygon { .. } => { + // polygon_sides via engine when available + } + _ => {} + } + } + } +} + +/// Pull properties from the currently selected vector shape back into +/// tool settings and app color state. +/// +/// Called when the user clicks/selects a shape in VectorSelect mode +/// so the properties panel and tool state reflect the shape's actual values. +pub fn sync_tool_from_shape(app: &mut HcieIcedApp) { + if app.tool_state.active_tool != Tool::VectorSelect { + return; + } + let shape_idx = match app.documents[app.active_doc].selected_vector_shape { + Some(i) => i, + None => return, + }; + + if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() { + if let Some(shape) = shapes.get(shape_idx) { + let ts = &mut app.settings.tool_settings; + + // Common properties + ts.vector_stroke_width = shape.stroke(); + ts.vector_opacity = shape.shape_opacity(); + app.tool_state.brush_hardness = shape.hardness(); + ts.vector_fill = shape.is_filled(); + + // Shape-specific properties + match shape { + VectorShape::Rect { radius, .. } => { + ts.vector_radius = *radius; + } + VectorShape::Star { + points, + inner_radius: _, + .. + } => { + ts.vector_points = *points; + } + VectorShape::Polygon { sides, .. } => { + ts.vector_sides = *sides; + } + VectorShape::Line { + cap_start, + cap_end: _, + .. + } => { + // Store line caps in settings if available + let _ = cap_start; + } + _ => {} + } + + // Colors + app.fg_color = shape.color(); + if let Some(fc) = shape.fill_color() { + app.bg_color = fc; + } + } + } +}