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:
@@ -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"]
|
||||
|
||||
+28
-2
@@ -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 { .. } => {}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/// SVG path tessellator using `usvg`.
|
||||
///
|
||||
/// Parses an SVG string, extracts `<path>` 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<Vec<Vec<(f32, f32)>>, 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<(f32, f32)>> = Vec::new();
|
||||
collect_paths(tree.root(), &mut result, ox, oy, w, h);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn collect_paths(
|
||||
node: &usvg::Group,
|
||||
out: &mut Vec<Vec<(f32, f32)>>,
|
||||
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
|
||||
}
|
||||
@@ -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, f32>) -> 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#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path d="{}"/></svg>"#,
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user