feat: introduce SVG-based complex vector shapes
- Added `SvgShape` variant to `VectorShape` in `hcie-protocol`, allowing for SVG rendering of complex shapes. - Updated serialization to skip old shape variants for backward compatibility. - Implemented SVG generation functions in `svg_templates.rs` for various shapes (arrow, star, rhombus, etc.). - Created `svg_render.rs` to parse SVG strings and tessellate paths using `usvg`. - Modified rendering logic in `hcie-vector` to handle `SvgShape` and integrate with existing shape rendering. - Updated shape creation API in `hcie-engine-api` to support SVG shapes. - Enhanced property editing and shape synchronization in both `hcie-egui-app` and `hcie-iced-app` to accommodate new SVG shapes. - Added comprehensive documentation for new features and changes.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# SVG-based Complex Vector Shapes
|
||||
|
||||
## Goal
|
||||
Replace hard-coded vertex generation for complex `VectorShape` variants with SVG strings rendered at runtime via `usvg`. Simple primitives (Line, Rect, Circle, Polygon, FreePath) stay as-is.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Choice |
|
||||
|----------|--------|
|
||||
| SVG coordinate system | `viewBox="0 0 1 1"` (normalized). Resize only updates x1,y1,x2,y2 — SVG never regenerated. |
|
||||
| Shape kind field | `kind: String` in SvgShape ("arrow", "star", etc.) for property panel controls. |
|
||||
| Backward compat | Keep old variants with `#[serde(skip_serializing)]`. Deserialized old variants stay as-is in memory but convert to SvgShape on first user edit. On serialize, only SvgShape writes out. |
|
||||
| SVG path parser | `usvg` 0.43 (already workspace dep) — parse SVG, extract path segments, tessellate curves, scale to bounding box. |
|
||||
| Both GUIs | egui + iced both updated. |
|
||||
|
||||
## Changes (17 files, 5 crates)
|
||||
|
||||
---
|
||||
|
||||
### 1. `hcie-protocol/src/lib.rs` — Add `SvgShape` variant
|
||||
|
||||
```rust
|
||||
SvgShape {
|
||||
x1: f32, y1: f32, x2: f32, y2: f32,
|
||||
kind: String, // "arrow", "star", "rhombus", "cylinder", "heart", "bubble", "gear", "cross", "crescent", "bolt", "arrow4"
|
||||
svg: String, // SVG with viewBox="0 0 1 1", path data in normalized coords
|
||||
stroke: f32,
|
||||
color: [u8; 4],
|
||||
fill_color: [u8; 4],
|
||||
fill: bool,
|
||||
angle: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
}
|
||||
```
|
||||
|
||||
Old variants (Arrow, Star, Rhombus, Cylinder, Heart, Bubble, Gear, Cross, Crescent, Bolt, Arrow4) keep `#[serde(skip_serializing)]` — they deserialize fine for backward compat but never serialize.
|
||||
|
||||
Update every exhaustive match arm in `impl VectorShape`:
|
||||
- `bounds()` — add `SvgShape { x1,y1,x2,y2,.. }`
|
||||
- `normalized_bounds()` — no change, delegates to bounds
|
||||
- `color()` / `set_color()`
|
||||
- `shape_opacity()` / `set_shape_opacity()`
|
||||
- `hardness()` / `set_hardness()`
|
||||
- `stroke()` / `set_stroke()` / `get_stroke_mut()`
|
||||
- `angle()` / `set_angle()` / `get_angle_mut()` / `rotate()`
|
||||
- `is_filled()` / `fill_color()` / `fill()` / `get_fill_mut()` / `get_fill_color_mut()`
|
||||
- `coords_mut()`
|
||||
- `set_bounds()` — SvgShape updates x1,y1,x2,y2 like other non-FreePath variants (no SVG regeneration needed since viewBox is normalized)
|
||||
|
||||
---
|
||||
|
||||
### 2. `hcie-vector/Cargo.toml` — Add dependency
|
||||
|
||||
```toml
|
||||
usvg = { workspace = true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `hcie-vector/src/svg_templates.rs` — NEW: SVG template functions
|
||||
|
||||
11 functions, each returns an SVG string with `viewBox="0 0 1 1"`:
|
||||
|
||||
```rust
|
||||
pub fn arrow_svg(kind: &str, thick: bool) -> String;
|
||||
pub fn star_svg(kind: &str, points: u32, inner_radius: f32) -> String;
|
||||
pub fn rhombus_svg(kind: &str) -> String;
|
||||
pub fn cylinder_svg(kind: &str) -> String;
|
||||
pub fn heart_svg(kind: &str) -> String;
|
||||
pub fn bubble_svg(kind: &str) -> String;
|
||||
pub fn gear_svg(kind: &str) -> String;
|
||||
pub fn cross_svg(kind: &str) -> String;
|
||||
pub fn crescent_svg(kind: &str) -> String;
|
||||
pub fn bolt_svg(kind: &str) -> String;
|
||||
pub fn arrow4_svg(kind: &str) -> String;
|
||||
```
|
||||
|
||||
Each generates SVG path data mathematically equivalent to the current Rust vertex code. For example, `arrow_svg` (non-thick):
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1">
|
||||
<path d="M 0,1 L 1,0.5 L 0,0 L 0.2,0.5 Z"/>
|
||||
</svg>
|
||||
```
|
||||
(`arrow4_svg` has 4 arrowheads → returns `<path d="..."/>` with the full 12-point cross+arrowhead polygon.)
|
||||
|
||||
Also add a registry function:
|
||||
```rust
|
||||
pub fn create_svg(kind: &str, params: &HashMap<String, f32>) -> String {
|
||||
match kind {
|
||||
"arrow" => arrow_svg(kind, params.get("thick").copied().unwrap_or(0.0) > 0.5),
|
||||
"star" => star_svg(kind, params.get("points").copied().unwrap_or(5.0) as u32, params.get("inner_radius").copied().unwrap_or(0.5)),
|
||||
// ... etc
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `hcie-vector/src/svg_render.rs` — NEW: SVG path tessellator
|
||||
|
||||
Uses `usvg` to parse SVG string, extract path segments, tessellate curves, scale to bounding box:
|
||||
|
||||
```rust
|
||||
/// Parse SVG string, extract all path polygons, scale to fit (x1,y1)-(x2,y2).
|
||||
/// Returns one polygon per subpath.
|
||||
pub fn svg_to_points(svg: &str, x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Vec<Vec<(f32, f32)>>, String>;
|
||||
```
|
||||
|
||||
Implementation:
|
||||
1. `usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default())`
|
||||
2. Traverse `tree.root()` children, collect `usvg::Node::Path` nodes
|
||||
3. For each path:
|
||||
- Get `path.data().segments()`
|
||||
- Accumulate points per subpath (separate Vec per `MoveTo` after `Close`)
|
||||
- Tessellate `CubicTo` and `QuadTo` via cubic/quad bezier splitting (use `hcie-io/src/svg_import.rs` pattern)
|
||||
- Apply viewBox → bounding-box scaling: since viewBox is `0 0 1 1`, map point (px,py) → (x1 + px*(x2-x1), y1 + py*(y2-y1))
|
||||
4. Return `Vec<Vec<(f32,f32)>>`
|
||||
|
||||
Edge cases:
|
||||
- Empty SVG → return empty vec
|
||||
- No `<path>` elements → return empty vec
|
||||
- Invalid SVG → return `Err` with usvg error message
|
||||
|
||||
---
|
||||
|
||||
### 5. `hcie-vector/src/lib.rs` — Update render entry points
|
||||
|
||||
Add modules:
|
||||
```rust
|
||||
pub mod svg_render;
|
||||
```
|
||||
|
||||
Update `render_shape` match:
|
||||
```rust
|
||||
SvgShape { svg, x1, y1, x2, y2, fill, color, fill_color, stroke, opacity, hardness, .. } => {
|
||||
let points_list = svg_render::svg_to_points(svg, *x1, *y1, *x2, *y2).unwrap_or_default();
|
||||
let alpha = (color[3] as f32 * opacity).round() as u8;
|
||||
let px_color = [color[0], color[1], color[2], alpha];
|
||||
if *fill {
|
||||
let fill_alpha = (fill_color[3] as f32 * opacity).round() as u8;
|
||||
let px_fill = [fill_color[0], fill_color[1], fill_color[2], fill_alpha];
|
||||
for poly in &points_list { fill_polygon(layer, poly, px_fill); }
|
||||
}
|
||||
for poly in &points_list {
|
||||
for i in 0..poly.len() {
|
||||
let j = (i + 1) % poly.len();
|
||||
draw_line(layer, poly[i].0, poly[i].1, poly[j].0, poly[j].1, px_color, *stroke, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `get_shape_hardness` — add `SvgShape { hardness, .. }` arm.
|
||||
|
||||
Update `rotate_shape` / `set_shape_angle` — add `SvgShape { angle, .. }` arm.
|
||||
|
||||
---
|
||||
|
||||
### 6. `hcie-vector/examples/vector_test.rs` — Update creation
|
||||
|
||||
Replace old variant construction with SvgShape, calling `svg_templates::create_svg()` and wrapping in `VectorShape::SvgShape { ... }`.
|
||||
|
||||
---
|
||||
|
||||
### 7. `hcie-engine-api/src/lib.rs` — Add shape creation API
|
||||
|
||||
```rust
|
||||
pub fn create_vector_shape(kind: &str, x1: f32, y1: f32, x2: f32, y2: f32, params: &HashMap<String, f32>) -> VectorShape;
|
||||
```
|
||||
|
||||
Add `hcie-vector = { path = "../hcie-vector" }` dependency.
|
||||
|
||||
Implementation: call `hcie_vector::svg_templates::create_svg(kind, params)`, return `VectorShape::SvgShape { svg, kind, x1, y1, x2, y2, stroke: 2.0, color: [0,0,0,255], fill_color: [255,255,255,255], fill: true, angle: 0.0, opacity: 1.0, hardness: 1.0 }`.
|
||||
|
||||
---
|
||||
|
||||
### 8. `hcie-egui-app/…/canvas/mod.rs` — Update shape creation (~lines 2179–2379)
|
||||
|
||||
Each `Tool::VectorXxx` arm:
|
||||
```
|
||||
Tool::VectorArrow => {
|
||||
let params = HashMap::from([("thick".into(), if thick { 1.0 } else { 0.0 })]);
|
||||
Some(engine.create_vector_shape("arrow", x1, y1, x2, y2, ¶ms))
|
||||
}
|
||||
Tool::VectorStar => { /* params: points, inner_radius */ }
|
||||
Tool::VectorRhombus => { /* no params */ }
|
||||
// ... etc for all 11 complex shapes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. `hcie-egui-app/…/canvas/render.rs` — `get_shape_key_points` SvgShape arm
|
||||
|
||||
Add:
|
||||
```rust
|
||||
VectorShape::SvgShape { x1, y1, x2, y2, .. } => {
|
||||
let r = normalized_rect(*x1, *y1, *x2, *y2);
|
||||
vec![(r.0, r.1), (r.2, r.1), (r.2, r.3), (r.0, r.3)]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. `hcie-egui-app/…/app/mod.rs` — Property editing
|
||||
|
||||
In the property editing match (~line 4151 area), add `SvgShape { kind, svg, fill, fill_color, .. }` arm. When user changes star points/inner_radius (via kind-specific UI), regenerate SVG via `create_vector_shape` and update the `svg` field.
|
||||
|
||||
---
|
||||
|
||||
### 11. `hcie-egui-app/…/tools/geometry_panel.rs` — Labels + kind UI
|
||||
|
||||
Add `SvgShape { kind, .. }` to the label matching and shape-type dropdown. Show kind-specific controls (e.g., points slider for "star", thick toggle for "arrow").
|
||||
|
||||
---
|
||||
|
||||
### 12. `hcie-egui-app/…/tools/shape_sync.rs` — Param sync
|
||||
|
||||
Handle `SvgShape` in the sync match arms: read/write `kind` field, regenerate SVG on param changes.
|
||||
|
||||
---
|
||||
|
||||
### 13-17. `hcie-iced-app/…` — Mirror of steps 8-12
|
||||
|
||||
Same pattern in the iced GUI crate:
|
||||
- `app.rs` — shape creation
|
||||
- `panels/properties.rs` — labels
|
||||
- `panels/geometry.rs` — labels + editing
|
||||
- `shape_sync.rs` — param sync
|
||||
|
||||
---
|
||||
|
||||
### 18. `hcie-io/src/svg_import.rs` — No change needed
|
||||
|
||||
Already uses `usvg` correctly. SVG files imported via `import_svg()` already convert to `FreePath` — fine as-is.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
# Compile all crates
|
||||
cargo check -p hcie-protocol -p hcie-vector -p hcie-engine-api -p hcie-gui-egui -p hcie-iced-gui
|
||||
|
||||
# Run vector engine tests
|
||||
cargo test -p hcie-vector --lib
|
||||
cargo test -p hcie-vector --examples
|
||||
|
||||
# Run all tests
|
||||
cargo test -p hcie-vector -p hcie-protocol -p hcie-engine-api
|
||||
```
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| SVG path differs from old vertex code visually | Compare screenshots before/after for each shape kind |
|
||||
| Old documents fail to load | Old variants still deserialize (kept with `#[serde(skip_serializing)]`), stay as old enum variants in memory |
|
||||
| Performance regression from usvg parsing at render time | usvg parse is fast (<0.1ms for our tiny SVGs). Cache can be added later if needed |
|
||||
| Bezier tessellation quality differs | Use same tessellation density as current code (8 steps per curve quad, matching existing) |
|
||||
Reference in New Issue
Block a user