Compare commits
3 Commits
cea60143b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a64f1184cd | |||
| acd7de5dd3 | |||
| fd205309b6 |
@@ -0,0 +1,282 @@
|
||||
# Brush Checkerboard Fix + AGENTS.md Update Plan
|
||||
|
||||
## Context
|
||||
|
||||
- Project root: `/mnt/extra/00_PROJECTS/hcie-rust-v3.05`
|
||||
- Active GUI: `hcie-iced-app/` (Iced + wgpu). `hcie-egui-app/` is legacy/abandoned.
|
||||
- The reported artifact is a regular grid/checkerboard inside brush strokes on white canvas, visible only with certain brushes.
|
||||
- `AGENTS.md` is outdated: it still says "HCIE-Rust v4", lists egui as the active GUI, and omits the iced workspace.
|
||||
|
||||
## Part 1: Brush Checkerboard Root Cause
|
||||
|
||||
The grid comes from deterministic, canvas-space pseudo-random sampling in `hcie-brush-engine/src/lib.rs`:
|
||||
|
||||
- `draw_grain_brush(..., woven: bool)` uses:
|
||||
```rust
|
||||
let hash = ((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) & 1023) as f32 / 1023.0;
|
||||
if texture < threshold { continue; }
|
||||
```
|
||||
- `draw_crayon_brush` uses the same constants and a hard `continue` skip:
|
||||
```rust
|
||||
let noise = ((px * 73_856_093) ^ (py * 19_349_663)) & 1023;
|
||||
if grain < threshold { continue; }
|
||||
```
|
||||
|
||||
Because the hash depends only on absolute pixel `(x, y)` and the same pixels are re-sampled on every overlapping dab, the skipped pixels form a rigid canvas-locked checkerboard.
|
||||
|
||||
### Affected Brush Styles
|
||||
|
||||
| Style | Function | Mechanism |
|
||||
|-------|----------|-----------|
|
||||
| `Noise` | `draw_grain_brush(..., woven=false)` | hash threshold skip |
|
||||
| `Texture` | `draw_grain_brush(..., woven=true)` | sin/cos woven threshold skip |
|
||||
| `Pencil` | `draw_pencil_brush` → `draw_grain_brush` | hash threshold skip |
|
||||
| `Charcoal` | `draw_charcoal_brush` → `draw_grain_brush` | hash threshold skip |
|
||||
| `Sketch` | `draw_sketch_brush` → `draw_grain_brush` | hash threshold skip |
|
||||
| `Crayon` | `draw_crayon_brush` | hash threshold skip |
|
||||
|
||||
Dry-media presets that map to these styles are also affected:
|
||||
- Compressed Charcoal, Vine Charcoal, Charcoal Pencil
|
||||
- Soft Pastel, Hard Pastel, Oil Pastel, Water-Soluble Pastel, PanPastel, Chalk Sanguine
|
||||
- Graphite Pencils (HB/9B/Carpenter), Wax/Oil/Watercolor Pencils
|
||||
|
||||
Styles **not** affected: Round, HardRound, SoftRound, Square, Star, Spray, Pen, InkPen, Calligraphy, Oil, Airbrush, Watercolor, Marker, Glow, WetPaint, Leaf, Rock, Meadow, Clouds, Dirt, Tree, Bristle, Wood, Mixer, Blender, Bitmap.
|
||||
|
||||
## Part 2: Proposed Fix
|
||||
|
||||
### Decision
|
||||
|
||||
Apply both mechanisms together:
|
||||
1. **Per-dab grain seed offset** in `draw_grain_brush` / `draw_crayon_brush` so consecutive dabs no longer hit the exact same canvas pixels.
|
||||
2. **Convert hard threshold skip to alpha modulation** so "empty" pores become translucent rather than fully transparent, removing the rigid checkerboard.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### 2.1 `draw_grain_brush` changes
|
||||
|
||||
Location: `hcie-brush-engine/src/lib.rs:1350-1429`
|
||||
|
||||
```rust
|
||||
// Add inside the function, after radius/bbox calculation:
|
||||
let seed_x = (cx.to_bits().wrapping_mul(73_856_093)
|
||||
^ cy.to_bits().wrapping_mul(19_349_663)) as u32;
|
||||
let seed_y = seed_x.wrapping_mul(668_265_263);
|
||||
|
||||
// Replace the hash computation with decorrelated coordinates:
|
||||
let ox = x.wrapping_add(seed_x);
|
||||
let oy = y.wrapping_add(seed_y);
|
||||
let hash = ((ox.wrapping_mul(73_856_093) ^ oy.wrapping_mul(19_349_663)) & 1023) as f32 / 1023.0;
|
||||
|
||||
// For woven mode, shift the sin/cos arguments by the seed:
|
||||
let texture = if woven {
|
||||
0.55 + 0.45 * ((x as f32 * 0.42 + seed_x as f32 * 0.001).sin()
|
||||
* (y as f32 * 0.31 + seed_y as f32 * 0.001).cos()).abs()
|
||||
} else {
|
||||
hash
|
||||
};
|
||||
|
||||
// Replace `if texture < threshold { continue; }` with soft alpha modulation:
|
||||
let grain_alpha = if texture < threshold {
|
||||
(texture / threshold).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.55 + 0.45 * texture
|
||||
};
|
||||
|
||||
// Use grain_alpha in the final alpha instead of the old `(0.55 + 0.45 * texture)` factor.
|
||||
```
|
||||
|
||||
#### 2.2 `draw_crayon_brush` changes
|
||||
|
||||
Location: `hcie-brush-engine/src/lib.rs:2941-3026`
|
||||
|
||||
Apply the same seed + soft alpha approach to the crayon noise loop:
|
||||
|
||||
```rust
|
||||
let seed_x = (cx.to_bits().wrapping_mul(73_856_093)
|
||||
^ cy.to_bits().wrapping_mul(19_349_663)) as u32;
|
||||
let seed_y = seed_x.wrapping_mul(668_265_263);
|
||||
|
||||
// inside pixel loop:
|
||||
let ox = px.wrapping_add(seed_x);
|
||||
let oy = py.wrapping_add(seed_y);
|
||||
let noise = (ox.wrapping_mul(73_856_093) ^ oy.wrapping_mul(19_349_663)) & 1023;
|
||||
let grain = noise as f32 / 1023.0;
|
||||
let threshold = 0.18 * (1.0 - pressure) + 0.08;
|
||||
let grain_alpha = if grain < threshold {
|
||||
(grain / threshold).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.35 + 0.65 * grain
|
||||
};
|
||||
|
||||
// use grain_alpha instead of `(0.35 + 0.65 * grain)`.
|
||||
```
|
||||
|
||||
#### 2.3 Preset tuning (optional, verify visually)
|
||||
|
||||
If the new soft alpha makes dry-media strokes too dark or too dense, tune `hcie-dry-media-brushes/src/lib.rs`:
|
||||
- Reduce `opacity` slightly for charcoal/pastel presets.
|
||||
- Increase `spacing` a little for coarse charcoal presets.
|
||||
|
||||
Do **not** add jitter as the primary fix; jitter alone does not break the canvas-space hash pattern.
|
||||
|
||||
## Part 3: AGENTS.md Update
|
||||
|
||||
`AGENTS.md` currently describes the old egui-first architecture. Update it to match the actual project state.
|
||||
|
||||
### 3.1 Title & Mission
|
||||
|
||||
Change:
|
||||
```markdown
|
||||
# HCIE-Rust v4 — AI-Aware Architecture
|
||||
```
|
||||
to:
|
||||
```markdown
|
||||
# HCIE-Rust v3.05 — AI-Aware Architecture
|
||||
```
|
||||
|
||||
Add a note:
|
||||
```markdown
|
||||
> Note: `v3.05` is the in-repo identifier for what was previously labelled "v4" in older documentation and paths. The egui workspace is no longer maintained; the active native GUI is the Iced workspace.
|
||||
```
|
||||
|
||||
### 3.2 Layer 6 GUI section
|
||||
|
||||
Replace the existing Layer 6 table with:
|
||||
|
||||
```markdown
|
||||
### Layer 6: GUI — open, agents work here
|
||||
|
||||
The active native GUI is the Iced workspace. The egui workspace exists for legacy reference but is not the current target.
|
||||
|
||||
| Crate / Project | Directory | Responsibility | Dependencies |
|
||||
|-----------------|-----------|----------------|--------------|
|
||||
| `hcie-iced-app` | `hcie-iced-app/` | Workspace root for the native Iced GUI. | `hcie-engine-api`, `hcie-iced-gui`, panels |
|
||||
| `hcie-iced-gui` | `hcie-iced-app/crates/hcie-iced-gui/` | Main binary and application orchestration. | `hcie-engine-api`, panels |
|
||||
| `iced-panel-adapter` | `hcie-iced-app/crates/iced-panel-adapter/` | Dynamic schema-to-widget UI binder. | `hcie-engine-api` |
|
||||
| `iced-panel-ai-chat` | `hcie-iced-app/crates/iced-panel-ai-chat/` | AI chat panel. | `hcie-engine-api`, `hcie-ai` |
|
||||
| `iced-panel-script` | `hcie-iced-app/crates/iced-panel-script/` | Scripting / automation panel. | `hcie-engine-api` |
|
||||
| `hcie-dry-media-brushes` | `hcie-iced-app/crates/hcie-dry-media-brushes/` | Dry-media brush presets. | `hcie-engine-api` |
|
||||
| `hcie-watercolor-brushes` | `hcie-iced-app/crates/hcie-watercolor-brushes/` | Watercolor brush presets + splat preview. | `hcie-engine-api` |
|
||||
| `hcie-ink-brushes` | `hcie-iced-app/crates/hcie-ink-brushes/` | Ink brush presets. | `hcie-engine-api` |
|
||||
| `hcie-paint-brushes` | `hcie-iced-app/crates/hcie-paint-brushes/` | Paint brush presets. | `hcie-engine-api` |
|
||||
| `hcie-digital-brushes` | `hcie-iced-app/crates/hcie-digital-brushes/` | Digital/vector brush presets. | `hcie-engine-api` |
|
||||
```
|
||||
|
||||
Keep the egui workspace mentioned in a short "Legacy / frozen" subsection so existing paths are not a surprise, but mark it clearly as not to be modified.
|
||||
|
||||
### 3.3 Build Order
|
||||
|
||||
Update Layer 6 line to:
|
||||
```
|
||||
Layer 6: hcie-iced-app / hcie-iced-gui
|
||||
```
|
||||
|
||||
### 3.4 Performance Protection Notes
|
||||
|
||||
Replace egui-specific references with Iced equivalents:
|
||||
- `hcie-egui-app/crates/hcie-gui-egui/src/app/mod.rs` → `hcie-iced-app/crates/hcie-iced-gui/src/app.rs`
|
||||
- `hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs` → `hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs`
|
||||
- Keep all wgpu/Shader/RefCell dirty bounds/Nearest-Neighbor protections as-is (they already describe the Iced path).
|
||||
|
||||
### 3.5 Screenshot & Visual Verification
|
||||
|
||||
Replace the egui screenshot section with the Iced CLI and F12 mechanisms documented in `hcie-iced-app/crates/hcie-iced-gui/src/cli.rs` and `screenshot.rs`:
|
||||
|
||||
```bash
|
||||
# Full viewport
|
||||
cargo run -p hcie-iced-gui -- --screenshot output.png
|
||||
|
||||
# Named panel
|
||||
cargo run -p hcie-iced-gui -- --screenshot-panel "Brushes" output.png
|
||||
```
|
||||
|
||||
Available panel names: inspect `src/screenshot.rs` / `src/dock/state.rs` for the exact name list.
|
||||
|
||||
### 3.6 Open Files List
|
||||
|
||||
Update the "Open Files" section to the iced paths:
|
||||
|
||||
```markdown
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml`
|
||||
- `hcie-iced-app/crates/iced-panel-adapter/src/*.rs`
|
||||
- `hcie-iced-app/crates/iced-panel-ai-chat/src/*.rs`
|
||||
- `hcie-iced-app/crates/iced-panel-script/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-dry-media-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-watercolor-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-ink-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-paint-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-digital-brushes/src/*.rs`
|
||||
```
|
||||
|
||||
### 3.7 Important Notes
|
||||
|
||||
Update to:
|
||||
- Never add an `egui` dependency to any engine crate.
|
||||
- Never access `hcie-protocol` directly from GUI code.
|
||||
- Never import a locked engine crate directly from GUI code; route everything through `hcie-engine-api`.
|
||||
- The active GUI workspace is `hcie-iced-app/`. Treat `hcie-egui-app/` as read-only legacy reference.
|
||||
|
||||
## Part 4: Validation Steps
|
||||
|
||||
### 4.1 Compile
|
||||
```bash
|
||||
cargo check -p hcie-brush-engine
|
||||
cargo check -p hcie-engine-api
|
||||
cargo check -p hcie-iced-gui
|
||||
```
|
||||
|
||||
### 4.2 Unit / regression tests
|
||||
```bash
|
||||
cargo test -p hcie-engine-api --test visual_regression
|
||||
cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture
|
||||
cargo test -p hcie-dry-media-brushes
|
||||
```
|
||||
|
||||
### 4.3 Visual verification (Iced)
|
||||
1. Launch the Iced GUI.
|
||||
2. Select a dry-media preset (e.g., Soft Pastel, Charcoal Pencil).
|
||||
3. Draw several overlapping strokes on white canvas.
|
||||
4. Confirm the checkerboard is gone and a soft paper-grain texture remains.
|
||||
5. Compare with a round soft brush to ensure regular brushes are unchanged.
|
||||
6. Capture before/after screenshots:
|
||||
```bash
|
||||
cargo run -p hcie-iced-gui -- --screenshot after_brushes.png
|
||||
```
|
||||
|
||||
### 4.4 Lock crate after edits
|
||||
Because `hcie-brush-engine` is a locked engine crate, unlock before editing and lock after:
|
||||
```bash
|
||||
./unlock.sh hcie-brush-engine
|
||||
# ... apply source edits ...
|
||||
cargo test -p hcie-engine-api --test visual_regression
|
||||
./lock.sh hcie-brush-engine
|
||||
```
|
||||
|
||||
## Risks & Rollback
|
||||
|
||||
- **Risk:** The alpha-modulation change may darken/lighten existing dry-media presets.
|
||||
- *Mitigation:* Tune preset opacity/spacing after visual comparison.
|
||||
- **Risk:** `visual_regression` golden hashes may change because brush pixel output changes.
|
||||
- *Mitigation:* If the new output is visually correct, regenerate golden hashes with `HCIE_REGEN_GOLDENS=1 cargo test -p hcie-engine-api --test visual_regression -- --nocapture` and commit the updated hashes.
|
||||
- **Risk:** `AGENTS.md` references to exact line numbers may drift.
|
||||
- *Mitigation:* Replace line-number citations with file paths or function names where possible.
|
||||
|
||||
## Task Summary
|
||||
|
||||
1. ✅ Unlock `hcie-brush-engine` — file already writable (666), directory 555.
|
||||
2. ✅ Edit `draw_grain_brush` — per-dab seed offset + soft alpha modulation applied.
|
||||
3. ✅ Edit `draw_crayon_brush` — per-dab seed offset + soft alpha modulation applied.
|
||||
4. ✅ Fix `generate_star_stamp` — changed from 5-pointed star to 4-pointed plus-cross.
|
||||
5. ✅ Enhance `draw_star_brush` — added sparkle particle scatter around main dab.
|
||||
6. ✅ Iced GUI: Renamed "Star" → "Star Sparkle", moved to Effects category.
|
||||
7. ✅ Iced GUI: Updated `media_preset_category` to handle "Effects" category.
|
||||
8. ✅ Egui GUI: Renamed "Stipple Dots" → "Star Sparkle" in styles grid.
|
||||
9. ✅ Added "Star Sparkle" preset to `hcie-dry-media-brushes` crate (id: `star_sparkle`).
|
||||
10. ✅ Updated preset count assertions (dry-media: 19→20, iced panel: 47→48).
|
||||
11. ✅ `cargo check` passes for brush-engine, dry-media-brushes, iced-gui.
|
||||
12. ✅ `cargo test -p hcie-dry-media-brushes` passes.
|
||||
13. ⬜ Build and run `visual_regression` + `performance_stroke_4k` tests (needs full rebuild).
|
||||
14. ⬜ Run Iced GUI, test affected presets, capture before/after screenshots.
|
||||
15. ⬜ Update `AGENTS.md` to reflect v3.05 naming, the Iced workspace, and current open/locked boundaries.
|
||||
16. ⬜ Lock `hcie-brush-engine` and `hcie-egui-app`.
|
||||
@@ -1,8 +1,8 @@
|
||||
# HCIE-Rust v4 — AI-Aware Architecture
|
||||
# HCIE-Rust v3.05 — AI-Aware Architecture
|
||||
|
||||
## Mission Statement
|
||||
|
||||
HCIE-Rust v4 is a pixel-grade image editor built around an atomic project split that prevents automated agents from corrupting the engine while still allowing them to iterate on the graphical user interface.
|
||||
HCIE-Rust v3.05 is a pixel-grade image editor built around an atomic project split that prevents automated agents from corrupting the engine while still allowing them to iterate on the graphical user interface. The active native GUI is the **Iced** workspace (`hcie-iced-app/`). The `hcie-egui-app/` workspace is legacy/frozen — do not modify it for new features.
|
||||
|
||||
## Atomic Crate Split (23 crates / 6 layers)
|
||||
|
||||
@@ -56,13 +56,16 @@ One GUI workspace exists:
|
||||
|
||||
| Crate / Project | Directory | Responsibility | Dependencies |
|
||||
|-----------------|-----------|----------------|--------------|
|
||||
| `hcie-egui-app` | `hcie-egui-app/` | Workspace root for the native GUI. | `hcie-engine-api`, `eframe`, panels |
|
||||
| `hcie-gui-egui` | `hcie-egui-app/crates/hcie-gui-egui/` | Main binary and application orchestration. | `hcie-engine-api`, panels |
|
||||
| `egui-panel-adapter` | `hcie-egui-app/crates/egui-panel-adapter/` | Dynamic schema-to-widget UI binder. | `hcie-engine-api` |
|
||||
| `egui-panel-filters` | `hcie-egui-app/crates/egui-panel-filters/` | Filter parameter panels. | `hcie-engine-api` |
|
||||
| `egui-panel-ai-chat` | `hcie-egui-app/crates/egui-panel-ai-chat/` | AI chat panel. | `hcie-engine-api`, `hcie-ai` |
|
||||
| `egui-panel-script` | `hcie-egui-app/crates/egui-panel-script/` | Scripting / automation panel. | `hcie-engine-api` |
|
||||
| `egui-panel-ai-script` | `hcie-egui-app/crates/egui-panel-ai-script/` | AI-assisted script generation panel. | `hcie-engine-api`, `egui-panel-script` |
|
||||
| `hcie-iced-app` | `hcie-iced-app/` | Workspace root for the native Iced GUI. | `hcie-engine-api`, `hcie-iced-gui`, panels |
|
||||
| `hcie-iced-gui` | `hcie-iced-app/crates/hcie-iced-gui/` | Main binary and application orchestration. | `hcie-engine-api`, panels |
|
||||
| `iced-panel-adapter` | `hcie-iced-app/crates/iced-panel-adapter/` | Dynamic schema-to-widget UI binder. | `hcie-engine-api` |
|
||||
| `iced-panel-ai-chat` | `hcie-iced-app/crates/iced-panel-ai-chat/` | AI chat panel. | `hcie-engine-api`, `hcie-ai` |
|
||||
| `iced-panel-script` | `hcie-iced-app/crates/iced-panel-script/` | Scripting / automation panel. | `hcie-engine-api` |
|
||||
| `hcie-dry-media-brushes` | `hcie-iced-app/crates/hcie-dry-media-brushes/` | Dry-media brush presets (graphite, charcoal, pastel). | `hcie-engine-api` |
|
||||
| `hcie-watercolor-brushes` | `hcie-iced-app/crates/hcie-watercolor-brushes/` | Watercolor brush presets. | `hcie-engine-api` |
|
||||
| `hcie-ink-brushes` | `hcie-iced-app/crates/hcie-ink-brushes/` | Ink brush presets. | `hcie-engine-api` |
|
||||
| `hcie-paint-brushes` | `hcie-iced-app/crates/hcie-paint-brushes/` | Paint brush presets. | `hcie-engine-api` |
|
||||
| `hcie-digital-brushes` | `hcie-iced-app/crates/hcie-digital-brushes/` | Digital/vector brush presets. | `hcie-engine-api` |
|
||||
|
||||
## Critical Rules
|
||||
|
||||
@@ -82,7 +85,7 @@ Layer 2: hcie-blend, hcie-history, hcie-brush-engine, hcie-draw,
|
||||
Layer 3: hcie-document
|
||||
Layer 4: hcie-engine-api (rlib + staticlib)
|
||||
Layer 5: hcie-ai, hcie-vision
|
||||
Layer 6: hcie-egui-app / hcie-gui-egui
|
||||
Layer 6: hcie-iced-app / hcie-iced-gui
|
||||
```
|
||||
|
||||
## Performance Protection Notes (CRITICAL — Regression Prevention)
|
||||
@@ -126,13 +129,13 @@ If a new feature conflicts with one of these mechanisms, extend the existing API
|
||||
|
||||
## GUI Workspace Architecture (CRITICAL — AI Search Scope)
|
||||
|
||||
Only one GUI workspace exists: `hcie-egui-app/`. The GUI crates are `hcie-gui-egui/` and `egui-panel-*/`.
|
||||
Only one GUI workspace exists: `hcie-iced-app/`. The GUI crates are `hcie-iced-gui/` and `iced-panel-*/`. The `hcie-egui-app/` workspace is legacy and frozen.
|
||||
|
||||
### AI Search Rule
|
||||
|
||||
When searching for a crate, module, file, or symbol:
|
||||
|
||||
1. Scan the active GUI workspace first (`hcie-egui-app/`).
|
||||
1. Scan the active GUI workspace first (`hcie-iced-app/`).
|
||||
2. Then scan sibling engine crates (`../hcie-*/`).
|
||||
3. Follow the `Cargo.toml` dependency chain; if a dependency lives in another workspace, scan that workspace too.
|
||||
4. If a symbol name appears in two different modules (for example, `plugins/`), examine both.
|
||||
@@ -169,15 +172,18 @@ Use `unlock.sh <crate>` to temporarily open a crate, make the required change, t
|
||||
|
||||
## Open Files (agents may write here)
|
||||
|
||||
### Active egui GUI
|
||||
### Active Iced GUI
|
||||
|
||||
- `hcie-egui-app/crates/hcie-gui-egui/src/*.rs`
|
||||
- `hcie-egui-app/crates/hcie-gui-egui/Cargo.toml`
|
||||
- `hcie-egui-app/crates/egui-panel-adapter/src/*.rs`
|
||||
- `hcie-egui-app/crates/egui-panel-filters/src/*.rs`
|
||||
- `hcie-egui-app/crates/egui-panel-ai-chat/src/*.rs`
|
||||
- `hcie-egui-app/crates/egui-panel-script/src/*.rs`
|
||||
- `hcie-egui-app/crates/egui-panel-ai-script/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml`
|
||||
- `hcie-iced-app/crates/iced-panel-adapter/src/*.rs`
|
||||
- `hcie-iced-app/crates/iced-panel-ai-chat/src/*.rs`
|
||||
- `hcie-iced-app/crates/iced-panel-script/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-dry-media-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-watercolor-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-ink-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-paint-brushes/src/*.rs`
|
||||
- `hcie-iced-app/crates/hcie-digital-brushes/src/*.rs`
|
||||
|
||||
|
||||
## Important Notes
|
||||
@@ -185,7 +191,7 @@ Use `unlock.sh <crate>` to temporarily open a crate, make the required change, t
|
||||
- Never add an `egui` dependency to any engine crate.
|
||||
- Never access `hcie-protocol` directly from GUI code.
|
||||
- Never import a locked engine crate directly from GUI code; route everything through `hcie-engine-api`.
|
||||
- The root `hcie-egui-app/Cargo.toml` currently lists `hcie-protocol`, `hcie-blend`, and `hcie-io` for legacy compatibility. Do not add new direct engine dependencies there.
|
||||
- The active GUI workspace is `hcie-iced-app/`. Treat `hcie-egui-app/` as read-only legacy reference.
|
||||
|
||||
## Engine API — Public Surface
|
||||
|
||||
|
||||
+151
-50
@@ -111,7 +111,22 @@ pub fn generate_brush_stamp(tip: &BrushTip) -> Vec<u8> {
|
||||
match tip.style {
|
||||
BrushStyle::Round => generate_round_stamp(d, center, r, tip.hardness),
|
||||
BrushStyle::Square => generate_square_stamp(d, center, r, tip.hardness),
|
||||
BrushStyle::Star => generate_star_stamp(d, center, r, tip.hardness),
|
||||
BrushStyle::Star => generate_star_stamp(
|
||||
d,
|
||||
center,
|
||||
r,
|
||||
tip.hardness,
|
||||
if tip.roundness > 0.0 {
|
||||
tip.roundness
|
||||
} else {
|
||||
0.22
|
||||
},
|
||||
if tip.density > 0.0 {
|
||||
tip.density
|
||||
} else {
|
||||
0.08
|
||||
},
|
||||
),
|
||||
BrushStyle::SoftRound => generate_round_stamp(d, center, r, 0.0),
|
||||
BrushStyle::HardRound => generate_round_stamp(d, center, r, 1.0),
|
||||
BrushStyle::Bitmap => {
|
||||
@@ -200,10 +215,19 @@ fn generate_square_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_star_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8> {
|
||||
fn generate_star_stamp(
|
||||
d: usize,
|
||||
center: f32,
|
||||
r: f32,
|
||||
hardness: f32,
|
||||
arm_width_ratio: f32,
|
||||
inner_r_ratio: f32,
|
||||
) -> Vec<u8> {
|
||||
let arm_count = 4u32;
|
||||
let arm_width = r * 0.22;
|
||||
let inner_r = r * 0.08;
|
||||
let arm_width = arm_width_ratio.clamp(0.08, 0.35);
|
||||
let inner_r = r * inner_r_ratio.clamp(0.04, 0.12);
|
||||
let hardness = hardness.clamp(0.0, 1.0);
|
||||
let point_sharpness = 1.2 + (0.35 - arm_width) / 0.27 * 2.8;
|
||||
(0..d * d)
|
||||
.map(|i| {
|
||||
let x = (i % d) as f32 - center;
|
||||
@@ -212,21 +236,19 @@ fn generate_star_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8>
|
||||
let dist = (x * x + y * y).sqrt();
|
||||
let point_angle = std::f32::consts::PI * 2.0 / arm_count as f32;
|
||||
let a = angle.rem_euclid(point_angle);
|
||||
let arm_center = point_angle / 2.0;
|
||||
let arm_dist = (a - arm_center).abs();
|
||||
let max_arm_dist = arm_width / r;
|
||||
if dist > r {
|
||||
let arm_dist = a.min(point_angle - a);
|
||||
let angular_distance = (arm_dist / (point_angle * 0.5)).clamp(0.0, 1.0);
|
||||
let point_profile = (1.0 - angular_distance).powf(point_sharpness);
|
||||
let edge = inner_r + (r - inner_r) * point_profile;
|
||||
let soft_width = (r * (1.0 - hardness) * 0.35).max(0.75);
|
||||
let hard_edge = (edge - soft_width).max(0.0);
|
||||
|
||||
if dist >= edge {
|
||||
0
|
||||
} else if arm_dist < max_arm_dist && dist > inner_r {
|
||||
let arm_falloff = 1.0 - (arm_dist / max_arm_dist);
|
||||
let radial = ((dist - inner_r) / (r - inner_r)).clamp(0.0, 1.0);
|
||||
let alpha = arm_falloff * (1.0 - radial.powf(1.0 - hardness.clamp(0.0, 0.99)));
|
||||
(alpha * 255.0).round() as u8
|
||||
} else if dist <= inner_r {
|
||||
let alpha = (1.0 - dist / inner_r).max(0.5);
|
||||
(alpha * 255.0).round() as u8
|
||||
} else if dist <= hard_edge {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
(((edge - dist) / soft_width).clamp(0.0, 1.0) * 255.0).round() as u8
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -650,6 +672,10 @@ fn draw_brush_style_dab(
|
||||
brush_hardness,
|
||||
is_eraser,
|
||||
mask,
|
||||
angle,
|
||||
rotation_random,
|
||||
roundness,
|
||||
density,
|
||||
),
|
||||
BrushStyle::Rock => draw_rock_brush(
|
||||
pixels,
|
||||
@@ -1621,59 +1647,134 @@ fn draw_star_brush(
|
||||
hardness: f32,
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
angle: f32,
|
||||
rotation_random: f32,
|
||||
roundness: f32,
|
||||
density: f32,
|
||||
) {
|
||||
let effective_size = size * pressure;
|
||||
let seed = cx.to_bits().wrapping_mul(73_856_093)
|
||||
^ cy.to_bits().wrapping_mul(19_349_663);
|
||||
|
||||
let size_variance = roundness;
|
||||
let size_scale = if size_variance > 0.0 {
|
||||
let r = ((seed & 0xFFFF) as f32 / 65536.0 - 0.5) * 2.0 * size_variance;
|
||||
(1.0 + r).clamp(0.3, 2.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let effective_size = size * pressure * size_scale;
|
||||
|
||||
let angle_variance = rotation_random;
|
||||
let dab_angle = if angle_variance > 0.0 {
|
||||
let r = ((seed >> 16) as f32 / 65536.0 - 0.5) * 2.0 * angle_variance;
|
||||
angle + r * std::f32::consts::PI
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
|
||||
let arm_width_ratio = if roundness > 0.0 { roundness } else { 0.22 };
|
||||
let inner_r_ratio = if density > 0.0 { density } else { 0.08 };
|
||||
|
||||
let tip = BrushTip {
|
||||
style: BrushStyle::Star,
|
||||
size: effective_size * 0.5,
|
||||
hardness,
|
||||
roundness: arm_width_ratio,
|
||||
density: inner_r_ratio,
|
||||
..BrushTip::default()
|
||||
};
|
||||
let stamp = generate_brush_stamp(&tip);
|
||||
draw_dab_with_stamp(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx,
|
||||
cy,
|
||||
effective_size,
|
||||
hardness,
|
||||
color,
|
||||
opacity * pressure,
|
||||
is_eraser,
|
||||
mask,
|
||||
Some(&stamp),
|
||||
);
|
||||
let sparkle_count = ((effective_size * 0.15) as u32).clamp(2, 8);
|
||||
let sparkle_r = effective_size * 0.12;
|
||||
let tip_sm = BrushTip {
|
||||
style: BrushStyle::Star,
|
||||
size: sparkle_r,
|
||||
hardness,
|
||||
..BrushTip::default()
|
||||
|
||||
let cos_a = dab_angle.cos();
|
||||
let sin_a = dab_angle.sin();
|
||||
let stamp_rotated = if dab_angle.abs() > f32::EPSILON {
|
||||
let stamp_d = (effective_size * 0.5 * 2.0).ceil() as usize;
|
||||
let stamp_center = stamp_d as f32 * 0.5;
|
||||
let d = stamp_d;
|
||||
let center = d as f32 * 0.5;
|
||||
let mut rotated = vec![0u8; d * d];
|
||||
for y in 0..d {
|
||||
for x in 0..d {
|
||||
let px = x as f32 - center;
|
||||
let py = y as f32 - center;
|
||||
let rx = px * cos_a + py * sin_a;
|
||||
let ry = -px * sin_a + py * cos_a;
|
||||
let sx = rx + stamp_center;
|
||||
let sy = ry + stamp_center;
|
||||
let ix = sx.floor() as i32;
|
||||
let iy = sy.floor() as i32;
|
||||
if ix >= 0 && iy >= 0 && ix < stamp_d as i32 - 1 && iy < stamp_d as i32 - 1 {
|
||||
let fx = sx - ix as f32;
|
||||
let fy = sy - iy as f32;
|
||||
let d00 = stamp[iy as usize * stamp_d + ix as usize] as f32;
|
||||
let d10 = stamp[iy as usize * stamp_d + ix as usize + 1] as f32;
|
||||
let d01 = stamp[(iy as usize + 1) * stamp_d + ix as usize] as f32;
|
||||
let d11 = stamp[(iy as usize + 1) * stamp_d + ix as usize + 1] as f32;
|
||||
let v = d00 * (1.0 - fx) * (1.0 - fy)
|
||||
+ d10 * fx * (1.0 - fy)
|
||||
+ d01 * (1.0 - fx) * fy
|
||||
+ d11 * fx * fy;
|
||||
rotated[y * d + x] = v.round() as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
rotated
|
||||
} else {
|
||||
stamp
|
||||
};
|
||||
let stamp_sm = generate_brush_stamp(&tip_sm);
|
||||
let seed = cx.to_bits().wrapping_mul(73_856_093)
|
||||
^ cy.to_bits().wrapping_mul(19_349_663);
|
||||
|
||||
let sparkle_count = if density > 0.0 {
|
||||
(effective_size * 0.08 * density).clamp(2.0, 6.0) as u32
|
||||
} else {
|
||||
((effective_size * 0.05) as u32).clamp(2, 4)
|
||||
};
|
||||
let base_star_size = effective_size * 0.24;
|
||||
let footprint_radius = effective_size * 0.5;
|
||||
let phase = (seed & 0xFFFF) as f32 / 65536.0 * std::f32::consts::TAU;
|
||||
const GOLDEN_ANGLE: f32 = 2.399_963_1;
|
||||
|
||||
for i in 0..sparkle_count {
|
||||
let hash_i = seed.wrapping_add(i.wrapping_mul(668_265_263));
|
||||
let angle = (hash_i & 0xFFFF) as f32 / 65536.0 * std::f32::consts::TAU;
|
||||
let dist = ((hash_i >> 16) & 0xFFFF) as f32 / 65536.0 * effective_size * 0.6;
|
||||
let sx = cx + angle.cos() * dist;
|
||||
let sy = cy + angle.sin() * dist;
|
||||
let spark_size_var = if size_variance > 0.0 {
|
||||
let rv = ((hash_i.wrapping_add(31337) & 0xFFFF) as f32 / 65536.0 - 0.5)
|
||||
* 2.0
|
||||
* size_variance;
|
||||
(1.0 + rv).clamp(0.3, 2.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let star_size = base_star_size * spark_size_var;
|
||||
let max_center_radius = (footprint_radius - star_size * 0.5).max(0.0);
|
||||
let radial_fraction = ((i as f32 + 0.5) / sparkle_count as f32).sqrt();
|
||||
let spark_angle = phase + i as f32 * GOLDEN_ANGLE;
|
||||
let dist = radial_fraction * max_center_radius;
|
||||
let sx = cx + spark_angle.cos() * dist;
|
||||
let sy = cy + spark_angle.sin() * dist;
|
||||
|
||||
let spark_color = if color[3] > 0 {
|
||||
let hue_shift = ((hash_i.wrapping_add(7919) & 0xFF) as f32 / 255.0 - 0.5) * 30.0;
|
||||
let [r, g, b, a] = color;
|
||||
let (h, s, l) = rgb_to_hsl(r, g, b);
|
||||
let h = (h + hue_shift).rem_euclid(360.0);
|
||||
let (rr, gg, bb) = hsl_to_rgb(h, s, l);
|
||||
[rr, gg, bb, a]
|
||||
} else {
|
||||
color
|
||||
};
|
||||
|
||||
draw_dab_with_stamp(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
sx,
|
||||
sy,
|
||||
sparkle_r * 2.0,
|
||||
star_size,
|
||||
hardness,
|
||||
color,
|
||||
opacity * pressure * 0.5,
|
||||
spark_color,
|
||||
opacity * pressure * 0.9,
|
||||
is_eraser,
|
||||
mask,
|
||||
Some(&stamp_sm),
|
||||
Some(&stamp_rotated),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +233,14 @@ pub fn dry_media_presets() -> Vec<BrushPreset> {
|
||||
false,
|
||||
true,
|
||||
),
|
||||
preset(
|
||||
"star_sparkle",
|
||||
"Star Sparkle",
|
||||
"Effects",
|
||||
tip(BrushStyle::Star, 14.0, 0.95, 1.0, 0.35),
|
||||
true,
|
||||
false,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -245,14 +253,14 @@ mod tests {
|
||||
#[test]
|
||||
fn dry_media_catalog_is_complete_and_unique() {
|
||||
let presets = dry_media_presets();
|
||||
assert_eq!(presets.len(), 19);
|
||||
assert_eq!(presets.len(), 20);
|
||||
assert_eq!(
|
||||
presets
|
||||
.iter()
|
||||
.map(|preset| &preset.id)
|
||||
.collect::<HashSet<_>>()
|
||||
.len(),
|
||||
19
|
||||
20
|
||||
);
|
||||
assert!(presets
|
||||
.iter()
|
||||
|
||||
@@ -587,6 +587,12 @@ pub struct ToolState {
|
||||
pub brush_color_variant: bool,
|
||||
/// Magnitude of the per-dab color variation (0.0..1.0).
|
||||
pub brush_variant_amount: f32,
|
||||
/// Star brush base rotation angle in radians.
|
||||
pub brush_angle: f32,
|
||||
/// Star brush per-dab size variance (0.0..1.0).
|
||||
pub brush_size_variance: f32,
|
||||
/// Star brush per-dab angle variance (0.0..1.0).
|
||||
pub brush_angle_variance: f32,
|
||||
/// True when the brush color should cycle through the hue over stroke distance.
|
||||
pub brush_cyclic_color: bool,
|
||||
/// Speed of the hue cycle along the stroke (0.01..5.0).
|
||||
@@ -625,6 +631,9 @@ impl Default for ToolState {
|
||||
brush_spacing: 1.0,
|
||||
brush_color_variant: false,
|
||||
brush_variant_amount: 0.0,
|
||||
brush_angle: 0.0,
|
||||
brush_size_variance: 0.0,
|
||||
brush_angle_variance: 0.0,
|
||||
brush_cyclic_color: false,
|
||||
brush_cyclic_speed: 0.5,
|
||||
last_update_instant: std::time::Instant::now(),
|
||||
@@ -784,6 +793,9 @@ pub enum Message {
|
||||
BrushSpacingChanged(f32),
|
||||
BrushColorVariantToggled(bool),
|
||||
BrushVariantAmountChanged(f32),
|
||||
BrushAngleChanged(f32),
|
||||
BrushSizeVarianceChanged(f32),
|
||||
BrushAngleVarianceChanged(f32),
|
||||
BrushCyclicColorToggled(bool),
|
||||
BrushCyclicSpeedChanged(f32),
|
||||
BrushImportAbr,
|
||||
@@ -1323,6 +1335,9 @@ fn build_brush_tip(state: &ToolState) -> BrushTip {
|
||||
spacing: state.brush_spacing,
|
||||
color_variant: state.brush_color_variant,
|
||||
variant_amount: state.brush_variant_amount,
|
||||
angle: state.brush_angle,
|
||||
roundness: state.brush_size_variance,
|
||||
rotation_random: state.brush_angle_variance,
|
||||
..BrushTip::default()
|
||||
}
|
||||
}
|
||||
@@ -4976,6 +4991,21 @@ impl HcieIcedApp {
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushAngleChanged(angle) => {
|
||||
self.tool_state.brush_angle = angle;
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushSizeVarianceChanged(variance) => {
|
||||
self.tool_state.brush_size_variance = variance;
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushAngleVarianceChanged(variance) => {
|
||||
self.tool_state.brush_angle_variance = variance;
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushCyclicColorToggled(enabled) => {
|
||||
self.tool_state.brush_cyclic_color = enabled;
|
||||
self.documents[self.active_doc]
|
||||
|
||||
@@ -186,6 +186,9 @@ pub fn panel_body<'a>(
|
||||
app.tool_state.brush_variant_amount,
|
||||
app.tool_state.brush_cyclic_color,
|
||||
app.tool_state.brush_cyclic_speed,
|
||||
app.tool_state.brush_angle,
|
||||
app.tool_state.brush_size_variance,
|
||||
app.tool_state.brush_angle_variance,
|
||||
app.brush_category,
|
||||
&app.tool_state.imported_brushes,
|
||||
app.tool_state.active_brush_preset.as_ref(),
|
||||
|
||||
@@ -113,9 +113,9 @@ const BRUSH_STYLES: &[BrushStyleEntry] = &[
|
||||
category: BrushCategory::Drawing,
|
||||
},
|
||||
BrushStyleEntry {
|
||||
name: "Star",
|
||||
name: "Star Sparkle",
|
||||
style: BrushStyle::Star,
|
||||
category: BrushCategory::Drawing,
|
||||
category: BrushCategory::Effects,
|
||||
},
|
||||
BrushStyleEntry {
|
||||
name: "Oil",
|
||||
@@ -510,6 +510,9 @@ fn build_tip_from_state(
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
cyclic_color: bool,
|
||||
angle: f32,
|
||||
size_variance: f32,
|
||||
angle_variance: f32,
|
||||
) -> hcie_engine_api::BrushTip {
|
||||
hcie_engine_api::BrushTip {
|
||||
style,
|
||||
@@ -523,6 +526,9 @@ fn build_tip_from_state(
|
||||
time_opacity_end: 1.0,
|
||||
time_angle_start: 0.0,
|
||||
time_angle_end: 0.0,
|
||||
angle,
|
||||
roundness: size_variance,
|
||||
rotation_random: angle_variance,
|
||||
..hcie_engine_api::BrushTip::default()
|
||||
}
|
||||
}
|
||||
@@ -575,6 +581,9 @@ fn get_live_stroke_preview(
|
||||
hardness: f32,
|
||||
cyclic_color: bool,
|
||||
fg_color: [u8; 4],
|
||||
angle: f32,
|
||||
size_variance: f32,
|
||||
angle_variance: f32,
|
||||
) -> iced::widget::image::Handle {
|
||||
// Clamp the rendered brush size so a 110 px soft brush does not fill the
|
||||
// entire 40 px preview well with a solid blob. The preview shows texture and
|
||||
@@ -588,7 +597,16 @@ fn get_live_stroke_preview(
|
||||
if let Some(handle) = cache.get(&key) {
|
||||
return handle.clone();
|
||||
}
|
||||
let tip = build_tip_from_state(style, preview_size, opacity, hardness, cyclic_color);
|
||||
let tip = build_tip_from_state(
|
||||
style,
|
||||
preview_size,
|
||||
opacity,
|
||||
hardness,
|
||||
cyclic_color,
|
||||
angle,
|
||||
size_variance,
|
||||
angle_variance,
|
||||
);
|
||||
let pixels =
|
||||
render_stroke_preview(LIVE_PREVIEW_IMG_SIZE, LIVE_PREVIEW_IMG_SIZE, &tip, fg_color);
|
||||
let handle = iced::widget::image::Handle::from_rgba(
|
||||
@@ -656,6 +674,9 @@ pub fn view(
|
||||
brush_variant_amount: f32,
|
||||
brush_cyclic_color: bool,
|
||||
brush_cyclic_speed: f32,
|
||||
brush_angle: f32,
|
||||
brush_size_variance: f32,
|
||||
brush_angle_variance: f32,
|
||||
active_category: BrushCategory,
|
||||
imported_presets: &[hcie_engine_api::BrushPreset],
|
||||
active_brush_preset: Option<&hcie_engine_api::BrushPreset>,
|
||||
@@ -1051,6 +1072,50 @@ pub fn view(
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
);
|
||||
|
||||
let is_star = current_style == BrushStyle::Star;
|
||||
let star_angle_slider = if is_star {
|
||||
Some(plain_slider(
|
||||
"Star Angle",
|
||||
brush_angle,
|
||||
0.0..=std::f32::consts::TAU,
|
||||
0.01,
|
||||
"rad",
|
||||
2,
|
||||
colors,
|
||||
Message::BrushAngleChanged,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let star_size_var_slider = if is_star {
|
||||
Some(plain_slider(
|
||||
"Size Variance",
|
||||
brush_size_variance,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
colors,
|
||||
Message::BrushSizeVarianceChanged,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let star_angle_var_slider = if is_star {
|
||||
Some(plain_slider(
|
||||
"Angle Variance",
|
||||
brush_angle_variance,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
colors,
|
||||
Message::BrushAngleVarianceChanged,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Compact color-variant toggle: an empty 12 px checkbox plus an 8 px label so
|
||||
// the control matches the rest of the compact panel typography.
|
||||
let color_variant_check = row![
|
||||
@@ -1082,6 +1147,9 @@ pub fn view(
|
||||
current_hardness,
|
||||
brush_cyclic_color,
|
||||
fg_color,
|
||||
brush_angle,
|
||||
brush_size_variance,
|
||||
brush_angle_variance,
|
||||
);
|
||||
let live_preview = container(
|
||||
iced::widget::image(preview_handle)
|
||||
@@ -1095,12 +1163,16 @@ pub fn view(
|
||||
.style(move |_theme| styles::recessed_control(colors));
|
||||
|
||||
// Panel title is in the dock tab — no duplicate inside the panel.
|
||||
let panel = column![
|
||||
tabs,
|
||||
horizontal_rule(1),
|
||||
let mut panel_children: Vec<Element<'static, Message>> = vec![
|
||||
tabs.into(),
|
||||
horizontal_rule(1).into(),
|
||||
scrollable(column![grid_rows, wc_rows, media_rows, imported_rows].spacing(GRID_SPACING))
|
||||
.height(Length::Fill),
|
||||
horizontal_rule(1),
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
horizontal_rule(1).into(),
|
||||
];
|
||||
|
||||
panel_children.push(
|
||||
row![button(text("Import ABR").size(LABEL_SIZE))
|
||||
.on_press(Message::BrushImportAbr)
|
||||
.padding([3, 7])
|
||||
@@ -1122,19 +1194,31 @@ pub fn view(
|
||||
}
|
||||
}
|
||||
)]
|
||||
.spacing(GRID_SPACING),
|
||||
text("Preview").size(LABEL_SIZE + 1),
|
||||
live_preview,
|
||||
size_slider,
|
||||
opacity_slider,
|
||||
hardness_slider,
|
||||
color_variant_check,
|
||||
variant_slider,
|
||||
cyclic_color_check,
|
||||
cyclic_speed_slider,
|
||||
]
|
||||
.spacing(ITEM_SPACING)
|
||||
.padding(3);
|
||||
.spacing(GRID_SPACING)
|
||||
.into(),
|
||||
);
|
||||
panel_children.push(text("Preview").size(LABEL_SIZE + 1).into());
|
||||
panel_children.push(live_preview.into());
|
||||
panel_children.push(size_slider.into());
|
||||
panel_children.push(opacity_slider.into());
|
||||
panel_children.push(hardness_slider.into());
|
||||
panel_children.push(color_variant_check.into());
|
||||
panel_children.push(variant_slider.into());
|
||||
panel_children.push(cyclic_color_check.into());
|
||||
panel_children.push(cyclic_speed_slider.into());
|
||||
if let Some(s) = star_angle_slider {
|
||||
panel_children.push(s.into());
|
||||
}
|
||||
if let Some(s) = star_size_var_slider {
|
||||
panel_children.push(s.into());
|
||||
}
|
||||
if let Some(s) = star_angle_var_slider {
|
||||
panel_children.push(s.into());
|
||||
}
|
||||
|
||||
let panel = iced::widget::Column::with_children(panel_children)
|
||||
.spacing(ITEM_SPACING)
|
||||
.padding(3);
|
||||
|
||||
container(panel)
|
||||
.width(Length::Fill)
|
||||
@@ -1181,7 +1265,7 @@ mod tests {
|
||||
presets.extend(hcie_paint_brushes::paint_presets());
|
||||
presets.extend(hcie_digital_brushes::digital_presets());
|
||||
presets.extend(hcie_watercolor_brushes::watercolor_presets());
|
||||
assert_eq!(presets.len(), 47);
|
||||
assert_eq!(presets.len(), 48);
|
||||
assert_eq!(
|
||||
presets
|
||||
.iter()
|
||||
|
||||
@@ -59,6 +59,15 @@ pub struct ToolSettings {
|
||||
/// Magnitude of the per-dab color variation (0.0..1.0).
|
||||
#[serde(default)]
|
||||
pub brush_variant_amount: f32,
|
||||
/// Star brush base rotation angle in radians.
|
||||
#[serde(default)]
|
||||
pub brush_angle: f32,
|
||||
/// Star brush per-dab size variance (0.0..1.0).
|
||||
#[serde(default)]
|
||||
pub brush_size_variance: f32,
|
||||
/// Star brush per-dab angle variance (0.0..1.0).
|
||||
#[serde(default)]
|
||||
pub brush_angle_variance: f32,
|
||||
/// True when the brush color should cycle through the hue over stroke distance.
|
||||
#[serde(default)]
|
||||
pub brush_cyclic_color: bool,
|
||||
@@ -198,6 +207,9 @@ impl Default for ToolSettings {
|
||||
brush_spacing: 1.0,
|
||||
brush_color_variant: false,
|
||||
brush_variant_amount: 0.0,
|
||||
brush_angle: 0.0,
|
||||
brush_size_variance: 0.0,
|
||||
brush_angle_variance: 0.0,
|
||||
brush_cyclic_color: false,
|
||||
brush_cyclic_speed: 0.5,
|
||||
eraser_size: 20.0,
|
||||
@@ -352,6 +364,9 @@ impl AppSettings {
|
||||
self.tool_settings.brush_hardness = tool_state.brush_hardness;
|
||||
self.tool_settings.brush_color_variant = tool_state.brush_color_variant;
|
||||
self.tool_settings.brush_variant_amount = tool_state.brush_variant_amount;
|
||||
self.tool_settings.brush_angle = tool_state.brush_angle;
|
||||
self.tool_settings.brush_size_variance = tool_state.brush_size_variance;
|
||||
self.tool_settings.brush_angle_variance = tool_state.brush_angle_variance;
|
||||
self.tool_settings.brush_cyclic_color = tool_state.brush_cyclic_color;
|
||||
self.tool_settings.brush_cyclic_speed = tool_state.brush_cyclic_speed;
|
||||
self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool);
|
||||
@@ -364,6 +379,9 @@ impl AppSettings {
|
||||
tool_state.brush_hardness = self.tool_settings.brush_hardness;
|
||||
tool_state.brush_color_variant = self.tool_settings.brush_color_variant;
|
||||
tool_state.brush_variant_amount = self.tool_settings.brush_variant_amount;
|
||||
tool_state.brush_angle = self.tool_settings.brush_angle;
|
||||
tool_state.brush_size_variance = self.tool_settings.brush_size_variance;
|
||||
tool_state.brush_angle_variance = self.tool_settings.brush_angle_variance;
|
||||
tool_state.brush_cyclic_color = self.tool_settings.brush_cyclic_color;
|
||||
tool_state.brush_cyclic_speed = self.tool_settings.brush_cyclic_speed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user