- 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.
9.3 KiB
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()— addSvgShape { x1,y1,x2,y2,.. }normalized_bounds()— no change, delegates to boundscolor()/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:
usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default())- Traverse
tree.root()children, collectusvg::Node::Pathnodes - For each path:
- Get
path.data().segments() - Accumulate points per subpath (separate Vec per
MoveToafterClose) - Tessellate
CubicToandQuadTovia cubic/quad bezier splitting (usehcie-io/src/svg_import.rspattern) - Apply viewBox → bounding-box scaling: since viewBox is
0 0 1 1, map point (px,py) → (x1 + px*(x2-x1), y1 + py*(y2-y1))
- Get
- Return
Vec<Vec<(f32,f32)>>
Edge cases:
- Empty SVG → return empty vec
- No
<path>elements → return empty vec - Invalid SVG → return
Errwith 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 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:
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 creationpanels/properties.rs— labelspanels/geometry.rs— labels + editingshape_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) |