From cbe03f74a3d0b3aa13f8830c7e53db2c33663827 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Tue, 21 Jul 2026 05:08:08 +0300 Subject: [PATCH] MiMo 2.5 feat: enhance vector shape handling with angle snapping and color state management --- ...84597466971-vector-state-management-fix.md | 120 ++++++++++++++++++ .../crates/hcie-gui-egui/src/canvas/mod.rs | 18 ++- hcie-iced-app/crates/hcie-iced-gui/src/app.rs | 18 ++- .../crates/hcie-iced-gui/src/dock/view.rs | 9 +- .../crates/hcie-iced-gui/src/shape_sync.rs | 18 +-- .../crates/hcie-iced-gui/src/vector_edit.rs | 30 ++++- notlar.txt | 1 + 7 files changed, 194 insertions(+), 20 deletions(-) create mode 100644 .kilo/plans/1784597466971-vector-state-management-fix.md diff --git a/.kilo/plans/1784597466971-vector-state-management-fix.md b/.kilo/plans/1784597466971-vector-state-management-fix.md new file mode 100644 index 0000000..64040be --- /dev/null +++ b/.kilo/plans/1784597466971-vector-state-management-fix.md @@ -0,0 +1,120 @@ +# Plan: Fix Vector Tool Angle Persistence and Color State Leaking + +## Problem Analysis + +Both issues stem from `sync_tool_from_shape()` in `hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs`. + +### Issue 1: Angle Persistence + +**Root cause:** Two mechanisms conspire to leak angle state: + +1. `sync_tool_from_shape()` (line 150) copies the selected shape's angle into `tool_settings.vector_angle`: + ```rust + ts.vector_angle = shape.angle().to_degrees(); + ``` + +2. Canvas drag-rotation (line 6397 in `app.rs`) also writes the rotated angle back: + ```rust + self.settings.tool_settings.vector_angle = angle.to_degrees(); + ``` + +3. New SVG shape creation (line 5768 in `app.rs`) reads from the same field: + ```rust + let svg_angle = self.settings.tool_settings.vector_angle.to_radians(); + ``` + Then applies it to every new SVG shape at line 6203-6204: + ```rust + if matches!(shape, hcie_engine_api::VectorShape::SvgShape { .. }) { + shape.set_angle(svg_angle); + } + ``` + +**Flow that reproduces the bug:** +1. Draw a shape (angle=0, correct) +2. Select the shape → `sync_tool_from_shape` copies its angle to `tool_settings.vector_angle` +3. Rotate via drag → `tool_settings.vector_angle` updated to rotated value (e.g., 45°) +4. Draw a NEW shape → `svg_angle = 45°` → new shape created with 45° rotation + +**Desired behavior:** Every new drawing starts at angle 0, while the properties panel angle slider continues to work for editing selected shapes. + +### Issue 2: Color State Leaking + +**Root cause:** `sync_tool_from_shape()` (lines 182-186) copies the selected shape's colors into the app's active palette: + +```rust +app.fg_color = shape.color(); +if let Some(fc) = shape.fill_color() { + app.bg_color = fc; +} +app.colors_dirty = true; +``` + +This is called from `VectorShapeSelect` handler (line 6527 in `app.rs`). + +**Desired behavior:** Selecting a shape should NOT change the active color palette. The palette should only change when the user explicitly picks a new color. + +--- + +## Implementation Plan + +### File 1: `hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs` + +**Change A — Remove angle sync from `sync_tool_from_shape` (line 150):** + +Delete the line: +```rust +ts.vector_angle = shape.angle().to_degrees(); +``` + +The Properties panel (`properties.rs`) already receives `vector_angle` from `tool_settings` and passes it to the slider. When the user changes the slider, `VectorAngleChanged` writes directly to `tool_settings.vector_angle` and calls `sync_shape_from_tool`. The selected shape's own angle will no longer contaminate the "default for new shapes" value. + +**Change B — Remove color sync from `sync_tool_from_shape` (lines 182-186):** + +Delete these lines: +```rust +// Colors +app.fg_color = shape.color(); +if let Some(fc) = shape.fill_color() { + app.bg_color = fc; +} +app.colors_dirty = true; +``` + +The Geometry and Properties panels already display the selected shape's stroke/fill colors directly from the engine (`shapes[idx].color()`, `shapes[idx].fill_color()`), so the UI remains correct. Color changes in those panels go directly to the engine via `set_vector_shape_color` / `set_vector_shape_fill_color`, independent of `fg_color`/`bg_color`. + +### File 2: `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` + +**Change C — Reset angle after new shape creation (after line 6206):** + +After `engine.add_vector_shape(shape)` at line 6206, add: +```rust +self.settings.tool_settings.vector_angle = 0.0; +``` + +This provides a safety net: even if something else writes to `vector_angle` between the last `sync_tool_from_shape` and the next draw, the angle resets after each new shape is committed. The reset also applies for non-SVG shapes (Rect, Circle, Line), ensuring complete consistency. + +--- + +## Files Modified + +| File | Change | +|------|--------| +| `hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs` | Remove angle copy (line 150) and color copy (lines 182-186) from `sync_tool_from_shape` | +| `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` | Reset `vector_angle` to 0.0 after `add_vector_shape` in `VectorDrawEnd` | + +## What Is Preserved + +- **Properties panel angle slider:** Still works. It reads from `tool_settings.vector_angle` and writes via `VectorAngleChanged`. The slider just won't auto-populate from the selected shape anymore — it retains its value until explicitly changed. +- **Geometry panel stroke/fill colors:** Still read directly from the engine shape, unaffected by this change. +- **Other tool properties (stroke, opacity, fill, radius, points, sides):** Unaffected — `sync_tool_from_shape` still syncs all non-angle, non-color properties. +- **Color palette when picking colors:** Still works. `sync_shape_from_tool` is still called when the user changes colors while a shape is selected, pushing the new palette color to the shape. + +## Validation + +1. Build: `cargo check -p hcie-iced-gui` +2. Manual test sequence: + - Draw a Rect → select → rotate to 30° → deselect → draw a new Rect → verify new Rect has angle 0° + - Draw a Star → select → change angle slider to 45° → deselect → draw a new Star → verify new Star has angle 0° + - Select a shape → verify active palette color is NOT changed + - Change palette color while shape is selected → verify shape color updates + - Use Properties panel angle slider on selected shape → verify the shape's angle changes correctly diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs index 66d4850..ad0e871 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs @@ -1019,6 +1019,11 @@ impl<'a> egui::Widget for CanvasWidget<'a> { .engine .vector_shape_angle(layer_id, shape_idx); angle += da; + let shift = ui.input(|i| i.modifiers.shift); + if shift { + let step = std::f32::consts::PI / 12.0; + angle = (angle / step).round() * step; + } self.doc .engine .set_vector_shape_angle(layer_id, shape_idx, angle); @@ -2230,10 +2235,15 @@ fn create_shape_from_drag( let mut p = std::collections::HashMap::new(); p.insert("thick".to_string(), 0.0); match hcie_engine_api::create_vector_shape("arrow", x1, y1, x2, y2, &p) { - VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { - name: String::new(), kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, - angle: 0.0, opacity, hardness, - }, + VectorShape::SvgShape { kind, svg, .. } => { + // Derive the arrow's rotation from the drag direction so + // it points along the line drawn by the user. + let arrow_angle = (y2 - y1).atan2(x2 - x1); + VectorShape::SvgShape { + name: String::new(), kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: arrow_angle, opacity, hardness, + } + } _ => unreachable!(), } }, 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 1a65914..c3b8382 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -6200,11 +6200,26 @@ impl HcieIcedApp { }; if let Some(mut shape) = shape { - if matches!(shape, hcie_engine_api::VectorShape::SvgShape { .. }) { + // For arrow shapes, derive the angle from the drag + // direction so the arrow points along the drawn line. + if matches!( + self.tool_state.active_tool, + hcie_engine_api::Tool::VectorArrow + ) { + let drag_angle = (y1 - y0).atan2(x1 - x0); + let drag_angle = if self.modifiers.shift() { + crate::vector_edit::snap_to_15deg(drag_angle) + } else { + drag_angle + }; + shape.set_angle(drag_angle); + } else if matches!(shape, hcie_engine_api::VectorShape::SvgShape { .. }) { shape.set_angle(svg_angle); } engine.add_vector_shape(shape); self.documents[self.active_doc].modified = true; + // Reset angle so every new drawing starts at 0°. + self.settings.tool_settings.vector_angle = 0.0; } self.refresh_composite_if_needed(); } @@ -6380,6 +6395,7 @@ impl HcieIcedApp { (x, y), self.modifiers.shift(), self.modifiers.alt(), + self.modifiers.shift(), ); let (x1, y1, x2, y2) = shape.normalized_bounds(); let angle = shape.angle(); diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs index a29ebb2..935ca01 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -220,6 +220,13 @@ pub fn panel_body<'a>( let layer_height = layer_info.map(|l| l.height).unwrap_or(0); let vector_shapes = doc.engine.active_vector_shapes().unwrap_or_default(); let ts2 = app.settings.tool_settings.clone(); + // Read the selected shape's angle directly from the engine so + // the slider updates instantly when the selection changes. + let effective_vector_angle = doc + .selected_vector_shape + .and_then(|idx| vector_shapes.get(idx)) + .map(|s| s.angle().to_degrees()) + .unwrap_or(ts2.vector_angle); crate::panels::properties::view( &app.tool_state.active_tool, app.tool_state.brush_size, @@ -243,7 +250,7 @@ pub fn panel_body<'a>( ts2.vector_radius, ts2.vector_points, ts2.vector_sides, - ts2.vector_angle, + effective_vector_angle, active_id, app.fg_color, app.bg_color, 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 index c6b0119..d3e1c73 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs @@ -142,12 +142,16 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) { if let Some(shape) = shapes.get(shape_idx) { let ts = &mut app.settings.tool_settings; - // Common properties + // Common properties — note: angle is intentionally NOT synced here + // so that the "default angle for new shapes" stays independent from + // the selected shape's rotation. The angle slider in the Properties + // panel writes directly to `tool_settings.vector_angle` via + // VectorAngleChanged and is pushed to the shape via + // sync_shape_from_tool. 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(); - ts.vector_angle = shape.angle().to_degrees(); // Shape-specific properties match shape { @@ -178,12 +182,10 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) { _ => {} } - // Colors - app.fg_color = shape.color(); - if let Some(fc) = shape.fill_color() { - app.bg_color = fc; - } - app.colors_dirty = true; + // Colors are intentionally NOT synced from the shape into the + // active palette. The geometry / properties panels read the + // shape's colors directly from the engine for display, and the + // user picks new colors via the color palette or panel pickers. } } } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/vector_edit.rs b/hcie-iced-app/crates/hcie-iced-gui/src/vector_edit.rs index 56fed30..271b8c4 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/vector_edit.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/vector_edit.rs @@ -114,15 +114,18 @@ pub fn hit_test_handle( /// Derives a safe shape for the current pointer position in a drag. /// -/// Arguments: `session` is the immutable drag basis, `pointer` is canvas-space, `lock_aspect` -/// corresponds to Shift, and `from_center` corresponds to Alt. Returns: A finite shape with -/// dimensions at least `MIN_VECTOR_SIZE`; invalid pointer input returns the original shape. +/// Arguments: `session` is the immutable drag basis, `pointer` is canvas-space, +/// `lock_aspect` corresponds to Shift (aspect-lock on resize), `from_center` +/// corresponds to Alt, and `snap_angle` corresponds to Shift on rotation. +/// Returns: A finite shape with dimensions at least `MIN_VECTOR_SIZE`; invalid +/// pointer input returns the original shape. /// Side Effects: None. pub fn transform_shape( session: &VectorDragSession, pointer: (f32, f32), lock_aspect: bool, from_center: bool, + snap_angle: bool, ) -> VectorShape { use VectorEditHandle as H; @@ -152,8 +155,13 @@ pub fn transform_shape( (session.pointer_start.1 - center.1).atan2(session.pointer_start.0 - center.0); let current_angle = (pointer.1 - center.1).atan2(pointer.0 - center.0); let angle = session.original.angle() + normalize_angle(current_angle - start_angle); + let angle = if snap_angle { + snap_to_15deg(angle) + } else { + normalize_angle(angle) + }; if angle.is_finite() { - result.set_angle(normalize_angle(angle)); + result.set_angle(angle); } return result; } @@ -302,6 +310,16 @@ pub fn normalize_angle(angle: f32) -> f32 { (angle + std::f32::consts::PI).rem_euclid(two_pi) - std::f32::consts::PI } +/// Snaps an angle (radians) to the nearest 15° increment. +/// +/// Used for Shift-constrained drawing and rotation. 15° = PI/12. +/// Returns the nearest snapped angle within [-PI, PI]. +pub fn snap_to_15deg(angle: f32) -> f32 { + let step = std::f32::consts::PI / 12.0; // 15° + let snapped = (angle / step).round() * step; + normalize_angle(snapped) +} + /// Inverse-rotates a point around a center. Arguments and return are canvas-space coordinates. /// Side Effects: None. fn inverse_rotate(point: (f32, f32), center: (f32, f32), angle: f32) -> (f32, f32) { @@ -467,10 +485,10 @@ mod tests { pointer_start: (5.0, 5.0), original: rect(), }; - let moved = transform_shape(&session, (15.0, 25.0), false, false); + let moved = transform_shape(&session, (15.0, 25.0), false, false, false); assert_eq!(moved.normalized_bounds(), (20.0, 40.0, 120.0, 90.0)); assert_eq!( - transform_shape(&session, (f32::NAN, 0.0), false, false), + transform_shape(&session, (f32::NAN, 0.0), false, false, false), rect() ); } diff --git a/notlar.txt b/notlar.txt index 4675a10..411655d 100755 --- a/notlar.txt +++ b/notlar.txt @@ -1,6 +1,7 @@ cargo run -p hcie-wx-app --bin hcie-wx-app cargo run --bin hcie-gui cargo run --example gui +cargo run -p ##TAURI npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın: