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.
This commit is contained in:
2026-07-19 02:44:59 +03:00
parent e90cb2d31d
commit 5632c916ee
17 changed files with 1124 additions and 300 deletions
@@ -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!(
"<path d=\"{}\" transform=\"translate({},{}) scale({},{})\" stroke=\"{}\" stroke-width=\"{}\" fill=\"{}\" fill-opacity=\"{}\" stroke-opacity=\"{}\"/>\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("</svg>");
@@ -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);
@@ -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
}
_ => {}
}
@@ -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,
@@ -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),