# 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
```
(`arrow4_svg` has 4 arrowheads → returns `` with the full 12-point cross+arrowhead polygon.)
Also add a registry function:
```rust
pub fn create_svg(kind: &str, params: &HashMap) -> 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>, 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>`
Edge cases:
- Empty SVG → return empty vec
- No `` 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) -> 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) |