diff --git a/Cargo.toml b/Cargo.toml index d398c99..800f3b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ roxmltree = "0.21" serde = { version = "1", default-features = false } serde_json = "1" svgtypes = "0.16" +uom = "0.38" diff --git a/g_code/src/lib.rs b/g_code/src/lib.rs index 06a410b..094b03f 100644 --- a/g_code/src/lib.rs +++ b/g_code/src/lib.rs @@ -4,7 +4,10 @@ use g_code::emit::Token; use roxmltree::Document; -use svg2star::lower::{ConversionOptions, svg_to_turtle}; +use svg2star::{ + lower::{ConversionOptions, svg_to_turtle}, + turtle::CoordinateSystem, +}; pub use self::{machine::Machine, turtle::GCodeTurtle}; use crate::config::GCodeConfig; @@ -31,5 +34,12 @@ pub fn svg_to_gcode<'a, 'input: 'a>( feedrate: config.feedrate, program: vec![], }; - svg_to_turtle(doc, &config.inner, options, gcode_turtle).program + svg_to_turtle( + doc, + &config.inner, + options, + gcode_turtle, + CoordinateSystem::YUp, + ) + .program } diff --git a/star/Cargo.toml b/star/Cargo.toml index 7d726ac..3b4bb4b 100644 --- a/star/Cargo.toml +++ b/star/Cargo.toml @@ -18,7 +18,7 @@ rustc-hash = "2" lyon_geom.workspace = true euclid = "0.22" log.workspace = true -uom = "0.38" +uom.workspace = true roxmltree.workspace = true svgtypes.workspace = true diff --git a/star/src/lower/mod.rs b/star/src/lower/mod.rs index e61f618..78e95bf 100644 --- a/star/src/lower/mod.rs +++ b/star/src/lower/mod.rs @@ -14,7 +14,8 @@ use self::units::CSS_DEFAULT_DPI; use crate::{ lower::selector::SelectorList, turtle::{ - DpiConvertingTurtle, PreprocessTurtle, StrokeCollectingTurtle, Terrarium, Turtle, + CoordinateSystem, DpiConvertingTurtle, PreprocessTurtle, StrokeCollectingTurtle, Terrarium, + Turtle, elements::{Stroke, minimize_travel_time}, }, }; @@ -83,6 +84,8 @@ pub struct ConversionOptions { #[derive(Debug)] struct ConversionVisitor<'a, T: Turtle> { terrarium: Terrarium, + /// Whether to flip the Y axis to convert from SVG (Y-down) to the output coordinate system. + coordinate_system: CoordinateSystem, name_stack: Vec, /// Used to convert percentage values viewport_dim_stack: Vec<[f64; 2]>, @@ -111,14 +114,18 @@ impl<'a, T: Turtle> ConversionVisitor<'a, T> { } fn begin(&mut self) { - // Part 1 of converting from SVG to GCode coordinates - self.terrarium.push_transform(Transform2D::scale(1., -1.)); + if self.coordinate_system == CoordinateSystem::YUp { + // Part 1 of converting from SVG (Y-down) to output (Y-up) coordinates + self.terrarium.push_transform(Transform2D::scale(1., -1.)); + } self.terrarium.turtle.begin(); } fn end(&mut self) { self.terrarium.turtle.end(); - self.terrarium.pop_transform(); + if self.coordinate_system == CoordinateSystem::YUp { + self.terrarium.pop_transform(); + } } } @@ -137,6 +144,7 @@ pub fn svg_to_turtle( config: &ConversionConfig, options: ConversionOptions, turtle: T, + coordinate_system: CoordinateSystem, ) -> T { let selector_filter = config .selector_filter @@ -149,6 +157,7 @@ pub fn svg_to_turtle( inner: PreprocessTurtle::default(), dpi: config.dpi, }), + coordinate_system, _config: config, options: options.clone(), name_stack: vec![], @@ -189,6 +198,7 @@ pub fn svg_to_turtle( inner: turtle, dpi: config.dpi, }), + coordinate_system, _config: config, options: options.clone(), name_stack: vec![], @@ -202,8 +212,14 @@ pub fn svg_to_turtle( conversion_visitor.begin(); if config.optimize_path_order { - let strokes = - svg_to_optimized_strokes(doc, config, options, origin_transform, selector_filter); + let strokes = svg_to_optimized_strokes( + doc, + config, + options, + origin_transform, + selector_filter, + coordinate_system, + ); let turtle = &mut conversion_visitor.terrarium.turtle; for stroke in strokes { turtle.move_to(stroke.start_point()); @@ -227,9 +243,11 @@ fn svg_to_optimized_strokes( options: ConversionOptions, origin_transform: Transform2D, selector_filter: Option, + coordinate_system: CoordinateSystem, ) -> Vec { let mut collect_visitor = ConversionVisitor { terrarium: Terrarium::new(StrokeCollectingTurtle::default()), + coordinate_system, _config: config, options, name_stack: vec![], diff --git a/star/src/lower/visit.rs b/star/src/lower/visit.rs index 1426a15..88fb88c 100644 --- a/star/src/lower/visit.rs +++ b/star/src/lower/visit.rs @@ -14,7 +14,10 @@ use super::{ transform::{get_viewport_transform, svg_transform_into_euclid_transform}, units::DimensionHint, }; -use crate::{lower::node_name, turtle::Turtle}; +use crate::{ + lower::node_name, + turtle::{CoordinateSystem, Turtle}, +}; const SVG_TAG_NAME: &str = "svg"; const CLIP_PATH_TAG_NAME: &str = "clipPath"; @@ -168,10 +171,7 @@ impl<'a, T: Turtle> XmlVisitor for ConversionVisitor<'a, T> { .options .dimensions .map(|l| l.map(|l| self.length_to_user_units(l, DimensionHint::Horizontal))); - for (original_dim, override_dim) in viewport_size - .iter_mut() - .zip(dimensions_override.into_iter()) - { + for (original_dim, override_dim) in viewport_size.iter_mut().zip(dimensions_override) { *original_dim = override_dim.or(*original_dim); } @@ -219,11 +219,14 @@ impl<'a, T: Turtle> XmlVisitor for ConversionVisitor<'a, T> { ); flattened_transform = flattened_transform.then(&viewport_transform); } - // Part 2 of converting from SVG to GCode coordinates - flattened_transform = flattened_transform.then(&Transform2D::translation( - 0., - -(viewport_size[1] + viewport_pos[1].unwrap_or(0.)), - )); + if self.coordinate_system == CoordinateSystem::YUp { + // Part 2 of converting from SVG (Y-down) to output (Y-up) coordinates: + // shift the origin from the top-left to the bottom-left of the viewport. + flattened_transform = flattened_transform.then(&Transform2D::translation( + 0., + -(viewport_size[1] + viewport_pos[1].unwrap_or(0.)), + )); + } } else if node.has_tag_name(USE_TAG_NAME) { // Per SVG spec, x/y translate is appended to the element's transform // https://www.w3.org/TR/SVG2/struct.html#UseLayout @@ -468,8 +471,6 @@ impl<'a, T: Turtle> XmlVisitor for ConversionVisitor<'a, T> { "image" => { use base64::{Engine, engine::general_purpose::STANDARD}; - use crate::turtle::elements::RasterImage; - let Some(href) = node .attribute("href") .or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href"))) diff --git a/star/src/turtle/dpi.rs b/star/src/turtle/dpi.rs index 0688f0e..c08efec 100644 --- a/star/src/turtle/dpi.rs +++ b/star/src/turtle/dpi.rs @@ -27,6 +27,11 @@ impl DpiConvertingTurtle { fn vector_to_mm(&self, v: Vector) -> Vector { vector(self.to_mm(v.x), self.to_mm(v.y)) } + + #[cfg(feature = "image")] + fn box_to_mm(&self, b: lyon_geom::Box2D) -> lyon_geom::Box2D { + lyon_geom::Box2D::new(self.point_to_mm(b.min), self.point_to_mm(b.max)) + } } impl Turtle for DpiConvertingTurtle { @@ -100,10 +105,7 @@ impl Turtle for DpiConvertingTurtle { #[cfg(feature = "image")] fn image(&mut self, img: super::elements::RasterImage) { self.inner.image(super::elements::RasterImage { - x: self.to_mm(img.x), - y: self.to_mm(img.y), - width: self.to_mm(img.width), - height: self.to_mm(img.height), + dimensions: self.box_to_mm(img.dimensions), image: img.image, }) } diff --git a/star/src/turtle/elements/mod.rs b/star/src/turtle/elements/mod.rs index 396e332..42c81a6 100644 --- a/star/src/turtle/elements/mod.rs +++ b/star/src/turtle/elements/mod.rs @@ -20,9 +20,9 @@ mod tsp; /// /// #[cfg(feature = "image")] +#[derive(Debug, Clone)] pub struct RasterImage { - pub position: Point, - pub dimensions: Vector, + pub dimensions: lyon_geom::Box2D, pub image: image::DynamicImage, } diff --git a/star/src/turtle/mod.rs b/star/src/turtle/mod.rs index 91fdbf3..a84fd78 100644 --- a/star/src/turtle/mod.rs +++ b/star/src/turtle/mod.rs @@ -20,6 +20,22 @@ pub use self::{ svg_preview::SvgPreviewTurtle, }; +/// The coordinate system expected by a [`Turtle`] implementation. +/// +/// Passed as a parameter to [`crate::lower::svg_to_turtle`] so each backend can declare +/// whether it needs SVG's native Y-down space or Y-up (typical for machine tools / G-code). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CoordinateSystem { + /// Y increases downward (SVG default). No extra transform is applied. + #[default] + YDown, + /// Y increases upward (typical for machine tools / G-code). + /// + /// [`crate::lower::svg_to_turtle`] will flip the Y axis so that coordinates delivered to + /// the turtle have the origin at the bottom-left and Y increasing upward. + YUp, +} + /// Abstraction for drawing paths based on [Turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) pub trait Turtle: Debug { fn begin(&mut self); @@ -343,9 +359,10 @@ impl Terrarium { .transform_point(point(x, y) + vector(width, height)); self.turtle.image(crate::turtle::elements::RasterImage { // After transformation, the corners may be swapped resulting in a new x y. - // Also need to pick the larger y because of the G-Code coordinate space swap (?). - position: point(t0.x.min(t1.x), t0.y.max(t1.y)), - dimensions: (t1 - t0).abs(), + dimensions: lyon_geom::Box2D::new( + point(t0.x.min(t1.x), t0.y.min(t1.y)), + point(t0.x.max(t1.x), t0.y.max(t1.y)), + ), image, }); } diff --git a/web/src/main.rs b/web/src/main.rs index 062c599..d724df2 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -17,7 +17,7 @@ use roxmltree::{Document, ParsingOptions}; use svg2gcode::{Machine, svg_to_gcode}; use svg2star::{ lower::{ConversionOptions, svg_to_turtle}, - turtle::SvgPreviewTurtle, + turtle::{CoordinateSystem, SvgPreviewTurtle}, }; use yew::prelude::*; @@ -231,7 +231,7 @@ fn app() -> Html { .ok() .map(|doc| { let options = ConversionOptions { dimensions: svg.dimensions }; - svg_to_turtle(&doc, &app_store.settings.conversion.inner, options, SvgPreviewTurtle::default()).into_preview() + svg_to_turtle(&doc, &app_store.settings.conversion.inner, options, SvgPreviewTurtle::default(), CoordinateSystem::YUp).into_preview() }) .unwrap_or_default(); let preview_svg_base64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(preview_svg.as_bytes());