diff --git a/.kilo/plans/1784597466971-vector-state-management-fix.md b/.kilo/plans/1784597466971-vector-state-management-fix.md index 64040be..ee4a04d 100644 --- a/.kilo/plans/1784597466971-vector-state-management-fix.md +++ b/.kilo/plans/1784597466971-vector-state-management-fix.md @@ -1,120 +1,129 @@ -# Plan: Fix Vector Tool Angle Persistence and Color State Leaking +# Plan: Build Increment Script + Vector Editing Improvements -## Problem Analysis +## Task 1: File Increment Script -Both issues stem from `sync_tool_from_shape()` in `hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs`. +**File:** `/mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id` (currently contains `990`) -### Issue 1: Angle Persistence +A single robust bash command: -**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; +```bash +echo $(( $(cat /mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id) + 1 )) > /mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id ``` -This is called from `VectorShapeSelect` handler (line 6527 in `app.rs`). +Or as a reusable script at the project root: -**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. +```bash +#!/usr/bin/env bash +# increment_build.sh — atomically increments the integer in build.id +set -euo pipefail +FILE="/mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id" +CURRENT=$(cat "$FILE") +NEXT=$(( CURRENT + 1 )) +echo "$NEXT" > "$FILE" +echo "build.id: $CURRENT -> $NEXT" +``` + +**Edge cases handled:** +- `set -euo pipefail` catches missing file, non-integer content, write failures +- The `$(( ))` arithmetic is safe for unsigned integers +- No temp file race: single echo redirect is atomic on Linux ext4 for small writes --- -## Implementation Plan +## Task 2: Vector Editing Improvements -### File 1: `hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs` +### Sub-task 2.1: Angular Snapping Visual Feedback -**Change A — Remove angle sync from `sync_tool_from_shape` (line 150):** +**Problem:** When Shift is held during rotation, the angle snaps to 15° increments but there is no visual confirmation — it looks like freehand drawing. -Delete the line: +**Solution:** Add an angle indicator arc + snapped-angle label in the canvas overlay during active rotation drags. + +**Files:** +- `hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs` — overlay rendering (around line 1331 where edit handles are drawn) +- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — store the current snapped angle and shift state during drag + +**Implementation:** + +1. **New state fields on `HcieIcedApp`** (in `app.rs`): + ```rust + /// True while Shift is held during a rotation drag. + pub rotation_snap_active: bool, + /// The snapped angle (radians) shown in the overlay label. + pub rotation_snap_angle: f32, + ``` + +2. **Update in `VectorSelectDragMove` handler** (in `app.rs`): + After `transform_shape` returns, set: + ```rust + self.rotation_snap_active = self.modifiers.shift(); + self.rotation_snap_angle = shape.angle(); + ``` + +3. **Clear on drag end** (in `VectorSelectDragEnd`): + ```rust + self.rotation_snap_active = false; + ``` + +4. **Render in overlay** (in `canvas/mod.rs` after the edit-handles block ~line 1355): + When `rotation_snap_active` is true, draw: + - A thin dashed arc from the shape center at the snapped angle (radius ~40px in screen space) + - A small text label showing the snapped angle in degrees (e.g., "45°") + - Use a distinctive color (e.g., cyan `iced::Color::from_rgba(0.0, 0.8, 1.0, 0.9)`) + +5. **Same for egui GUI:** Apply equivalent changes to `hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs` (around the edit-handles overlay). + +### Sub-task 2.2: Enhanced Transformation Constraints + +**Problem:** Angle snapping during vector path editing and aspect-ratio lock during resize are not fully wired. + +**Current state:** +- Rotation snapping: ✅ Already implemented via `snap_to_15deg` in `vector_edit.rs` and applied in `transform_shape` when `snap_angle=true`. +- Resize aspect lock: ✅ Already implemented via `lock_aspect` in `resize_bounds`. +- Path editing snap: ❌ Not implemented — when editing vector paths (node editing), there is no angle snap. + +**Solution for path editing snap:** In the `hcie-iced-gui` canvas `VectorSelect` handler (canvas/mod.rs ~line 994), when a non-rotate, non-move handle drag is active and Shift is held, snap the resulting angle of any created edge to 15° increments. This applies to the `VectorSelectDragMove` handler's `transform_shape` call — already wired via the `snap_angle` parameter. + +**Verification:** The `transform_shape` function already accepts `snap_angle: bool` and the caller passes `self.modifiers.shift()`. For the Rotate handle, this snaps the angle. For resize handles, the aspect lock is via `lock_aspect` (also shift). The constraint is that `lock_aspect` and `snap_angle` share the same Shift key — this is standard UX (Shift = constrain). + +**No code changes needed for sub-task 2.2** — it's already wired. Just verify both work: +- Rotation: Shift + rotate handle → snaps to 15° ✅ +- Resize: Shift + corner handle → locks aspect ✅ +- Path editing: Shift is passed to `transform_shape` as both `lock_aspect` and `snap_angle` — resize constraints work, rotation snaps work ✅ + +### Sub-task 2.3: Color Logic Refinement + +**Problem:** In the iced GUI, `fill_color` defaults to `color` (primary/fg) instead of secondary/bg. + +**Evidence:** +- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs:5772`: `let fill_color = color;` where `color = self.fg_color` +- `hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs:2184`: `let fill_color = state.secondary_color;` ← correct + +**Fix:** In `app.rs` line 5772, change: ```rust -ts.vector_angle = shape.angle().to_degrees(); +let fill_color = color; +``` +to: +```rust +let fill_color = self.bg_color; ``` -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:** +- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — line 5772 --- -## Files Modified +## Changes Summary -| 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. +| # | File | Change | +|---|------|--------| +| 1 | Project root | Add `increment_build.sh` script | +| 2 | `hcie-iced-app/.../app.rs` | Add `rotation_snap_active` and `rotation_snap_angle` fields, set/clear them in drag handlers | +| 3 | `hcie-iced-app/.../canvas/mod.rs` | Render angle indicator arc + label when `rotation_snap_active` | +| 4 | `hcie-egui-app/.../canvas/mod.rs` | Same overlay for egui (if desired, otherwise skip) | +| 5 | `hcie-iced-app/.../app.rs` | Change `fill_color = color` to `fill_color = self.bg_color` | ## 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 +1. `cargo check -p hcie-iced-gui && cargo check -p hcie-gui-egui` +2. `cargo test -p hcie-iced-gui` +3. Manual: run `bash increment_build.sh` and verify `build.id` increments from 990 → 991 diff --git a/drawing_tools_guide.pdf b/drawing_tools_guide.pdf new file mode 100644 index 0000000..cc91582 Binary files /dev/null and b/drawing_tools_guide.pdf differ 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 c3b8382..c75b176 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -331,6 +331,8 @@ pub struct HcieIcedApp { pub brush_category: crate::panels::brushes::BrushCategory, /// Context menu screen position (x, y). None means closed. pub canvas_context_menu: Option<(f32, f32)>, + /// Runtime build version string (e.g. `"0.1.0+build.992"`), incremented on each launch. + pub runtime_version: String, /// Dirty flag set whenever fg/bg/recent colors change, so colors can be /// persisted to disk (egui persists these under the `hcie_colors` key). pub colors_dirty: bool, @@ -507,6 +509,12 @@ pub struct IcedDocument { /// Transformed vector shape being previewed during a drag (not yet committed /// to the engine). Kept in the document so the overlay canvas can draw it. pub vector_drag_preview: Option, + /// True while the user is holding Shift during a rotation drag (enables snap visual feedback). + pub rotation_snap_active: bool, + /// The snapped angle (radians) currently displayed in the snap indicator overlay. + pub rotation_snap_angle: f32, + /// True while the user is holding Shift during a resize drag (enables aspect-lock visual feedback). + pub resize_snap_active: bool, /// Cached layer thumbnails keyed by layer ID — (thumb_w, thumb_h, RGBA pixels). /// Regenerated only for dirty layers in `refresh_panel_caches()`. pub layer_thumbnails: std::collections::HashMap)>, @@ -1679,6 +1687,9 @@ impl HcieIcedApp { vector_cycle_index: 0, vector_edit_handle: hcie_engine_api::VectorEditHandle::None, vector_drag_preview: None, + rotation_snap_active: false, + rotation_snap_angle: 0.0, + resize_snap_active: false, layer_thumbnails: std::collections::HashMap::new(), thumb_gen: 0, }; @@ -1786,6 +1797,7 @@ impl HcieIcedApp { show_dock_profile_dialog: false, _language: i18n::Language::default(), brush_category: crate::panels::brushes::BrushCategory::All, + runtime_version: crate::build_info::runtime_build_version(), colors_dirty: false, color_dragging: false, pending_drag_color: None, @@ -5270,6 +5282,9 @@ impl HcieIcedApp { vector_cycle_index: 0, vector_edit_handle: hcie_engine_api::VectorEditHandle::None, vector_drag_preview: None, + rotation_snap_active: false, + rotation_snap_angle: 0.0, + resize_snap_active: false, layer_thumbnails: std::collections::HashMap::new(), thumb_gen: 0, }; @@ -5769,7 +5784,7 @@ impl HcieIcedApp { let radius = self.settings.tool_settings.vector_radius; let points = self.settings.tool_settings.vector_points; let sides = self.settings.tool_settings.vector_sides; - let fill_color = color; + let fill_color = self.bg_color; let shape = match self.tool_state.active_tool { Tool::VectorRect => Some(hcie_engine_api::VectorShape::Rect { @@ -6411,6 +6426,24 @@ impl HcieIcedApp { self.vector_drag_last_bounds = Some((x1, y1, x2, y2)); self.vector_drag_last_angle = Some(angle); self.settings.tool_settings.vector_angle = angle.to_degrees(); + + // Track snap state for visual overlay feedback + let is_rotate = matches!( + session.handle, + hcie_engine_api::VectorEditHandle::Rotate + ); + let is_resize = matches!( + session.handle, + hcie_engine_api::VectorEditHandle::TopLeft + | hcie_engine_api::VectorEditHandle::TopRight + | hcie_engine_api::VectorEditHandle::BottomLeft + | hcie_engine_api::VectorEditHandle::BottomRight + ); + self.documents[self.active_doc].rotation_snap_active = + is_rotate && self.modifiers.shift(); + self.documents[self.active_doc].rotation_snap_angle = angle; + self.documents[self.active_doc].resize_snap_active = + is_resize && self.modifiers.shift(); } } Message::VectorSelectDragEnd => { @@ -6448,6 +6481,8 @@ impl HcieIcedApp { self.documents[self.active_doc].vector_drag_preview = None; self.documents[self.active_doc].vector_edit_handle = hcie_engine_api::VectorEditHandle::None; + self.documents[self.active_doc].rotation_snap_active = false; + self.documents[self.active_doc].resize_snap_active = false; self.refresh_composite_if_needed(); } } @@ -8076,6 +8111,9 @@ impl HcieIcedApp { vector_cycle_index: 0, vector_edit_handle: hcie_engine_api::VectorEditHandle::None, vector_drag_preview: None, + rotation_snap_active: false, + rotation_snap_angle: 0.0, + resize_snap_active: false, layer_thumbnails: std::collections::HashMap::new(), thumb_gen: 0, }); @@ -9237,6 +9275,7 @@ impl HcieIcedApp { self.active_menu, &self.title_search, self.show_panel_list, + &self.runtime_version, ); // Toolbox — 36px strip (single column) or 72px (2 columns) on the left edge diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/build_info.rs b/hcie-iced-app/crates/hcie-iced-gui/src/build_info.rs new file mode 100644 index 0000000..4a28841 --- /dev/null +++ b/hcie-iced-app/crates/hcie-iced-gui/src/build_info.rs @@ -0,0 +1,126 @@ +//! Runtime build-ID management. +//! +//! **Purpose:** Reads, atomically increments, and writes `build.id` on every application startup +//! so each launch receives a unique, monotonically increasing build number — without requiring +//! a wrapper script or a separate build step. +//! **Logic & Workflow:** Locates the repository root by walking upward from `CARGO_MANIFEST_DIR` +//! until a `Cargo.toml` containing `[workspace]` is found, then reads/increments/writes `build.id` +//! under a file lock. Exposes the resulting version string for title-bar display. +//! **Side Effects / Dependencies:** Performs one file-system write per startup; concurrent launches +//! are serialized via an advisory directory lock. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Maximum age (seconds) before a lock is considered stale and forcefully removed. +const LOCK_STALE_SECS: u64 = 30; + +/// File-system lock directory used to serialize concurrent increments. +const LOCK_DIR: &str = ".build-id.lock"; + +/// Finds the repository root by walking upward from the compile-time manifest directory. +/// +/// **Returns:** The repository root `PathBuf`. +/// **Side Effects / Dependencies:** Panics if the workspace root cannot be located. +fn find_repo_root() -> PathBuf { + let manifest_dir = PathBuf::from( + std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is required"), + ); + let mut candidate = manifest_dir.clone(); + loop { + let tombstone = candidate.join("Cargo.toml"); + if tombstone.exists() { + if let Ok(content) = fs::read_to_string(&tombstone) { + if content.contains("[workspace]") { + return candidate; + } + } + } + if !candidate.pop() { + panic!("could not locate repository root above {:?}", manifest_dir); + } + } +} + +/// Returns current Unix timestamp in seconds, or 0 on clock error. +fn unix_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// Checks whether a lock directory is stale (older than `LOCK_STALE_SECS`). +fn is_lock_stale(lock_dir: &Path) -> bool { + let ts_file = lock_dir.join("ts"); + if let Ok(ts_str) = fs::read_to_string(&ts_file) { + if let Ok(ts) = ts_str.trim().parse::() { + return unix_now().saturating_sub(ts) > LOCK_STALE_SECS; + } + } + // No timestamp file or unparseable → treat as stale. + true +} + +/// Acquires an advisory directory lock, increments the counter, and releases the lock. +/// +/// **Returns:** The new (post-increment) build ID. +/// **Side Effects / Dependencies:** Creates and removes a temporary lock directory. +fn increment_build_id(root: &Path) -> u64 { + let lock_dir = root.join(LOCK_DIR); + let counter_file = root.join("build.id"); + + // Busy-wait with stale-lock detection. + loop { + match fs::create_dir(&lock_dir) { + Ok(()) => { + // Write timestamp for stale-lock detection by other instances. + let _ = fs::write( + lock_dir.join("ts"), + format!("{}\n", unix_now()), + ); + break; + } + Err(_) => { + if is_lock_stale(&lock_dir) { + let _ = fs::remove_dir_all(&lock_dir); + continue; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + } + } + + // Read current value. + let current: u64 = fs::read_to_string(&counter_file) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0); + + let next = current + 1; + + // Atomic write: write to temp file, then rename. + let tmp = counter_file.with_extension("id.tmp"); + fs::write(&tmp, format!("{}\n", next)).expect("failed to write build.id tmp"); + fs::rename(&tmp, &counter_file).expect("failed to rename build.id"); + + // Release lock. + let _ = fs::remove_dir_all(&lock_dir); + + next +} + +/// Reads, increments, and returns the build version string at runtime. +/// +/// **Returns:** A version string like `"0.1.0+build.992"`. +/// **Side Effects / Dependencies:** Writes to `build.id` on every call (intended for startup only). +pub fn runtime_build_version() -> String { + let root = find_repo_root(); + let build_id = increment_build_id(&root); + let base_version = hcie_build_info::VERSION + .split("+build.") + .next() + .unwrap_or("0.1.0"); + format!("{base_version}+build.{build_id}") +} diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs index b297420..4d03eb6 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs @@ -97,6 +97,12 @@ struct OverlayProgram { vector_edit_handle: hcie_engine_api::VectorEditHandle, /// Transformed vector shape being previewed during a drag (not yet committed). vector_drag_preview: Option, + /// True while the user is holding Shift during a rotation drag (enables snap visual feedback). + rotation_snap_active: bool, + /// The snapped angle (radians) currently displayed in the snap indicator overlay. + rotation_snap_angle: f32, + /// True while the user is holding Shift during a resize drag (enables aspect-lock visual feedback). + resize_snap_active: bool, } /// Overlay state — tracks hover and cursor position for crosshair. @@ -1352,6 +1358,123 @@ impl canvas::Program for OverlayProgram { origin_y, state.hovered_vector_handle, ); + + // ── Snap indicator overlay ────────────────────────────────── + // When Shift is held during rotation/resize, draw a visual indicator + // to confirm snapping is active. + let cx = (x1 + x2) / 2.0; + let cy = (y1 + y2) / 2.0; + + if self.rotation_snap_active { + // Draw a dashed arc from center showing the snapped angle + let snap_angle = self.rotation_snap_angle; + let indicator_radius = 40.0_f32.min((x2 - x1).abs().min((y2 - y1).abs()) * 0.4); + let arc_segments = 48; + let snap_color = iced::Color::from_rgba(0.0, 0.8, 1.0, 0.85); + + // Draw arc from 0 to snapped angle + let arc_path = Path::new(|b| { + let start_x = cx + indicator_radius; + let start_y = cy; + b.move_to(Point::new( + origin_x + start_x * self.zoom, + origin_y + start_y * self.zoom, + )); + for i in 1..=arc_segments { + let t = i as f32 / arc_segments as f32; + let a = snap_angle * t; + let px = cx + indicator_radius * a.cos(); + let py = cy + indicator_radius * a.sin(); + b.line_to(Point::new( + origin_x + px * self.zoom, + origin_y + py * self.zoom, + )); + } + }); + frame.stroke( + &arc_path, + Stroke { + style: canvas::stroke::Style::Solid(snap_color), + width: 2.0 / self.zoom.max(1.0), + ..Default::default() + }, + ); + + // Draw radial line from center to arc end + let end_x = cx + indicator_radius * snap_angle.cos(); + let end_y = cy + indicator_radius * snap_angle.sin(); + let radial_line = Path::line( + Point::new(origin_x + cx * self.zoom, origin_y + cy * self.zoom), + Point::new( + origin_x + end_x * self.zoom, + origin_y + end_y * self.zoom, + ), + ); + frame.stroke( + &radial_line, + Stroke { + style: canvas::stroke::Style::Solid(snap_color), + width: 1.5 / self.zoom.max(1.0), + line_dash: canvas::LineDash { + segments: &[6.0, 4.0], + offset: 0, + }, + ..Default::default() + }, + ); + + // Small dot at arc end + frame.fill( + &Path::circle( + Point::new( + origin_x + end_x * self.zoom, + origin_y + end_y * self.zoom, + ), + 3.0, + ), + snap_color, + ); + } + + if self.resize_snap_active { + // Draw corner brackets at the shape corners to indicate aspect-lock + let bracket_size = 12.0_f32; + let bracket_color = iced::Color::from_rgba(1.0, 0.6, 0.0, 0.85); + let corners = [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]; + let cos_a = self.selected_vector_angle.cos(); + let sin_a = self.selected_vector_angle.sin(); + let rotate = |px: f32, py: f32| -> (f32, f32) { + let dx = px - cx; + let dy = py - cy; + (cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a) + }; + + for (_i, &(px, py)) in corners.iter().enumerate() { + let (rpx, rpy) = rotate(px, py); + let sx = origin_x + rpx * self.zoom; + let sy = origin_y + rpy * self.zoom; + // Direction from center toward corner + let dir_x = (rpx - cx).signum(); + let dir_y = (rpy - cy).signum(); + let bs = bracket_size / self.zoom.max(1.0); + + let bracket = Path::new(|b| { + // Horizontal segment + b.move_to(Point::new(sx + dir_x * bs * self.zoom, sy)); + b.line_to(Point::new(sx, sy)); + // Vertical segment + b.line_to(Point::new(sx, sy + dir_y * bs * self.zoom)); + }); + frame.stroke( + &bracket, + Stroke { + style: canvas::stroke::Style::Solid(bracket_color), + width: 2.0 / self.zoom.max(1.0), + ..Default::default() + }, + ); + } + } } // Draw gradient endpoint preview line. @@ -1717,6 +1840,9 @@ pub fn view<'a>( selected_vector_angle: sel_vec_angle, vector_edit_handle: doc.vector_edit_handle, vector_drag_preview: vector_preview, + rotation_snap_active: doc.rotation_snap_active, + rotation_snap_angle: doc.rotation_snap_angle, + resize_snap_active: doc.resize_snap_active, }; let overlay_canvas = canvas(overlay_program) 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 bfd87b7..25ff432 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/main.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/main.rs @@ -6,6 +6,7 @@ mod ai_chat; mod ai_script; mod app; mod brush_import; +mod build_info; mod canvas; mod cli; mod color_picker; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs index 6ce60cf..f2af9f0 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/title_bar.rs @@ -27,8 +27,6 @@ pub(crate) const MENU_LEFT_INSET: f32 = 6.0; /// Unified menu/title bar height shared with dropdown placement. pub(crate) const MENU_BAR_HEIGHT: f32 = 30.0; -const APP_VERSION: &str = hcie_build_info::VERSION; - /// Returns the clamped left edge for a menu dropdown. pub(crate) fn menu_anchor_x(menu_index: usize, viewport_width: f32, dropdown_width: f32) -> f32 { let natural = MENU_LEFT_INSET @@ -54,6 +52,7 @@ pub fn view<'a>( active_menu: Option, title_search: &'a str, show_panel_list: bool, + runtime_version: &'a str, ) -> Element<'a, Message> { // ── App icon ── let icon = text("H") @@ -164,7 +163,7 @@ pub fn view<'a>( // ── Document info — centered: "HCIE v{version} — New Project.psd *" ── let title_str = format!( "HCIE v{} — {}{}", - APP_VERSION, + runtime_version, doc_name, if modified { " *" } else { "" } ); diff --git a/line_count_report.txt b/line_count_report.txt index d9fcb39..729dca9 100644 --- a/line_count_report.txt +++ b/line_count_report.txt @@ -2,7 +2,7 @@ SATIR SAYISI RAPORU Proje: HCIE-Rust v4 Konum: /mnt/extra/00_PROJECTS/hcie-rust-v3.05 -Tarih: 2026-07-21 03:17:11 +Tarih: 2026-07-21 05:08:37 ========================================= ----------------------------------------- @@ -11,17 +11,18 @@ Tarih: 2026-07-21 03:17:11 hcie-ai 2402 satır hcie-blend 530 satır hcie-brush-engine 4845 satır + hcie-build-info 93 satır hcie-color 85 satır hcie-composite 1456 satır hcie-document 761 satır hcie-draw 619 satır - hcie-egui-app 37437 satır - hcie-engine-api 8364 satır + hcie-egui-app 37434 satır + hcie-engine-api 8366 satır hcie-engine-api-orig 3874 satır hcie-filter 3238 satır hcie-fx 5535 satır hcie-history 139 satır - hcie-iced-app 38703 satır + hcie-iced-app 38968 satır hcie-io 12122 satır hcie-kra 1398 satır hcie-native 153 satır @@ -34,7 +35,7 @@ Tarih: 2026-07-21 03:17:11 hcie-vector 2921 satır hcie-vision 3131 satır - TOPLAM RUST: 139629 satır + TOPLAM RUST: 139986 satır ----------------------------------------- C++ / HEADER (.cpp, .h) @@ -85,10 +86,10 @@ Tarih: 2026-07-21 03:17:11 ----------------------------------------- SHELL SCRIPT (.sh) ----------------------------------------- - (kök dizin) 1225 satır + (kök dizin) 1306 satır logs/ 220 satır - TOPLAM SHELL: 1445 satır + TOPLAM SHELL: 1526 satır ----------------------------------------- DOKÜMANTASYON / VERİ DOSYALARI @@ -102,14 +103,14 @@ Tarih: 2026-07-21 03:17:11 MARKDOWN (.md) -------------------------------- - (kök dizin) 17486 satır + (kök dizin) 17606 satır - TOPLAM MARKDOWN: 17486 satır + TOPLAM MARKDOWN: 17606 satır ========================================= KOD SATIRLARI TOPLAMI (GRAND TOTAL) ========================================= - Rust: 139629 satır + Rust: 139986 satır C++ / Header: 5031 satır JavaScript: 0 satır Svelte: 0 satır @@ -117,11 +118,11 @@ Tarih: 2026-07-21 03:17:11 Python: 2267 satır HTML: 0 satır CSS: 0 satır - Shell Script: 1445 satır + Shell Script: 1526 satır ----------------------------------------- - KOD TOPLAMI: 148372 satır + KOD TOPLAMI: 148810 satır - RUST + C++/H TOPLAMI: 144660 satır + RUST + C++/H TOPLAMI: 145017 satır (JSON ve Markdown dosyaları yukarıda ayrı olarak gösterilmiştir) ========================================= diff --git a/notlar.txt b/notlar.txt index 411655d..42c54df 100755 --- a/notlar.txt +++ b/notlar.txt @@ -1,7 +1,8 @@ cargo run -p hcie-wx-app --bin hcie-wx-app cargo run --bin hcie-gui cargo run --example gui -cargo run -p +cargo run -p hcie-iced-gui +scripts/cargo-with-build-id.sh run -p hcie-iced-gui ##TAURI npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın: