203 lines
8.4 KiB
Markdown
203 lines
8.4 KiB
Markdown
|
|
# HCIE-Rust v3.05 — Session Handoff Document
|
|||
|
|
|
|||
|
|
**Date:** 2026-07-12
|
|||
|
|
**Session ID:** ses_0a89f4c7dffeLzqNKSKFHyyaI1
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Summary
|
|||
|
|
|
|||
|
|
This session fixed critical engine bugs and completed iced GUI features for the HCIE-Rust image editor. The most significant fix was a fundamental architecture bug in the effects pipeline that caused new drawing strokes to disappear and effects to accumulate incorrectly.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Critical Engine Fixes (hcie-engine-api)
|
|||
|
|
|
|||
|
|
### 1.1 Effects Pipeline — `layer.pixels` Corruption Fix (CRITICAL)
|
|||
|
|
|
|||
|
|
**File:** `hcie-engine-api/src/partial_composite.rs` (line ~261)
|
|||
|
|
|
|||
|
|
**Root Cause:**
|
|||
|
|
`apply_effects_and_sync_tiles()` wrote effects-applied pixels back to `layer.pixels`:
|
|||
|
|
```rust
|
|||
|
|
layer.pixels = processed; // BUG: corrupted raw drawing data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
This caused a cascade of bugs:
|
|||
|
|
1. User draws stroke → `layer.pixels` = raw + stroke
|
|||
|
|
2. Effects pipeline runs → `layer.pixels` = effects(raw + stroke)
|
|||
|
|
3. User draws next stroke → `layer.pixels` = effects(raw + stroke1) + stroke2
|
|||
|
|
4. Effects pipeline restores from backup → `layer.pixels` = raw (stroke1+stroke2 LOST!)
|
|||
|
|
5. Effects applied → `layer.pixels` = effects(raw) → **strokes vanished**
|
|||
|
|
|
|||
|
|
**Fix:** Removed `layer.pixels = processed`. Now `layer.pixels` always contains raw drawing data. The composite pipeline in `tiled.rs` reads from `effects_cache` when it exists (line 123), bypassing `layer.pixels` entirely for effect-bearing layers.
|
|||
|
|
|
|||
|
|
**Impact:** Fixes ALL of the following:
|
|||
|
|
- New strokes disappearing after effects are applied
|
|||
|
|
- Effects accumulating (shadow-on-shadow stacking)
|
|||
|
|
- Toggle not working correctly (ON creating disabled styles)
|
|||
|
|
- Slider preview showing corrupted state
|
|||
|
|
|
|||
|
|
### 1.2 `update_layer_style()` Missing Dirty Flags
|
|||
|
|
|
|||
|
|
**File:** `hcie-engine-api/src/lib.rs` (line ~1720)
|
|||
|
|
|
|||
|
|
**Root Cause:**
|
|||
|
|
`update_layer_style()` set `effects_dirty`, `dirty`, and `composite_dirty` but did NOT set:
|
|||
|
|
- `dirty_bounds` — caused full-canvas recomposite (~33MB for 4K) on every toggle
|
|||
|
|
- `below_cache_dirty` — risked stale below-layer composite cache
|
|||
|
|
|
|||
|
|
**Fix:** Added:
|
|||
|
|
```rust
|
|||
|
|
let w = self.document.canvas_width;
|
|||
|
|
let h = self.document.canvas_height;
|
|||
|
|
self.document.dirty_bounds = Some([0, 0, w, h]);
|
|||
|
|
self.below_cache_dirty = true;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Same fix applied to `remove_layer_style_by_index()`.
|
|||
|
|
|
|||
|
|
### 1.3 Stroke Drawing — `effects_dirty` During Strokes
|
|||
|
|
|
|||
|
|
**File:** `hcie-engine-api/src/stroke_brush.rs` (4 locations)
|
|||
|
|
|
|||
|
|
**Context:** `effects_dirty` is set during stroke drawing so the effects pipeline re-runs after the stroke ends. This is now safe because the effects pipeline no longer corrupts `layer.pixels` (fix 1.1).
|
|||
|
|
|
|||
|
|
**Functions modified:**
|
|||
|
|
- `draw_brush_stroke` (line ~134)
|
|||
|
|
- `draw_brush_stroke` (line ~172)
|
|||
|
|
- `draw_pen_segment` (line ~203)
|
|||
|
|
- `draw_stroke` (line ~266)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Iced GUI Fixes (hcie-iced-gui)
|
|||
|
|
|
|||
|
|
### 2.1 Layer Style Toggle Inversion
|
|||
|
|
|
|||
|
|
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` (line ~1044)
|
|||
|
|
|
|||
|
|
**Root Cause:**
|
|||
|
|
```rust
|
|||
|
|
enabled: existing.map_or(false, |s| matches!(s, LayerStyle::DropShadow { enabled, .. } if *enabled))
|
|||
|
|
```
|
|||
|
|
When style doesn't exist (`existing = None`), `map_or(false, ...)` returns `false`. First click ON created disabled style.
|
|||
|
|
|
|||
|
|
**Fix:** Changed to explicit logic:
|
|||
|
|
- Style doesn't exist → `new_enabled = true` (first click enables)
|
|||
|
|
- Style exists → `new_enabled = !was_enabled` (toggle)
|
|||
|
|
|
|||
|
|
### 2.2 Layer Style Panel — Draggable Floating Dialog
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- `app.rs` — new state fields, messages, handlers
|
|||
|
|
- `panels/layer_styles.rs` — draggable title bar, X button, offset positioning
|
|||
|
|
|
|||
|
|
**Implementation:**
|
|||
|
|
- Added `layer_style_offset: (f32, f32)`, `layer_style_dragging: bool`, `layer_style_drag_start: Option<(f32, f32)>` state
|
|||
|
|
- Added `LayerStyleDialogDragStart/Move/End` messages
|
|||
|
|
- Title bar uses `mouse_area.on_press()` to start drag
|
|||
|
|
- Global `iced::event::listen_with()` subscription tracks `CursorMoved` events during drag
|
|||
|
|
- Delta-based position tracking: each move event adds delta to offset
|
|||
|
|
- Dialog rendering uses `padding()` with offset to shift from center
|
|||
|
|
- X close button (`\u{2715}`) wired to `LayerStyleCancel`
|
|||
|
|
|
|||
|
|
### 2.3 Layer Style Panel — Reopen Mechanism
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- `panels/layers.rs` — "fx" button in toolbar
|
|||
|
|
- `panels/menus.rs` — "Layer Styles..." in Layer menu (index 4,7)
|
|||
|
|
- `app.rs` — menu action handler
|
|||
|
|
|
|||
|
|
**Buttons:**
|
|||
|
|
- **"fx" button** in layers panel bottom toolbar → `Message::OpenLayerStyleDialog`
|
|||
|
|
- **Layer menu → "Layer Styles..."** → menu action (4,7) → `Message::OpenLayerStyleDialog`
|
|||
|
|
- Opening resets drag offset to (0, 0)
|
|||
|
|
|
|||
|
|
### 2.4 Layer Style Color Picker
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- `app.rs` — `LayerStyleUpdateColor`, `ShowStyleColorPicker`, `HideStyleColorPicker` messages
|
|||
|
|
- `panels/layer_styles.rs` — clickable color swatch, inline HSL picker popup
|
|||
|
|
|
|||
|
|
**Implementation:**
|
|||
|
|
- Color swatch is clickable (opens HSL color picker popup)
|
|||
|
|
- Popup has H/S/L sliders, color preview, hex display, Close button
|
|||
|
|
- `LayerStyleUpdateColor` handler updates the style's color field in the engine
|
|||
|
|
- HSL state tracked in `style_color_hsl: (f32, f32, f32)`
|
|||
|
|
|
|||
|
|
### 2.5 Color Picker Redesign
|
|||
|
|
|
|||
|
|
**File:** `color_picker.rs` (complete rewrite)
|
|||
|
|
|
|||
|
|
**Layout (matching egui version):**
|
|||
|
|
1. Primary/Secondary swatch buttons + swap button (`\u{21C5}`)
|
|||
|
|
2. Editable hex input field (`text_input`)
|
|||
|
|
3. Tab bar: W (Color Wheel), H (HSL Sliders), G (Palette Grid)
|
|||
|
|
4. Tab W: Hue ring (36 cells × 10°) + SL picker grid (12×12)
|
|||
|
|
5. Tab H: HSL sliders with degree/percent readout
|
|||
|
|
6. Tab G: 10×10 palette grid (algorithmically generated like egui)
|
|||
|
|
7. Recent colors section
|
|||
|
|
|
|||
|
|
**Tab state:** `color_tab: usize` field in `HcieIcedApp`, `ColorTabChanged(usize)` message.
|
|||
|
|
|
|||
|
|
### 2.6 Recent Files Persistence
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- `app.rs` — `load_recent_files()`, `save_recent_files()`, config helpers
|
|||
|
|
- `Cargo.toml` — added `dirs = "5.0"` and `serde = { workspace = true }`
|
|||
|
|
|
|||
|
|
**Implementation:**
|
|||
|
|
- Config stored at `~/.config/hcie-iced/recent_files.json`
|
|||
|
|
- `RecentFileEntry` derives `serde::Serialize` and `serde::Deserialize`
|
|||
|
|
- `add_recent_file()` persists to disk after each add
|
|||
|
|
- `ClearRecentFiles` handler persists empty list
|
|||
|
|
- `new()` loads from disk on startup
|
|||
|
|
|
|||
|
|
### 2.7 Backdrop for Modal Dialogs
|
|||
|
|
|
|||
|
|
**File:** `panels/layer_styles.rs`
|
|||
|
|
|
|||
|
|
**Fix:** Added semi-transparent backdrop (`rgba(0,0,0,0.5)`) around the dialog, matching the pattern used by all other dialogs (NewImage, BrightnessContrast, CloseConfirm).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Build Status
|
|||
|
|
|
|||
|
|
All changes compile successfully:
|
|||
|
|
```
|
|||
|
|
cargo build -p hcie-engine-api -p hcie-iced-gui
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Pre-existing warnings only (dead code in `MenuDef`, unused `union_regions`, etc.).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Files Modified
|
|||
|
|
|
|||
|
|
### Engine (locked layer)
|
|||
|
|
| File | Changes |
|
|||
|
|
|------|---------|
|
|||
|
|
| `hcie-engine-api/src/partial_composite.rs` | Removed `layer.pixels = processed` in effects pipeline |
|
|||
|
|
| `hcie-engine-api/src/lib.rs` | Added `dirty_bounds`/`below_cache_dirty` to `update_layer_style` and `remove_layer_style_by_index` |
|
|||
|
|
| `hcie-engine-api/src/stroke_brush.rs` | Restored `effects_dirty` in 4 drawing functions |
|
|||
|
|
|
|||
|
|
### Iced GUI (open layer)
|
|||
|
|
| File | Changes |
|
|||
|
|
|------|---------|
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/src/app.rs` | Toggle fix, drag state/messages/handlers, color messages, recent files, menu wiring |
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs` | Complete rewrite: draggable, X button, color picker, backdrop |
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/src/panels/layers.rs` | Added "fx" button to toolbar |
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/src/color_picker.rs` | Complete rewrite: tabs, wheel, hex input, swatches |
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs` | Updated `layer_styles::view()` calls with new parameters |
|
|||
|
|
| `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml` | Added `dirs` and `serde` dependencies |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Known Remaining Issues
|
|||
|
|
|
|||
|
|
1. **Panel positioning:** The drag uses `padding()` offset which may not perfectly center on all screen sizes. The dialog may need `top()`/`left()` absolute positioning for true floating behavior.
|
|||
|
|
2. **Color picker wheel:** The "W" tab uses a grid approximation (36 hue cells + 12×12 SL grid). An actual circular wheel would require custom rendering (iced Canvas widget).
|
|||
|
|
3. **Effects pipeline backup:** The `raw_pixel_backup` is never cleared when a style is removed and the layer has no more styles. This could accumulate memory for layers that had styles and then removed them all.
|
|||
|
|
4. **egui version has the same toggle bug:** The `map_or(false, ...)` pattern exists in the egui version too. The engine fix (1.1) resolves the effects corruption for both GUIs, but the egui toggle inversion needs a separate fix.
|