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:
@@ -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: _,
|
||||
|
||||
Reference in New Issue
Block a user