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
+147 -146
View File
@@ -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,
};
@@ -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),
}
}
@@ -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![
@@ -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: _,