Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ roxmltree = "0.21"
serde = { version = "1", default-features = false }
serde_json = "1"
svgtypes = "0.16"
uom = "0.38"
14 changes: 12 additions & 2 deletions g_code/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion star/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 24 additions & 6 deletions star/src/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
};
Expand Down Expand Up @@ -83,6 +84,8 @@ pub struct ConversionOptions {
#[derive(Debug)]
struct ConversionVisitor<'a, T: Turtle> {
terrarium: Terrarium<T>,
/// Whether to flip the Y axis to convert from SVG (Y-down) to the output coordinate system.
coordinate_system: CoordinateSystem,
name_stack: Vec<String>,
/// Used to convert percentage values
viewport_dim_stack: Vec<[f64; 2]>,
Expand Down Expand Up @@ -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();
}
}
}

Expand All @@ -137,6 +144,7 @@ pub fn svg_to_turtle<T: Turtle>(
config: &ConversionConfig,
options: ConversionOptions,
turtle: T,
coordinate_system: CoordinateSystem,
) -> T {
let selector_filter = config
.selector_filter
Expand All @@ -149,6 +157,7 @@ pub fn svg_to_turtle<T: Turtle>(
inner: PreprocessTurtle::default(),
dpi: config.dpi,
}),
coordinate_system,
_config: config,
options: options.clone(),
name_stack: vec![],
Expand Down Expand Up @@ -189,6 +198,7 @@ pub fn svg_to_turtle<T: Turtle>(
inner: turtle,
dpi: config.dpi,
}),
coordinate_system,
_config: config,
options: options.clone(),
name_stack: vec![],
Expand All @@ -202,8 +212,14 @@ pub fn svg_to_turtle<T: 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());
Expand All @@ -227,9 +243,11 @@ fn svg_to_optimized_strokes(
options: ConversionOptions,
origin_transform: Transform2D<f64>,
selector_filter: Option<SelectorList>,
coordinate_system: CoordinateSystem,
) -> Vec<Stroke> {
let mut collect_visitor = ConversionVisitor {
terrarium: Terrarium::new(StrokeCollectingTurtle::default()),
coordinate_system,
_config: config,
options,
name_stack: vec![],
Expand Down
25 changes: 13 additions & 12 deletions star/src/lower/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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, <use> x/y translate is appended to the element's transform
// https://www.w3.org/TR/SVG2/struct.html#UseLayout
Expand Down Expand Up @@ -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")))
Expand Down
10 changes: 6 additions & 4 deletions star/src/turtle/dpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ impl<T: Turtle> DpiConvertingTurtle<T> {
fn vector_to_mm(&self, v: Vector<f64>) -> Vector<f64> {
vector(self.to_mm(v.x), self.to_mm(v.y))
}

#[cfg(feature = "image")]
fn box_to_mm(&self, b: lyon_geom::Box2D<f64>) -> lyon_geom::Box2D<f64> {
lyon_geom::Box2D::new(self.point_to_mm(b.min), self.point_to_mm(b.max))
}
}

impl<T: Turtle> Turtle for DpiConvertingTurtle<T> {
Expand Down Expand Up @@ -100,10 +105,7 @@ impl<T: Turtle> Turtle for DpiConvertingTurtle<T> {
#[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,
})
}
Expand Down
4 changes: 2 additions & 2 deletions star/src/turtle/elements/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ mod tsp;
///
/// <https://www.w3.org/TR/SVG/embedded.html#ImageElement>
#[cfg(feature = "image")]
#[derive(Debug, Clone)]
pub struct RasterImage {
pub position: Point<f64>,
pub dimensions: Vector<f64>,
pub dimensions: lyon_geom::Box2D<f64>,
pub image: image::DynamicImage,
}

Expand Down
23 changes: 20 additions & 3 deletions star/src/turtle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -343,9 +359,10 @@ impl<T: Turtle + std::fmt::Debug> Terrarium<T> {
.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,
});
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -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());
Expand Down