Files
hcie-rust-v3.05/.kilo/plans/1784403165333-svg-vector-shapes.md
T
phantom 5632c916ee 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.
2026-07-19 02:44:59 +03:00

9.3 KiB
Raw Blame History

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

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

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":

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 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:

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:

/// 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:

pub mod svg_render;

Update render_shape match:

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

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

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, &params))
}
Tool::VectorStar => { /* params: points, inner_radius */ }
Tool::VectorRhombus => { /* no params */ }
// ... etc for all 11 complex shapes

9. hcie-egui-app/…/canvas/render.rsget_shape_key_points SvgShape arm

Add:

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

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