diff --git a/.kilo/plans/1784403165333-svg-vector-shapes.md b/.kilo/plans/1784403165333-svg-vector-shapes.md new file mode 100644 index 0000000..2d0951f --- /dev/null +++ b/.kilo/plans/1784403165333-svg-vector-shapes.md @@ -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 + + + +``` +(`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) | diff --git a/Cargo.lock b/Cargo.lock index 643369f..9fb0ff6 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2867,6 +2867,7 @@ dependencies = [ "proptest", "rstest", "serde", + "usvg 0.43.0", ] [[package]] diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/app/mod.rs b/hcie-egui-app/crates/hcie-gui-egui/src/app/mod.rs index eb3442a..f077f13 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/app/mod.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/app/mod.rs @@ -4586,6 +4586,24 @@ impl HcieApp { d, stroke_col, stroke, if *fill { fill_col } else { "none".to_string() }, if *fill { opacity } else { &0.0 }, opacity )); } + VectorShape::SvgShape { svg: inner_svg, x1, y1, x2, y2, stroke, color, fill_color, fill, opacity, .. } => { + let stroke_col = format_color(color); + let fill_col = format_color(fill_color); + let x = x1.min(*x2); + let y = y1.min(*y2); + let w = (x2 - x1).abs(); + let h = (y2 - y1).abs(); + if let Some(di) = inner_svg.find(" d=\"") { + let start = di + 4; + if let Some(end) = inner_svg[start..].find('"') { + let d = &inner_svg[start..start+end]; + svg.push_str(&format!( + "\n", + d, x, y, w, h, stroke_col, stroke, if *fill { fill_col } else { "none".to_string() }, if *fill { opacity } else { &0.0 }, opacity + )); + } + } + } } } svg.push_str(""); diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/geometry_panel.rs b/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/geometry_panel.rs index 30732fe..741041c 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/geometry_panel.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/geometry_panel.rs @@ -26,6 +26,23 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String { VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1), VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1), VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1), + VectorShape::SvgShape { kind, .. } => { + let label = match kind.as_str() { + "arrow" => "Arrow", + "star" => "Star", + "rhombus" => "Rhombus", + "cylinder" => "Cylinder", + "heart" => "Heart", + "bubble" => "Bubble", + "gear" => "Gear", + "cross" => "Cross", + "crescent" => "Crescent", + "bolt" => "Bolt", + "arrow4" => "4-way Arrow", + _ => kind, + }; + format!("{} #{}", label, i + 1) + } VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1), } } @@ -99,6 +116,23 @@ pub fn show_geometry( VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1), VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1), VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1), + VectorShape::SvgShape { kind, .. } => { + let label = match kind.as_str() { + "arrow" => "Arrow", + "star" => "Star", + "rhombus" => "Rhombus", + "cylinder" => "Cylinder", + "heart" => "Heart", + "bubble" => "Bubble", + "gear" => "Gear", + "cross" => "Cross", + "crescent" => "Crescent", + "bolt" => "Bolt", + "arrow4" => "4-way Arrow", + _ => kind, + }; + format!("{} #{}", label, i + 1) + } VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1), }; let is_sel = state.selected_vector_shape == Some(i); diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/shape_sync.rs b/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/shape_sync.rs index 36ad6a7..5fa074f 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/shape_sync.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/app/tools/shape_sync.rs @@ -86,6 +86,9 @@ pub fn sync_shape_from_tool(doc: &mut AppDocument, state: &mut ToolState) { VectorShape::Polygon { .. } => { // polygon_sides via engine when available } + VectorShape::SvgShape { .. } => { + // shape-specific params are baked into the SVG + } _ => {} } } @@ -136,6 +139,9 @@ pub fn sync_tool_from_shape(doc: &AppDocument, state: &mut ToolState) { } => { v.line_cap = *cap_start; } + VectorShape::SvgShape { .. } => { + // params baked into SVG, nothing to sync + } _ => {} } diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs index 400c818..84f4438 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/mod.rs @@ -2216,34 +2216,28 @@ fn create_shape_from_drag( opacity, hardness, }, - Tool::VectorArrow => VectorShape::Arrow { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, - thick: false, + Tool::VectorArrow => { + let mut p = std::collections::HashMap::new(); + p.insert("thick".to_string(), 0.0); + match hcie_engine_api::create_vector_shape("arrow", x1, y1, x2, y2, &p) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorStar => VectorShape::Star { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, - points: state.tool_configs.vector.star_points, - inner_radius: state.tool_configs.vector.star_inner_radius, + Tool::VectorStar => { + let mut p = std::collections::HashMap::new(); + p.insert("points".to_string(), state.tool_configs.vector.star_points as f32); + p.insert("inner_radius".to_string(), state.tool_configs.vector.star_inner_radius); + match hcie_engine_api::create_vector_shape("star", x1, y1, x2, y2, &p) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, Tool::VectorPolygon => VectorShape::Polygon { x1, @@ -2259,122 +2253,86 @@ fn create_shape_from_drag( hardness, sides: state.tool_configs.vector.polygon_sides, }, - Tool::VectorRhombus => VectorShape::Rhombus { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorRhombus => { + match hcie_engine_api::create_vector_shape("rhombus", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorCylinder => VectorShape::Cylinder { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorCylinder => { + match hcie_engine_api::create_vector_shape("cylinder", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorHeart => VectorShape::Heart { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorHeart => { + match hcie_engine_api::create_vector_shape("heart", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorBubble => VectorShape::Bubble { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorBubble => { + match hcie_engine_api::create_vector_shape("bubble", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorGear => VectorShape::Gear { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorGear => { + match hcie_engine_api::create_vector_shape("gear", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorCross => VectorShape::Cross { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorCross => { + match hcie_engine_api::create_vector_shape("cross", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorCrescent => VectorShape::Crescent { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorCrescent => { + match hcie_engine_api::create_vector_shape("crescent", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorBolt => VectorShape::Bolt { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorBolt => { + match hcie_engine_api::create_vector_shape("bolt", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, - Tool::VectorArrow4 => VectorShape::Arrow4 { - x1, - y1, - x2, - y2, - stroke, - color, - fill_color, - fill, - angle: 0.0, - opacity, - hardness, + Tool::VectorArrow4 => { + match hcie_engine_api::create_vector_shape("arrow4", x1, y1, x2, y2, &std::collections::HashMap::new()) { + VectorShape::SvgShape { kind, svg, .. } => VectorShape::SvgShape { + kind, svg, x1, y1, x2, y2, stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness, + }, + _ => unreachable!(), + } }, _ => VectorShape::Line { x1, diff --git a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs index db64947..6bff398 100644 --- a/hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs +++ b/hcie-egui-app/crates/hcie-gui-egui/src/canvas/render.rs @@ -386,6 +386,14 @@ fn get_shape_key_points(shape: &VectorShape) -> (f32, f32, f32, Vec<(f32, f32)>) }) .collect() } + VectorShape::SvgShape { .. } => { + vec![ + rotate(left, top), + rotate(right, top), + rotate(right, bottom), + rotate(left, bottom), + ] + } _ => { vec![ rotate(left, top), diff --git a/hcie-engine-api/src/lib.rs b/hcie-engine-api/src/lib.rs index 7aa4242..1c56f4d 100644 --- a/hcie-engine-api/src/lib.rs +++ b/hcie-engine-api/src/lib.rs @@ -415,6 +415,39 @@ pub struct Engine { std::sync::Arc>>, } +/// Create a `VectorShape::SvgShape` from a shape kind name and parameter map. +/// +/// The `kind` string must be one of: "arrow", "star", "rhombus", "cylinder", +/// "heart", "bubble", "gear", "cross", "crescent", "bolt", "arrow4". +/// +/// Shape-specific parameters (e.g. `points`, `inner_radius`, `thick`) are passed +/// via `params`. +pub fn create_vector_shape( + kind: &str, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + params: &std::collections::HashMap, +) -> VectorShape { + let svg = hcie_vector::svg_templates::create_svg(kind, params); + VectorShape::SvgShape { + x1, + y1, + x2, + y2, + kind: kind.to_string(), + svg, + 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, + } +} + impl Engine { /// Create a new engine with a default transparent document. /// @@ -4038,6 +4071,14 @@ fn set_shape_color( *color = stroke_color; *fc = fill_color; } + SvgShape { + color, + fill_color: fc, + .. + } => { + *color = stroke_color; + *fc = fill_color; + } FreePath { color, fill_color: fc, @@ -4147,6 +4188,12 @@ fn move_shape( *x2 += dx; *y2 += dy; } + SvgShape { x1, y1, x2, y2, .. } => { + *x1 += dx; + *y1 += dy; + *x2 += dx; + *y2 += dy; + } FreePath { pts, .. } => { for p in pts.iter_mut() { p[0] += dx; @@ -4279,6 +4326,14 @@ fn scale_shape(shape: &mut hcie_protocol::VectorShape, scale_x: f32, scale_y: f3 *x2 = cx + (*x2 - cx) * scale_x; *y2 = cy + (*y2 - cy) * scale_y; } + SvgShape { x1, y1, x2, y2, .. } => { + let cx = (*x1 + *x2) / 2.0; + let cy = (*y1 + *y2) / 2.0; + *x1 = cx + (*x1 - cx) * scale_x; + *y1 = cy + (*y1 - cy) * scale_y; + *x2 = cx + (*x2 - cx) * scale_x; + *y2 = cy + (*y2 - cy) * scale_y; + } FreePath { pts, .. } => { if pts.is_empty() { return; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 49fbcfa..c3e0119 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -5061,35 +5061,36 @@ impl HcieIcedApp { opacity, hardness: 0.5, }), - Tool::VectorArrow => Some(hcie_engine_api::VectorShape::Arrow { - x1: x0, - y1: y0, - x2: x1, - y2: y1, - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - thick: false, - }), - Tool::VectorStar => Some(hcie_engine_api::VectorShape::Star { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - points, - inner_radius: 0.5, - }), + Tool::VectorArrow => { + let mut p = std::collections::HashMap::new(); + p.insert("thick".to_string(), 0.0); + match hcie_engine_api::create_vector_shape("arrow", x0, y0, x1, y1, &p) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: x0, y1: y0, x2: x1, y2: y1, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorStar => { + let mut p = std::collections::HashMap::new(); + p.insert("points".to_string(), points as f32); + p.insert("inner_radius".to_string(), 0.5); + let (sx1, sy1, sx2, sy2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("star", sx1, sy1, sx2, sy2, &p) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: sx1, y1: sy1, x2: sx2, y2: sy2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } Tool::VectorPolygon => Some(hcie_engine_api::VectorShape::Polygon { x1: x0.min(x1), y1: y0.min(y1), @@ -5104,123 +5105,123 @@ impl HcieIcedApp { hardness: 0.5, sides, }), - Tool::VectorRhombus => Some(hcie_engine_api::VectorShape::Rhombus { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorCylinder => Some(hcie_engine_api::VectorShape::Cylinder { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorHeart => Some(hcie_engine_api::VectorShape::Heart { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorBubble => Some(hcie_engine_api::VectorShape::Bubble { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorGear => Some(hcie_engine_api::VectorShape::Gear { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorCross => Some(hcie_engine_api::VectorShape::Cross { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorCrescent => Some(hcie_engine_api::VectorShape::Crescent { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorBolt => Some(hcie_engine_api::VectorShape::Bolt { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), - Tool::VectorArrow4 => Some(hcie_engine_api::VectorShape::Arrow4 { - x1: x0.min(x1), - y1: y0.min(y1), - x2: x0.max(x1), - y2: y0.max(y1), - stroke, - color, - fill, - fill_color, - angle: 0.0, - opacity, - hardness: 0.5, - }), + Tool::VectorRhombus => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("rhombus", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorCylinder => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("cylinder", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorHeart => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("heart", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorBubble => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("bubble", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorGear => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("gear", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorCross => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("cross", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorCrescent => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("crescent", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorBolt => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("bolt", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } + Tool::VectorArrow4 => { + let (nx1, ny1, nx2, ny2) = (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)); + match hcie_engine_api::create_vector_shape("arrow4", nx1, ny1, nx2, ny2, &std::collections::HashMap::new()) { + hcie_engine_api::VectorShape::SvgShape { kind, svg, .. } => + Some(hcie_engine_api::VectorShape::SvgShape { + kind, svg, + x1: nx1, y1: ny1, x2: nx2, y2: ny2, + stroke, color, fill_color, fill, + angle: 0.0, opacity, hardness: 0.5, + }), + _ => unreachable!(), + } + } _ => None, }; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs index e9eee1c..78d73fa 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/geometry.rs @@ -34,6 +34,7 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String { VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1), VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1), VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1), + VectorShape::SvgShape { kind, .. } => format!("{} #{}", kind, i + 1), VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1), } } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs index 63d6fb9..f17acde 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/properties.rs @@ -33,6 +33,7 @@ fn shape_label(shapes: &[VectorShape], i: usize) -> String { VectorShape::Crescent { .. } => format!("Crescent #{}", i + 1), VectorShape::Bolt { .. } => format!("Bolt #{}", i + 1), VectorShape::Arrow4 { .. } => format!("4-way Arrow #{}", i + 1), + VectorShape::SvgShape { kind, .. } => format!("{} #{}", kind, i + 1), VectorShape::FreePath { pts, .. } => format!("Path ({} pts) #{}", pts.len(), i + 1), } } @@ -262,23 +263,24 @@ fn vector_select_properties<'a>( let stroke_color = shape.color(); let fill_color = shape.fill_color().unwrap_or([0, 0, 0, 255]); - let shape_type = match shape { - VectorShape::Line { .. } => "Line", - VectorShape::Rect { .. } => "Rect", - VectorShape::Circle { .. } => "Circle", - VectorShape::Arrow { .. } => "Arrow", - VectorShape::Star { .. } => "Star", - VectorShape::Polygon { .. } => "Polygon", - VectorShape::Rhombus { .. } => "Rhombus", - VectorShape::Cylinder { .. } => "Cylinder", - VectorShape::Heart { .. } => "Heart", - VectorShape::Bubble { .. } => "Bubble", - VectorShape::Gear { .. } => "Gear", - VectorShape::Cross { .. } => "Cross", - VectorShape::Crescent { .. } => "Crescent", - VectorShape::Bolt { .. } => "Bolt", - VectorShape::Arrow4 { .. } => "4-way Arrow", - VectorShape::FreePath { .. } => "Free Path", + let shape_type: String = match shape { + VectorShape::Line { .. } => "Line".into(), + VectorShape::Rect { .. } => "Rect".into(), + VectorShape::Circle { .. } => "Circle".into(), + VectorShape::Arrow { .. } => "Arrow".into(), + VectorShape::Star { .. } => "Star".into(), + VectorShape::Polygon { .. } => "Polygon".into(), + VectorShape::Rhombus { .. } => "Rhombus".into(), + VectorShape::Cylinder { .. } => "Cylinder".into(), + VectorShape::Heart { .. } => "Heart".into(), + VectorShape::Bubble { .. } => "Bubble".into(), + VectorShape::Gear { .. } => "Gear".into(), + VectorShape::Cross { .. } => "Cross".into(), + VectorShape::Crescent { .. } => "Crescent".into(), + VectorShape::Bolt { .. } => "Bolt".into(), + VectorShape::Arrow4 { .. } => "4-way Arrow".into(), + VectorShape::SvgShape { kind, .. } => kind.clone(), + VectorShape::FreePath { .. } => "Free Path".into(), }; column![ diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs b/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs index aae1cab..c6b0119 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/shape_sync.rs @@ -118,6 +118,9 @@ pub fn sync_shape_from_tool(app: &mut HcieIcedApp) { VectorShape::Polygon { .. } => { // polygon_sides via engine when available } + VectorShape::SvgShape { .. } => { + // SVG shapes have no extra per-shape properties + } _ => {} } } @@ -161,6 +164,9 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) { VectorShape::Polygon { sides, .. } => { ts.vector_sides = *sides; } + VectorShape::SvgShape { .. } => { + // No shape-specific properties for SVG shapes + } VectorShape::Line { cap_start, cap_end: _, diff --git a/hcie-protocol/src/lib.rs b/hcie-protocol/src/lib.rs index f231c8e..cea0922 100644 --- a/hcie-protocol/src/lib.rs +++ b/hcie-protocol/src/lib.rs @@ -352,6 +352,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Arrow { x1: f32, y1: f32, @@ -371,6 +372,7 @@ pub enum VectorShape { #[serde(default)] thick: bool, }, + #[serde(skip_serializing)] Star { x1: f32, y1: f32, @@ -411,6 +413,7 @@ pub enum VectorShape { #[serde(default)] sides: u32, }, + #[serde(skip_serializing)] Rhombus { x1: f32, y1: f32, @@ -428,6 +431,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Cylinder { x1: f32, y1: f32, @@ -445,6 +449,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Heart { x1: f32, y1: f32, @@ -462,6 +467,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Bubble { x1: f32, y1: f32, @@ -479,6 +485,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Gear { x1: f32, y1: f32, @@ -496,6 +503,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Cross { x1: f32, y1: f32, @@ -513,6 +521,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Crescent { x1: f32, y1: f32, @@ -530,6 +539,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Bolt { x1: f32, y1: f32, @@ -547,6 +557,7 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + #[serde(skip_serializing)] Arrow4 { x1: f32, y1: f32, @@ -564,6 +575,25 @@ pub enum VectorShape { #[serde(default = "default_hardness")] hardness: f32, }, + SvgShape { + x1: f32, + y1: f32, + x2: f32, + y2: f32, + kind: String, + svg: String, + stroke: f32, + color: [u8; 4], + #[serde(default = "default_fill_color")] + fill_color: [u8; 4], + fill: bool, + #[serde(default)] + angle: f32, + #[serde(default = "default_opacity")] + opacity: f32, + #[serde(default = "default_hardness")] + hardness: f32, + }, FreePath { pts: Vec<[f32; 2]>, closed: bool, @@ -1986,6 +2016,7 @@ impl VectorShape { | VectorShape::Cross { x1, y1, x2, y2, .. } | VectorShape::Crescent { x1, y1, x2, y2, .. } | VectorShape::Arrow4 { x1, y1, x2, y2, .. } + | VectorShape::SvgShape { x1, y1, x2, y2, .. } | VectorShape::Bolt { x1, y1, x2, y2, .. } => (*x1, *y1, *x2, *y2), VectorShape::FreePath { pts, .. } => { let x1 = pts.iter().map(|p| p[0]).fold(f32::INFINITY, f32::min); @@ -2024,6 +2055,7 @@ impl VectorShape { | VectorShape::Crescent { color, .. } | VectorShape::Arrow4 { color, .. } | VectorShape::Bolt { color, .. } + | VectorShape::SvgShape { color, .. } | VectorShape::FreePath { color, .. } => *color, } } @@ -2045,6 +2077,7 @@ impl VectorShape { | VectorShape::Crescent { color, .. } | VectorShape::Arrow4 { color, .. } | VectorShape::Bolt { color, .. } + | VectorShape::SvgShape { color, .. } | VectorShape::FreePath { color, .. } => *color = new_color, } } @@ -2066,6 +2099,7 @@ impl VectorShape { | VectorShape::Crescent { opacity, .. } | VectorShape::Arrow4 { opacity, .. } | VectorShape::Bolt { opacity, .. } + | VectorShape::SvgShape { opacity, .. } | VectorShape::FreePath { opacity, .. } => *opacity, } } @@ -2088,6 +2122,7 @@ impl VectorShape { | VectorShape::Crescent { opacity, .. } | VectorShape::Arrow4 { opacity, .. } | VectorShape::Bolt { opacity, .. } + | VectorShape::SvgShape { opacity, .. } | VectorShape::FreePath { opacity, .. } => *opacity = clamped, } } @@ -2109,6 +2144,7 @@ impl VectorShape { | VectorShape::Crescent { hardness, .. } | VectorShape::Arrow4 { hardness, .. } | VectorShape::Bolt { hardness, .. } + | VectorShape::SvgShape { hardness, .. } | VectorShape::FreePath { hardness, .. } => *hardness, } } @@ -2131,6 +2167,7 @@ impl VectorShape { | VectorShape::Crescent { hardness, .. } | VectorShape::Arrow4 { hardness, .. } | VectorShape::Bolt { hardness, .. } + | VectorShape::SvgShape { hardness, .. } | VectorShape::FreePath { hardness, .. } => *hardness = clamped, } } @@ -2152,6 +2189,7 @@ impl VectorShape { | VectorShape::Crescent { stroke, .. } | VectorShape::Arrow4 { stroke, .. } | VectorShape::Bolt { stroke, .. } + | VectorShape::SvgShape { stroke, .. } | VectorShape::FreePath { stroke, .. } => *stroke, } } @@ -2174,6 +2212,7 @@ impl VectorShape { | VectorShape::Crescent { stroke, .. } | VectorShape::Arrow4 { stroke, .. } | VectorShape::Bolt { stroke, .. } + | VectorShape::SvgShape { stroke, .. } | VectorShape::FreePath { stroke, .. } => *stroke = clamped, } } @@ -2387,6 +2426,7 @@ impl VectorShape { | VectorShape::Cross { angle, .. } | VectorShape::Crescent { angle, .. } | VectorShape::Arrow4 { angle, .. } + | VectorShape::SvgShape { angle, .. } | VectorShape::Bolt { angle, .. } => *angle, VectorShape::FreePath { .. } => 0.0, } @@ -2408,6 +2448,7 @@ impl VectorShape { | VectorShape::Cross { angle, .. } | VectorShape::Crescent { angle, .. } | VectorShape::Arrow4 { angle, .. } + | VectorShape::SvgShape { angle, .. } | VectorShape::Bolt { angle, .. } => { *angle += delta; } @@ -2431,6 +2472,7 @@ impl VectorShape { | VectorShape::Cross { angle, .. } | VectorShape::Crescent { angle, .. } | VectorShape::Arrow4 { angle, .. } + | VectorShape::SvgShape { angle, .. } | VectorShape::Bolt { angle, .. } => { *angle = new_angle; } @@ -2454,6 +2496,7 @@ impl VectorShape { | VectorShape::Cross { angle, .. } | VectorShape::Crescent { angle, .. } | VectorShape::Arrow4 { angle, .. } + | VectorShape::SvgShape { angle, .. } | VectorShape::Bolt { angle, .. } => Some(angle), VectorShape::FreePath { .. } => None, } @@ -2474,6 +2517,7 @@ impl VectorShape { | VectorShape::Cross { fill, .. } | VectorShape::Crescent { fill, .. } | VectorShape::Arrow4 { fill, .. } + | VectorShape::SvgShape { fill, .. } | VectorShape::Bolt { fill, .. } => *fill, _ => false, } @@ -2494,6 +2538,7 @@ impl VectorShape { | VectorShape::Cross { fill_color, .. } | VectorShape::Crescent { fill_color, .. } | VectorShape::Arrow4 { fill_color, .. } + | VectorShape::SvgShape { fill_color, .. } | VectorShape::Bolt { fill_color, .. } => Some(*fill_color), _ => None, } @@ -2514,6 +2559,7 @@ impl VectorShape { | VectorShape::Cross { fill, .. } | VectorShape::Crescent { fill, .. } | VectorShape::Arrow4 { fill, .. } + | VectorShape::SvgShape { fill, .. } | VectorShape::Bolt { fill, .. } => Some(*fill), _ => None, } @@ -2534,6 +2580,7 @@ impl VectorShape { | VectorShape::Cross { fill, .. } | VectorShape::Crescent { fill, .. } | VectorShape::Arrow4 { fill, .. } + | VectorShape::SvgShape { fill, .. } | VectorShape::Bolt { fill, .. } => Some(fill), _ => None, } @@ -2554,6 +2601,7 @@ impl VectorShape { | VectorShape::Cross { fill_color, .. } | VectorShape::Crescent { fill_color, .. } | VectorShape::Arrow4 { fill_color, .. } + | VectorShape::SvgShape { fill_color, .. } | VectorShape::Bolt { fill_color, .. } => Some(fill_color), _ => None, } @@ -2576,6 +2624,7 @@ impl VectorShape { | VectorShape::Crescent { stroke, .. } | VectorShape::Arrow4 { stroke, .. } | VectorShape::Bolt { stroke, .. } + | VectorShape::SvgShape { stroke, .. } | VectorShape::FreePath { stroke, .. } => Some(stroke), } } @@ -2659,6 +2708,7 @@ impl VectorShape { | VectorShape::Cross { x1, y1, x2, y2, .. } | VectorShape::Crescent { x1, y1, x2, y2, .. } | VectorShape::Arrow4 { x1, y1, x2, y2, .. } + | VectorShape::SvgShape { x1, y1, x2, y2, .. } | VectorShape::Bolt { x1, y1, x2, y2, .. } => (x1, y1, x2, y2), VectorShape::FreePath { .. } => unreachable!("coords_mut not valid for FreePath"), } diff --git a/hcie-vector/Cargo.toml b/hcie-vector/Cargo.toml index 4adc05d..a469fd5 100644 --- a/hcie-vector/Cargo.toml +++ b/hcie-vector/Cargo.toml @@ -8,6 +8,7 @@ hcie-protocol = { path = "../hcie-protocol" } hcie-blend = { path = "../hcie-blend" } serde = { version = "1.0", features = ["derive"] } bincode = "1.3" +usvg = { workspace = true } [lib] crate-type = ["staticlib", "rlib"] diff --git a/hcie-vector/src/lib.rs b/hcie-vector/src/lib.rs index b7bb406..c0939b0 100644 --- a/hcie-vector/src/lib.rs +++ b/hcie-vector/src/lib.rs @@ -1,6 +1,8 @@ //! Vector shape rasterization. pub mod path_boolean; +pub mod svg_templates; +pub mod svg_render; use hcie_protocol::{Layer, LayerData, VectorShape}; use hcie_blend::blend_pixels; @@ -28,6 +30,7 @@ fn get_shape_hardness(shape: &VectorShape) -> f32 { Crescent { hardness, .. } => *hardness, Bolt { hardness, .. } => *hardness, Arrow4 { hardness, .. } => *hardness, + SvgShape { hardness, .. } => *hardness, FreePath { hardness, .. } => *hardness, } } @@ -748,6 +751,27 @@ fn render_shape(layer: &mut Layer, shape: &VectorShape) { draw_line(layer, pts[i].0, pts[i].1, pts[j].0, pts[j].1, px_color, *stroke, None); } } + 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]; + let prev = ACTIVE_HARDNESS.with(|h| h.get()); + ACTIVE_HARDNESS.with(|h| h.set(*hardness)); + 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); + } + } + ACTIVE_HARDNESS.with(|h| h.set(prev)); + } FreePath { pts, stroke, color, fill, fill_color, opacity, .. } => { let alpha = (color[3] as f32 * opacity).round() as u8; let px_color = [color[0], color[1], color[2], alpha]; @@ -848,7 +872,8 @@ pub fn rotate_shape(shape: &mut VectorShape, delta_degrees: f32) { | Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. } | Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. } | Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. } - | Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => { + | Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } + | SvgShape { angle, .. } => { *angle += delta_degrees * std::f32::consts::PI / 180.0; } FreePath { .. } => {} @@ -863,7 +888,8 @@ pub fn set_shape_angle(shape: &mut VectorShape, new_angle_degrees: f32) { | Arrow { angle, .. } | Star { angle, .. } | Polygon { angle, .. } | Rhombus { angle, .. } | Cylinder { angle, .. } | Heart { angle, .. } | Bubble { angle, .. } | Gear { angle, .. } | Cross { angle, .. } - | Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } => { + | Crescent { angle, .. } | Bolt { angle, .. } | Arrow4 { angle, .. } + | SvgShape { angle, .. } => { *angle = rad; } FreePath { .. } => {} diff --git a/hcie-vector/src/svg_render.rs b/hcie-vector/src/svg_render.rs new file mode 100644 index 0000000..b4c9f52 --- /dev/null +++ b/hcie-vector/src/svg_render.rs @@ -0,0 +1,121 @@ +/// SVG path tessellator using `usvg`. +/// +/// Parses an SVG string, extracts `` elements, tessellates bezier curves +/// to line segments, and scales the result from normalized `viewBox="0 0 1 1"` +/// coordinates to the target bounding box `(x1,y1)-(x2,y2)`. + +use usvg::tiny_skia_path::PathSegment; + +/// Parse an SVG string and return tessellated polygons scaled to the target bounds. +/// +/// Returns one polygon per subpath (separated by MoveTo commands). +/// Polygons are in document pixel coordinates ready for `fill_polygon` / `draw_line`. +pub fn svg_to_points( + svg: &str, + x1: f32, + y1: f32, + x2: f32, + y2: f32, +) -> Result>, String> { + let opt = usvg::Options::default(); + let tree = usvg::Tree::from_data(svg.as_bytes(), &opt).map_err(|e| e.to_string())?; + let w = (x2 - x1).abs().max(1.0); + let h = (y2 - y1).abs().max(1.0); + let ox = x1.min(x2); + let oy = y1.min(y2); + + let mut result: Vec> = Vec::new(); + collect_paths(tree.root(), &mut result, ox, oy, w, h); + Ok(result) +} + +fn collect_paths( + node: &usvg::Group, + out: &mut Vec>, + ox: f32, + oy: f32, + w: f32, + h: f32, +) { + for child in node.children() { + match child { + usvg::Node::Path(path) => { + let data = path.data(); + let mut current_poly: Vec<(f32, f32)> = Vec::new(); + + for seg in data.segments() { + match seg { + PathSegment::MoveTo(p) => { + if !current_poly.is_empty() && current_poly.len() > 2 { + out.push(std::mem::take(&mut current_poly)); + } + current_poly.push(( + ox + p.x * w, + oy + p.y * h, + )); + } + PathSegment::LineTo(p) => { + current_poly.push(( + ox + p.x * w, + oy + p.y * h, + )); + } + PathSegment::QuadTo(p1, p) => { + if let Some(&(sx, sy)) = current_poly.last() { + let steps = 8; + for i in 1..=steps { + let t = i as f32 / steps as f32; + let bx = quad_bezier(sx, ox + p1.x * w, ox + p.x * w, t); + let by = quad_bezier(sy, oy + p1.y * w, oy + p.y * h, t); + current_poly.push((bx, by)); + } + } + } + PathSegment::CubicTo(p1, p2, p) => { + if let Some(&(sx, sy)) = current_poly.last() { + let steps = 8; + for i in 1..=steps { + let t = i as f32 / steps as f32; + let bx = cubic_bezier(sx, ox + p1.x * w, ox + p2.x * w, ox + p.x * w, t); + let by = cubic_bezier(sy, oy + p1.y * h, oy + p2.y * h, oy + p.y * h, t); + current_poly.push((bx, by)); + } + } + } + PathSegment::Close => { + if let Some(&first) = current_poly.first() { + current_poly.push(first); + } + if current_poly.len() > 2 { + out.push(std::mem::take(&mut current_poly)); + } + } + } + } + + // Flush remaining open poly + if !current_poly.is_empty() && current_poly.len() > 2 { + // Close it if it looks like a closed shape + if let Some(&first) = current_poly.first() { + current_poly.push(first); + } + out.push(current_poly); + } + } + usvg::Node::Group(g) => { + collect_paths(g, out, ox, oy, w, h); + } + _ => {} + } + } +} + +fn cubic_bezier(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 { + let u = 1.0 - t; + u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3 +} + +fn quad_bezier(p0: f32, p1: f32, p2: f32, t: f32) -> f32 { + let u = 1.0 - t; + u * u * p0 + 2.0 * u * t * p1 + t * t * p2 +} diff --git a/hcie-vector/src/svg_templates.rs b/hcie-vector/src/svg_templates.rs new file mode 100644 index 0000000..e3eb042 --- /dev/null +++ b/hcie-vector/src/svg_templates.rs @@ -0,0 +1,276 @@ +use std::collections::HashMap; + +/// Generate an SVG string for a vector shape. +/// +/// Each shape is defined in a normalized `viewBox="0 0 1 1"` coordinate system. +/// At render time the tessellator scales points to the shape's bounding box. +pub fn create_svg(kind: &str, params: &HashMap) -> String { + match kind { + "arrow" => arrow_svg(params.get("thick").copied().unwrap_or(0.0) > 0.5), + "star" => star_svg( + params.get("points").copied().unwrap_or(5.0) as u32, + params.get("inner_radius").copied().unwrap_or(0.5), + ), + "rhombus" => rhombus_svg(), + "cylinder" => cylinder_svg(), + "heart" => heart_svg(), + "bubble" => bubble_svg(), + "gear" => gear_svg(), + "cross" => cross_svg(), + "crescent" => crescent_svg(), + "bolt" => bolt_svg(), + "arrow4" => arrow4_svg(), + _ => String::new(), + } +} + +fn wrap(d: &str) -> String { + format!( + r#""#, + d + ) +} + +/// Arrow: tail → shaft → arrowhead triangle. +fn arrow_svg(thick: bool) -> String { + let shaft_w = if thick { 0.12 } else { 0.06 }; + let base_x = 0.7; + wrap(&format!( + "M 0,{sw} L {bx},{sw} L {bx},{lw} L 1,0.5 L {bx},{rw} L {bx},{sw2} L 0,{sw2} Z", + sw = 0.5 - shaft_w, + sw2 = 0.5 + shaft_w, + bx = base_x, + lw = 0.5 - 0.4, + rw = 0.5 + 0.4, + )) +} + +/// Star: parametric points/inner_radius. +fn star_svg(points: u32, inner_radius: f32) -> String { + let n = points.max(3) as i32; + let inner_r = inner_radius.max(0.05) * 0.5; + let outer_r = 0.5; + let cx = 0.5f32; + let cy = 0.5f32; + let mut d = String::from("M"); + for i in 0..(n * 2) { + let a = std::f32::consts::PI * 2.0 * i as f32 / (n * 2) as f32 + - std::f32::consts::PI / 2.0; + let r = if i % 2 == 0 { outer_r } else { inner_r }; + let px = cx + r * a.cos(); + let py = cy + r * a.sin(); + if i == 0 { + d = format!("M {},{}", px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + d.push_str(" Z"); + wrap(&d) +} + +/// Rhombus (diamond): 4 points. +fn rhombus_svg() -> String { + wrap("M 0.5,0 L 1,0.5 L 0.5,1 L 0,0.5 Z") +} + +/// Cylinder: top ellipse, bottom ellipse, two side lines. +fn cylinder_svg() -> String { + wrap("M 0,0.1 A 0.5,0.08 0 0,1 1,0.1 L 1,0.9 A 0.5,0.08 0 0,1 0,0.9 Z") +} + +/// Heart: parametric heart curve. +fn heart_svg() -> String { + let steps = 32; + let mut d = String::new(); + for i in 0..steps { + let t = std::f32::consts::PI * 2.0 * i as f32 / steps as f32; + let x = 0.5 + 0.5 * (16.0 * t.sin().powi(3)) / 16.0; + let y = 0.5 - 0.5 * (13.0 * t.cos() - 5.0 * (2.0 * t).cos() - 2.0 * (3.0 * t).cos() - (4.0 * t).cos()) / 16.0; + if i == 0 { + d = format!("M {},{}", x, y); + } else { + d = format!("{} L {},{}", d, x, y); + } + } + d.push_str(" Z"); + wrap(&d) +} + +/// Bubble: elliptical body + triangular tail at bottom-left. +fn bubble_svg() -> String { + wrap("M 0.86,0.5 A 0.36,0.4 0 1,0 0.14,0.5 A 0.36,0.4 0 0,0 0.5,0.86 L 0.3,1.0 L 0.5,0.82 A 0.36,0.4 0 0,0 0.86,0.5 Z") +} + +/// Gear: 8-tooth gear with inner hole. +fn gear_svg() -> String { + let teeth = 8; + let outer_r = 0.45; + let inner_r = 0.3; + let cx = 0.5f32; + let cy = 0.5f32; + let mut d = String::new(); + for i in 0..(teeth * 2) { + let a = std::f32::consts::PI * 2.0 * i as f32 / (teeth * 2) as f32 + - std::f32::consts::PI / 2.0; + let r = if i % 2 == 0 { outer_r } else { inner_r }; + let px = cx + r * a.cos(); + let py = cy + r * a.sin(); + if i == 0 { + d = format!("M {},{}", px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + d.push_str(" Z"); + // Add inner hole as a separate subpath in reverse direction + let hole_r = 0.12; + for i in (0..=16).rev() { + let a = std::f32::consts::PI * 2.0 * i as f32 / 16.0; + let px = cx + hole_r * a.cos(); + let py = cy + hole_r * a.sin(); + if i == 16 { + d = format!("{} M {},{}", d, px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + wrap(&d) +} + +/// Cross: four arms extending from center. +fn cross_svg() -> String { + let arm = 0.25; + let cx = 0.5f32; + let cy = 0.5f32; + let pts = vec![ + (cx - arm, 0.0), + (cx + arm, 0.0), + (cx + arm, cy - arm), + (1.0, cy - arm), + (1.0, cy + arm), + (cx + arm, cy + arm), + (cx + arm, 1.0), + (cx - arm, 1.0), + (cx - arm, cy + arm), + (0.0, cy + arm), + (0.0, cy - arm), + (cx - arm, cy - arm), + ]; + let mut d = String::new(); + for (i, &(px, py)) in pts.iter().enumerate() { + if i == 0 { + d = format!("M {},{}", px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + d.push_str(" Z"); + wrap(&d) +} + +/// Crescent moon: two elliptical arcs. +fn crescent_svg() -> String { + let steps = 32; + let cx = 0.5f32; + let cy = 0.5f32; + let rx = 0.45; + let ry = 0.45; + let start_angle = -std::f32::consts::PI * 0.2; + let end_angle = std::f32::consts::PI * 1.2; + let mut d = String::new(); + + // Outer arc + for i in 0..=steps { + let a = start_angle + (end_angle - start_angle) * i as f32 / steps as f32; + let px = cx + rx * a.cos(); + let py = cy + ry * a.sin(); + if i == 0 { + d = format!("M {},{}", px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + + // Inner arc (reversed to create the crescent shape) + let inner_cx = cx + rx * 0.4; + let inner_rx = rx * 0.7; + let inner_ry = ry * 0.7; + // Get correction at tips + let p_start_outer = (cx + rx * start_angle.cos(), cy + ry * start_angle.sin()); + let inner_start = ( + inner_cx + inner_rx * start_angle.cos(), + cy + inner_ry * start_angle.sin(), + ); + let p_end_outer = (cx + rx * end_angle.cos(), cy + ry * end_angle.sin()); + let inner_end = ( + inner_cx + inner_rx * end_angle.cos(), + cy + inner_ry * end_angle.sin(), + ); + let diff_start = (p_start_outer.0 - inner_start.0, p_start_outer.1 - inner_start.1); + let diff_end = (p_end_outer.0 - inner_end.0, p_end_outer.1 - inner_end.1); + + for i in (0..=steps).rev() { + let t = i as f32 / steps as f32; + let a = start_angle + (end_angle - start_angle) * t; + let raw_x = inner_cx + inner_rx * a.cos(); + let raw_y = cy + inner_ry * a.sin(); + let ox = (1.0 - t) * diff_start.0 + t * diff_end.0; + let oy = (1.0 - t) * diff_start.1 + t * diff_end.1; + d = format!("{} L {},{}", d, raw_x + ox, raw_y + oy); + } + d.push_str(" Z"); + wrap(&d) +} + +/// Lightning bolt: zigzag polygon. +fn bolt_svg() -> String { + wrap("M 0.68,0 L 1,0.28 L 0.55,0.47 L 0.68,0.55 L 0,0.72 L 0.45,0.53 L 0.32,0.45 L 0.68,0 Z") +} + +/// 4-way arrow: cross with triangular arrowheads at all four ends. +fn arrow4_svg() -> String { + let cx = 0.5f32; + let cy = 0.5f32; + let arm_w = 0.125; + let arm_h = 0.125; + let head_ext = 0.15; + + let pts = vec![ + // Up arm + (cx - arm_w, cy - arm_h), + (cx - arm_w, 0.0 + cy - 0.5 + head_ext), + (cx, 0.0), + (cx + arm_w, 0.0 + cy - 0.5 + head_ext), + (cx + arm_w, cy - arm_h), + // Right arm + (cx + arm_w, cy - arm_h), + (1.0 - head_ext, cy - arm_h), + (1.0, cy), + (1.0 - head_ext, cy + arm_h), + (cx + arm_w, cy + arm_h), + // Down arm + (cx + arm_w, cy + arm_h), + (cx + arm_w, 1.0 - cy + 0.5 - head_ext), + (cx, 1.0), + (cx - arm_w, 1.0 - cy + 0.5 - head_ext), + (cx - arm_w, cy + arm_h), + // Left arm + (cx - arm_w, cy + arm_h), + (0.0 + head_ext, cy + arm_h), + (0.0, cy), + (0.0 + head_ext, cy - arm_h), + (cx - arm_w, cy - arm_h), + ]; + + let mut d = String::new(); + for (i, &(px, py)) in pts.iter().enumerate() { + if i == 0 { + d = format!("M {},{}", px, py); + } else { + d = format!("{} L {},{}", d, px, py); + } + } + d.push_str(" Z"); + wrap(&d) +}