# 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) |