feat(svg-editor): Implement SVG shape loading and editing functionality
- Add SVG shape loading from disk, allowing users to customize shapes by placing SVG files in the designated directory. - Introduce a new `ShapeCatalog` to manage SVG shapes, prioritizing user-defined shapes over built-in templates. - Implement an SVG editor with node-based editing capabilities, enabling users to manipulate SVG paths directly. - Create a new `SvgEditable` structure to represent editable SVG shapes, supporting operations like adding, removing, and moving nodes. - Enhance the GUI with an SVG editor panel for visual editing of SVG shapes, including controls for node manipulation and saving changes. - Ensure compatibility with existing shape tools and maintain a seamless user experience across the application.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
/// 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)`.
|
||||
/// to line segments, and scales the result from the SVG's viewBox coordinate
|
||||
/// space to the target bounding box `(x1,y1)-(x2,y2)`.
|
||||
///
|
||||
/// The viewBox is extracted from the raw SVG string so non-zero min-x/min-y
|
||||
/// values are correctly handled ($viewBox="-50 -50 100 100" etc.).
|
||||
|
||||
use usvg::tiny_skia_path::PathSegment;
|
||||
|
||||
@@ -19,13 +22,19 @@ pub fn svg_to_points(
|
||||
) -> 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 target_w = (x2 - x1).abs().max(1.0);
|
||||
let target_h = (y2 - y1).abs().max(1.0);
|
||||
let ox = x1.min(x2);
|
||||
let oy = y1.min(y2);
|
||||
|
||||
// Extract viewBox from raw SVG so we can handle non-zero min-x/min-y.
|
||||
let (vb_min_x, vb_min_y, vb_w, vb_h) = parse_viewbox(svg).unwrap_or((0.0, 0.0, 1.0, 1.0));
|
||||
let svg_w = vb_w.max(0.001);
|
||||
let svg_h = vb_h.max(0.001);
|
||||
|
||||
let mut result: Vec<Vec<(f32, f32)>> = Vec::new();
|
||||
collect_paths(tree.root(), &mut result, ox, oy, w, h);
|
||||
collect_paths(tree.root(), &mut result, ox, oy, target_w, target_h, vb_min_x, vb_min_y, svg_w, svg_h);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -34,8 +43,12 @@ fn collect_paths(
|
||||
out: &mut Vec<Vec<(f32, f32)>>,
|
||||
ox: f32,
|
||||
oy: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
target_w: f32,
|
||||
target_h: f32,
|
||||
vb_min_x: f32,
|
||||
vb_min_y: f32,
|
||||
svg_w: f32,
|
||||
svg_h: f32,
|
||||
) {
|
||||
for child in node.children() {
|
||||
match child {
|
||||
@@ -43,21 +56,21 @@ fn collect_paths(
|
||||
let data = path.data();
|
||||
let mut current_poly: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
for seg in data.segments() {
|
||||
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,
|
||||
ox + ((p.x - vb_min_x) / svg_w) * target_w,
|
||||
oy + ((p.y - vb_min_y) / svg_h) * target_h,
|
||||
));
|
||||
}
|
||||
PathSegment::LineTo(p) => {
|
||||
current_poly.push((
|
||||
ox + p.x * w,
|
||||
oy + p.y * h,
|
||||
ox + ((p.x - vb_min_x) / svg_w) * target_w,
|
||||
oy + ((p.y - vb_min_y) / svg_h) * target_h,
|
||||
));
|
||||
}
|
||||
PathSegment::QuadTo(p1, p) => {
|
||||
@@ -65,8 +78,12 @@ fn collect_paths(
|
||||
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);
|
||||
let bx = quad_bezier(sx,
|
||||
ox + ((p1.x - vb_min_x) / svg_w) * target_w,
|
||||
ox + ((p.x - vb_min_x) / svg_w) * target_w, t);
|
||||
let by = quad_bezier(sy,
|
||||
oy + ((p1.y - vb_min_y) / svg_h) * target_h,
|
||||
oy + ((p.y - vb_min_y) / svg_h) * target_h, t);
|
||||
current_poly.push((bx, by));
|
||||
}
|
||||
}
|
||||
@@ -76,8 +93,14 @@ fn collect_paths(
|
||||
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);
|
||||
let bx = cubic_bezier(sx,
|
||||
ox + ((p1.x - vb_min_x) / svg_w) * target_w,
|
||||
ox + ((p2.x - vb_min_x) / svg_w) * target_w,
|
||||
ox + ((p.x - vb_min_x) / svg_w) * target_w, t);
|
||||
let by = cubic_bezier(sy,
|
||||
oy + ((p1.y - vb_min_y) / svg_h) * target_h,
|
||||
oy + ((p2.y - vb_min_y) / svg_h) * target_h,
|
||||
oy + ((p.y - vb_min_y) / svg_h) * target_h, t);
|
||||
current_poly.push((bx, by));
|
||||
}
|
||||
}
|
||||
@@ -95,7 +118,6 @@ fn collect_paths(
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -103,7 +125,7 @@ fn collect_paths(
|
||||
}
|
||||
}
|
||||
usvg::Node::Group(g) => {
|
||||
collect_paths(g, out, ox, oy, w, h);
|
||||
collect_paths(g, out, ox, oy, target_w, target_h, vb_min_x, vb_min_y, svg_w, svg_h);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -119,3 +141,23 @@ 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
|
||||
}
|
||||
|
||||
/// Extract the viewBox attribute from an SVG string.
|
||||
///
|
||||
/// Returns `(min_x, min_y, width, height)` or `None` if no viewBox found.
|
||||
fn parse_viewbox(svg: &str) -> Option<(f32, f32, f32, f32)> {
|
||||
let lower = svg.to_lowercase();
|
||||
let needle = "viewbox=\"";
|
||||
let start = lower.find(needle)?;
|
||||
let after_eq = &svg[start + needle.len()..];
|
||||
let end = after_eq.find('"')?;
|
||||
let val = after_eq[..end].trim();
|
||||
let nums: Vec<f32> = val.split_whitespace()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
if nums.len() == 4 {
|
||||
Some((nums[0], nums[1], nums[2], nums[3]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A built-in SVG template entry.
|
||||
pub struct TemplateEntry {
|
||||
pub kind: &'static str,
|
||||
pub svg: String,
|
||||
}
|
||||
|
||||
/// Generate an SVG string for a vector shape.
|
||||
///
|
||||
/// Each shape is defined in a normalized `viewBox="0 0 1 1"` coordinate system.
|
||||
@@ -274,3 +280,24 @@ fn arrow4_svg() -> String {
|
||||
d.push_str(" Z");
|
||||
wrap(&d)
|
||||
}
|
||||
|
||||
/// Return all built-in SVG templates with their kind names.
|
||||
///
|
||||
/// These are the default shapes shipped with the application. Each template
|
||||
/// uses a `viewBox="0 0 1 1"` and generates an SVG suitable for rendering.
|
||||
/// When written to disk, users can edit them in any SVG editor.
|
||||
pub fn all_templates() -> Vec<TemplateEntry> {
|
||||
vec![
|
||||
TemplateEntry { kind: "arrow", svg: arrow_svg(false) },
|
||||
TemplateEntry { kind: "star", svg: star_svg(5, 0.5) },
|
||||
TemplateEntry { kind: "rhombus", svg: rhombus_svg() },
|
||||
TemplateEntry { kind: "cylinder", svg: cylinder_svg() },
|
||||
TemplateEntry { kind: "heart", svg: heart_svg() },
|
||||
TemplateEntry { kind: "bubble", svg: bubble_svg() },
|
||||
TemplateEntry { kind: "gear", svg: gear_svg() },
|
||||
TemplateEntry { kind: "cross", svg: cross_svg() },
|
||||
TemplateEntry { kind: "crescent", svg: crescent_svg() },
|
||||
TemplateEntry { kind: "bolt", svg: bolt_svg() },
|
||||
TemplateEntry { kind: "arrow4", svg: arrow4_svg() },
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user