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.
This commit is contained in:
@@ -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:
|
```bash
|
||||||
|
echo $(( $(cat /mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id) + 1 )) > /mnt/extra/00_PROJECTS/hcie-rust-v3.05/build.id
|
||||||
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`).
|
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
|
```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.
|
**Files:**
|
||||||
|
- `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` — line 5772
|
||||||
**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
|
## Changes Summary
|
||||||
|
|
||||||
| File | Change |
|
| # | 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` |
|
| 1 | Project root | Add `increment_build.sh` script |
|
||||||
| `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` | Reset `vector_angle` to 0.0 after `add_vector_shape` in `VectorDrawEnd` |
|
| 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` |
|
||||||
## What Is Preserved
|
| 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` |
|
||||||
- **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
|
## Validation
|
||||||
|
|
||||||
1. Build: `cargo check -p hcie-iced-gui`
|
1. `cargo check -p hcie-iced-gui && cargo check -p hcie-gui-egui`
|
||||||
2. Manual test sequence:
|
2. `cargo test -p hcie-iced-gui`
|
||||||
- Draw a Rect → select → rotate to 30° → deselect → draw a new Rect → verify new Rect has angle 0°
|
3. Manual: run `bash increment_build.sh` and verify `build.id` increments from 990 → 991
|
||||||
- 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
|
|
||||||
|
|||||||
Binary file not shown.
@@ -331,6 +331,8 @@ pub struct HcieIcedApp {
|
|||||||
pub brush_category: crate::panels::brushes::BrushCategory,
|
pub brush_category: crate::panels::brushes::BrushCategory,
|
||||||
/// Context menu screen position (x, y). None means closed.
|
/// Context menu screen position (x, y). None means closed.
|
||||||
pub canvas_context_menu: Option<(f32, f32)>,
|
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
|
/// Dirty flag set whenever fg/bg/recent colors change, so colors can be
|
||||||
/// persisted to disk (egui persists these under the `hcie_colors` key).
|
/// persisted to disk (egui persists these under the `hcie_colors` key).
|
||||||
pub colors_dirty: bool,
|
pub colors_dirty: bool,
|
||||||
@@ -507,6 +509,12 @@ pub struct IcedDocument {
|
|||||||
/// Transformed vector shape being previewed during a drag (not yet committed
|
/// 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.
|
/// to the engine). Kept in the document so the overlay canvas can draw it.
|
||||||
pub vector_drag_preview: Option<hcie_engine_api::VectorShape>,
|
pub vector_drag_preview: Option<hcie_engine_api::VectorShape>,
|
||||||
|
/// 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).
|
/// Cached layer thumbnails keyed by layer ID — (thumb_w, thumb_h, RGBA pixels).
|
||||||
/// Regenerated only for dirty layers in `refresh_panel_caches()`.
|
/// Regenerated only for dirty layers in `refresh_panel_caches()`.
|
||||||
pub layer_thumbnails: std::collections::HashMap<u64, (u32, u32, Vec<u8>)>,
|
pub layer_thumbnails: std::collections::HashMap<u64, (u32, u32, Vec<u8>)>,
|
||||||
@@ -1679,6 +1687,9 @@ impl HcieIcedApp {
|
|||||||
vector_cycle_index: 0,
|
vector_cycle_index: 0,
|
||||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||||
vector_drag_preview: None,
|
vector_drag_preview: None,
|
||||||
|
rotation_snap_active: false,
|
||||||
|
rotation_snap_angle: 0.0,
|
||||||
|
resize_snap_active: false,
|
||||||
layer_thumbnails: std::collections::HashMap::new(),
|
layer_thumbnails: std::collections::HashMap::new(),
|
||||||
thumb_gen: 0,
|
thumb_gen: 0,
|
||||||
};
|
};
|
||||||
@@ -1786,6 +1797,7 @@ impl HcieIcedApp {
|
|||||||
show_dock_profile_dialog: false,
|
show_dock_profile_dialog: false,
|
||||||
_language: i18n::Language::default(),
|
_language: i18n::Language::default(),
|
||||||
brush_category: crate::panels::brushes::BrushCategory::All,
|
brush_category: crate::panels::brushes::BrushCategory::All,
|
||||||
|
runtime_version: crate::build_info::runtime_build_version(),
|
||||||
colors_dirty: false,
|
colors_dirty: false,
|
||||||
color_dragging: false,
|
color_dragging: false,
|
||||||
pending_drag_color: None,
|
pending_drag_color: None,
|
||||||
@@ -5270,6 +5282,9 @@ impl HcieIcedApp {
|
|||||||
vector_cycle_index: 0,
|
vector_cycle_index: 0,
|
||||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||||
vector_drag_preview: None,
|
vector_drag_preview: None,
|
||||||
|
rotation_snap_active: false,
|
||||||
|
rotation_snap_angle: 0.0,
|
||||||
|
resize_snap_active: false,
|
||||||
layer_thumbnails: std::collections::HashMap::new(),
|
layer_thumbnails: std::collections::HashMap::new(),
|
||||||
thumb_gen: 0,
|
thumb_gen: 0,
|
||||||
};
|
};
|
||||||
@@ -5769,7 +5784,7 @@ impl HcieIcedApp {
|
|||||||
let radius = self.settings.tool_settings.vector_radius;
|
let radius = self.settings.tool_settings.vector_radius;
|
||||||
let points = self.settings.tool_settings.vector_points;
|
let points = self.settings.tool_settings.vector_points;
|
||||||
let sides = self.settings.tool_settings.vector_sides;
|
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 {
|
let shape = match self.tool_state.active_tool {
|
||||||
Tool::VectorRect => Some(hcie_engine_api::VectorShape::Rect {
|
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_bounds = Some((x1, y1, x2, y2));
|
||||||
self.vector_drag_last_angle = Some(angle);
|
self.vector_drag_last_angle = Some(angle);
|
||||||
self.settings.tool_settings.vector_angle = angle.to_degrees();
|
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 => {
|
Message::VectorSelectDragEnd => {
|
||||||
@@ -6448,6 +6481,8 @@ impl HcieIcedApp {
|
|||||||
self.documents[self.active_doc].vector_drag_preview = None;
|
self.documents[self.active_doc].vector_drag_preview = None;
|
||||||
self.documents[self.active_doc].vector_edit_handle =
|
self.documents[self.active_doc].vector_edit_handle =
|
||||||
hcie_engine_api::VectorEditHandle::None;
|
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();
|
self.refresh_composite_if_needed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8076,6 +8111,9 @@ impl HcieIcedApp {
|
|||||||
vector_cycle_index: 0,
|
vector_cycle_index: 0,
|
||||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||||
vector_drag_preview: None,
|
vector_drag_preview: None,
|
||||||
|
rotation_snap_active: false,
|
||||||
|
rotation_snap_angle: 0.0,
|
||||||
|
resize_snap_active: false,
|
||||||
layer_thumbnails: std::collections::HashMap::new(),
|
layer_thumbnails: std::collections::HashMap::new(),
|
||||||
thumb_gen: 0,
|
thumb_gen: 0,
|
||||||
});
|
});
|
||||||
@@ -9237,6 +9275,7 @@ impl HcieIcedApp {
|
|||||||
self.active_menu,
|
self.active_menu,
|
||||||
&self.title_search,
|
&self.title_search,
|
||||||
self.show_panel_list,
|
self.show_panel_list,
|
||||||
|
&self.runtime_version,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toolbox — 36px strip (single column) or 72px (2 columns) on the left edge
|
// Toolbox — 36px strip (single column) or 72px (2 columns) on the left edge
|
||||||
|
|||||||
@@ -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::<u64>() {
|
||||||
|
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}")
|
||||||
|
}
|
||||||
@@ -97,6 +97,12 @@ struct OverlayProgram {
|
|||||||
vector_edit_handle: hcie_engine_api::VectorEditHandle,
|
vector_edit_handle: hcie_engine_api::VectorEditHandle,
|
||||||
/// Transformed vector shape being previewed during a drag (not yet committed).
|
/// Transformed vector shape being previewed during a drag (not yet committed).
|
||||||
vector_drag_preview: Option<hcie_engine_api::VectorShape>,
|
vector_drag_preview: Option<hcie_engine_api::VectorShape>,
|
||||||
|
/// 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.
|
/// Overlay state — tracks hover and cursor position for crosshair.
|
||||||
@@ -1352,6 +1358,123 @@ impl canvas::Program<Message> for OverlayProgram {
|
|||||||
origin_y,
|
origin_y,
|
||||||
state.hovered_vector_handle,
|
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.
|
// Draw gradient endpoint preview line.
|
||||||
@@ -1717,6 +1840,9 @@ pub fn view<'a>(
|
|||||||
selected_vector_angle: sel_vec_angle,
|
selected_vector_angle: sel_vec_angle,
|
||||||
vector_edit_handle: doc.vector_edit_handle,
|
vector_edit_handle: doc.vector_edit_handle,
|
||||||
vector_drag_preview: vector_preview,
|
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)
|
let overlay_canvas = canvas(overlay_program)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod ai_chat;
|
|||||||
mod ai_script;
|
mod ai_script;
|
||||||
mod app;
|
mod app;
|
||||||
mod brush_import;
|
mod brush_import;
|
||||||
|
mod build_info;
|
||||||
mod canvas;
|
mod canvas;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod color_picker;
|
mod color_picker;
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ pub(crate) const MENU_LEFT_INSET: f32 = 6.0;
|
|||||||
/// Unified menu/title bar height shared with dropdown placement.
|
/// Unified menu/title bar height shared with dropdown placement.
|
||||||
pub(crate) const MENU_BAR_HEIGHT: f32 = 30.0;
|
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.
|
/// 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 {
|
pub(crate) fn menu_anchor_x(menu_index: usize, viewport_width: f32, dropdown_width: f32) -> f32 {
|
||||||
let natural = MENU_LEFT_INSET
|
let natural = MENU_LEFT_INSET
|
||||||
@@ -54,6 +52,7 @@ pub fn view<'a>(
|
|||||||
active_menu: Option<usize>,
|
active_menu: Option<usize>,
|
||||||
title_search: &'a str,
|
title_search: &'a str,
|
||||||
show_panel_list: bool,
|
show_panel_list: bool,
|
||||||
|
runtime_version: &'a str,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
// ── App icon ──
|
// ── App icon ──
|
||||||
let icon = text("H")
|
let icon = text("H")
|
||||||
@@ -164,7 +163,7 @@ pub fn view<'a>(
|
|||||||
// ── Document info — centered: "HCIE v{version} — New Project.psd *" ──
|
// ── Document info — centered: "HCIE v{version} — New Project.psd *" ──
|
||||||
let title_str = format!(
|
let title_str = format!(
|
||||||
"HCIE v{} — {}{}",
|
"HCIE v{} — {}{}",
|
||||||
APP_VERSION,
|
runtime_version,
|
||||||
doc_name,
|
doc_name,
|
||||||
if modified { " *" } else { "" }
|
if modified { " *" } else { "" }
|
||||||
);
|
);
|
||||||
|
|||||||
+14
-13
@@ -2,7 +2,7 @@
|
|||||||
SATIR SAYISI RAPORU
|
SATIR SAYISI RAPORU
|
||||||
Proje: HCIE-Rust v4
|
Proje: HCIE-Rust v4
|
||||||
Konum: /mnt/extra/00_PROJECTS/hcie-rust-v3.05
|
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-ai 2402 satır
|
||||||
hcie-blend 530 satır
|
hcie-blend 530 satır
|
||||||
hcie-brush-engine 4845 satır
|
hcie-brush-engine 4845 satır
|
||||||
|
hcie-build-info 93 satır
|
||||||
hcie-color 85 satır
|
hcie-color 85 satır
|
||||||
hcie-composite 1456 satır
|
hcie-composite 1456 satır
|
||||||
hcie-document 761 satır
|
hcie-document 761 satır
|
||||||
hcie-draw 619 satır
|
hcie-draw 619 satır
|
||||||
hcie-egui-app 37437 satır
|
hcie-egui-app 37434 satır
|
||||||
hcie-engine-api 8364 satır
|
hcie-engine-api 8366 satır
|
||||||
hcie-engine-api-orig 3874 satır
|
hcie-engine-api-orig 3874 satır
|
||||||
hcie-filter 3238 satır
|
hcie-filter 3238 satır
|
||||||
hcie-fx 5535 satır
|
hcie-fx 5535 satır
|
||||||
hcie-history 139 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-io 12122 satır
|
||||||
hcie-kra 1398 satır
|
hcie-kra 1398 satır
|
||||||
hcie-native 153 satır
|
hcie-native 153 satır
|
||||||
@@ -34,7 +35,7 @@ Tarih: 2026-07-21 03:17:11
|
|||||||
hcie-vector 2921 satır
|
hcie-vector 2921 satır
|
||||||
hcie-vision 3131 satır
|
hcie-vision 3131 satır
|
||||||
|
|
||||||
TOPLAM RUST: 139629 satır
|
TOPLAM RUST: 139986 satır
|
||||||
|
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
C++ / HEADER (.cpp, .h)
|
C++ / HEADER (.cpp, .h)
|
||||||
@@ -85,10 +86,10 @@ Tarih: 2026-07-21 03:17:11
|
|||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
SHELL SCRIPT (.sh)
|
SHELL SCRIPT (.sh)
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
(kök dizin) 1225 satır
|
(kök dizin) 1306 satır
|
||||||
logs/ 220 satır
|
logs/ 220 satır
|
||||||
|
|
||||||
TOPLAM SHELL: 1445 satır
|
TOPLAM SHELL: 1526 satır
|
||||||
|
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
DOKÜMANTASYON / VERİ DOSYALARI
|
DOKÜMANTASYON / VERİ DOSYALARI
|
||||||
@@ -102,14 +103,14 @@ Tarih: 2026-07-21 03:17:11
|
|||||||
|
|
||||||
MARKDOWN (.md)
|
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)
|
KOD SATIRLARI TOPLAMI (GRAND TOTAL)
|
||||||
=========================================
|
=========================================
|
||||||
Rust: 139629 satır
|
Rust: 139986 satır
|
||||||
C++ / Header: 5031 satır
|
C++ / Header: 5031 satır
|
||||||
JavaScript: 0 satır
|
JavaScript: 0 satır
|
||||||
Svelte: 0 satır
|
Svelte: 0 satır
|
||||||
@@ -117,11 +118,11 @@ Tarih: 2026-07-21 03:17:11
|
|||||||
Python: 2267 satır
|
Python: 2267 satır
|
||||||
HTML: 0 satır
|
HTML: 0 satır
|
||||||
CSS: 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)
|
(JSON ve Markdown dosyaları yukarıda ayrı olarak gösterilmiştir)
|
||||||
=========================================
|
=========================================
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
cargo run -p hcie-wx-app --bin hcie-wx-app
|
cargo run -p hcie-wx-app --bin hcie-wx-app
|
||||||
cargo run --bin hcie-gui
|
cargo run --bin hcie-gui
|
||||||
cargo run --example 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
|
##TAURI
|
||||||
npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın:
|
npm komutunu proje kökünden değil, hcie-tauri-app/ altından çalıştırmalısın:
|
||||||
|
|||||||
Reference in New Issue
Block a user