From bc89eefa889228a4364eb8ba6a75eeec6d44bfa8 Mon Sep 17 00:00:00 2001 From: Halit Can Date: Tue, 21 Jul 2026 05:41:27 +0300 Subject: [PATCH] feat: add runtime build-ID management to increment and track application build numbers - Implemented a new module for managing build IDs at runtime. - Introduced functions to find the repository root, check for stale locks, and increment the build ID. - Ensured atomic writes to the build ID file with proper locking mechanisms. - The build version string is formatted as "0.1.0+build.{build_id}" for display purposes. --- ...84597466971-vector-state-management-fix.md | 203 +++++++++--------- drawing_tools_guide.pdf | Bin 0 -> 17066 bytes hcie-iced-app/crates/hcie-iced-gui/src/app.rs | 41 +++- .../crates/hcie-iced-gui/src/build_info.rs | 126 +++++++++++ .../crates/hcie-iced-gui/src/canvas/mod.rs | 126 +++++++++++ .../crates/hcie-iced-gui/src/main.rs | 1 + .../hcie-iced-gui/src/panels/title_bar.rs | 5 +- line_count_report.txt | 27 +-- notlar.txt | 3 +- 9 files changed, 417 insertions(+), 115 deletions(-) create mode 100644 drawing_tools_guide.pdf create mode 100644 hcie-iced-app/crates/hcie-iced-gui/src/build_info.rs 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 0000000000000000000000000000000000000000..cc915828bb11b4c581fa318ce9f17456c9dede2c GIT binary patch literal 17066 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 literal 0 HcmV?d00001 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: