From 4b82c064bd3d07c851bc11d7b7b684ae8499e170 Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 5 Jul 2026 23:14:02 -0700 Subject: [PATCH 1/2] Add machine.z_path and machine.z_travel to control Z axis --- cli/src/main.rs | 15 +++++++++ g_code/src/config/mod.rs | 6 ++++ g_code/src/machine.rs | 6 ++++ g_code/src/tests.rs | 4 +++ g_code/src/turtle.rs | 72 ++++++++++++++++++++++++++++------------ web/src/main.rs | 2 ++ 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 36b93c5..16f9c2e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -97,6 +97,12 @@ struct Opt { /// Useful to print the label of layer on SVG generated by Inkscape extra_attribute_name: Option, #[arg(long)] + /// Z height for rapid travel moves (pen/tool raised position) + z_travel: Option, + #[arg(long)] + /// Z height for drawing moves (pen/tool lowered position) + z_path: Option, + #[arg(long)] /// Reorder paths to minimize travel time optimize_path_order: Option, /// CSS selector to filter which SVG elements are converted. @@ -202,6 +208,13 @@ fn main() -> io::Result<()> { settings.postprocess.newline_before_comment = newline_before_comment; } + if let Some(z_travel) = opt.z_travel { + settings.machine.z_travel = Some(z_travel); + } + if let Some(z_path) = opt.z_path { + settings.machine.z_path = Some(z_path); + } + settings.conversion.inner.extra_attribute_name = opt.extra_attribute_name; if let Some(optimize_path_order) = opt.optimize_path_order { settings.conversion.inner.optimize_path_order = optimize_path_order; @@ -322,6 +335,8 @@ fn main() -> io::Result<()> { tool_off_action, program_begin_sequence, program_end_sequence, + settings.machine.z_travel, + settings.machine.z_path, ) } else { use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; diff --git a/g_code/src/config/mod.rs b/g_code/src/config/mod.rs index 3cc9f19..a84f55e 100644 --- a/g_code/src/config/mod.rs +++ b/g_code/src/config/mod.rs @@ -40,6 +40,12 @@ pub struct MachineConfig { pub tool_off_sequence: Option, pub begin_sequence: Option, pub end_sequence: Option, + /// Z coordinate for rapid travel moves (pen/tool raised position) + #[cfg_attr(feature = "serde", serde(default))] + pub z_travel: Option, + /// Z coordinate for drawing moves (pen/tool lowered position) + #[cfg_attr(feature = "serde", serde(default))] + pub z_path: Option, } #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/g_code/src/machine.rs b/g_code/src/machine.rs index 44c6bb3..d05fa5a 100644 --- a/g_code/src/machine.rs +++ b/g_code/src/machine.rs @@ -33,6 +33,8 @@ pub struct Machine<'input> { program_end_sequence: Snippet<'input>, /// Empty snippet used to provide the same iterator type when a sequence must be empty empty_snippet: Snippet<'input>, + pub z_travel: Option, + pub z_path: Option, } impl<'input> Machine<'input> { @@ -42,6 +44,8 @@ impl<'input> Machine<'input> { tool_off_sequence: Option>, program_begin_sequence: Option>, program_end_sequence: Option>, + z_travel: Option, + z_path: Option, ) -> Self { let empty_snippet = snippet_parser("").expect("empty string is a valid snippet"); Self { @@ -53,6 +57,8 @@ impl<'input> Machine<'input> { empty_snippet, tool_state: Default::default(), distance_mode: Default::default(), + z_travel, + z_path, } } diff --git a/g_code/src/tests.rs b/g_code/src/tests.rs index 04f7558..fd6766f 100644 --- a/g_code/src/tests.rs +++ b/g_code/src/tests.rs @@ -35,6 +35,8 @@ fn get_actual( None, None, None, + None, + None, ); svg_to_gcode(&document, &gcode_config, options, machine) } @@ -278,6 +280,8 @@ fn issue_105_optimize_path_order_does_not_shrink_output() { None, None, None, + None, + None, ); let normal = svg_to_gcode( &document, diff --git a/g_code/src/turtle.rs b/g_code/src/turtle.rs index 5c38437..43f557c 100644 --- a/g_code/src/turtle.rs +++ b/g_code/src/turtle.rs @@ -78,12 +78,22 @@ impl<'input> GCodeTurtle<'input> { } fn line_to(&self, to: Point) -> Vec> { - command!(LinearInterpolation { - X: self.round(to.x), - Y: self.round(to.y), - F: self.feedrate, - }) - .into_token_vec() + if let Some(z_path) = self.machine.z_path { + command!(LinearInterpolation { + X: self.round(to.x), + Y: self.round(to.y), + Z: z_path, + F: self.feedrate, + }) + .into_token_vec() + } else { + command!(LinearInterpolation { + X: self.round(to.x), + Y: self.round(to.y), + F: self.feedrate, + }) + .into_token_vec() + } } fn circular_interpolation(&self, svg_arc: SvgArc) -> Vec> { @@ -154,26 +164,46 @@ impl<'input> Turtle for GCodeTurtle<'input> { } } - self.program.append( - &mut command!(RapidPositioning { - X: self.round(start.x), - Y: self.round(start.y), - }) - .into_token_vec(), - ); + if let Some(z_travel) = self.machine.z_travel { + self.program.append( + &mut command!(RapidPositioning { + Z: z_travel, + }) + .into_token_vec(), + ); + self.program.append( + &mut command!(RapidPositioning { + X: self.round(start.x), + Y: self.round(start.y), + Z: z_travel, + }) + .into_token_vec(), + ); + } else { + self.program.append( + &mut command!(RapidPositioning { + X: self.round(start.x), + Y: self.round(start.y), + }) + .into_token_vec(), + ); + } + + if let Some(z_path) = self.machine.z_path { + self.program.append( + &mut command!(RapidPositioning { + Z: z_path, + }) + .into_token_vec(), + ); + } + self.tool_on(); for command in commands { match command { DrawCommand::LineTo { from: _, to } => { - self.program.append( - &mut command!(LinearInterpolation { - X: self.round(to.x), - Y: self.round(to.y), - F: self.feedrate, - }) - .into_token_vec(), - ); + self.program.append(&mut self.line_to(to)); } DrawCommand::Arc(svg_arc) => { if self diff --git a/web/src/main.rs b/web/src/main.rs index d724df2..4443fcf 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -108,6 +108,8 @@ fn app() -> Html { .map(snippet_parser) .transpose() .unwrap(), + app_store.settings.machine.z_travel, + app_store.settings.machine.z_path, ); let document = Document::parse_with_options( svg.content.as_str(), From 40cf44c503d1ca68878c6660ae0a952d2bc4f91e Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Mon, 6 Jul 2026 23:07:36 -0700 Subject: [PATCH 2/2] Support variable Z based on stroke-width --- cli/src/main.rs | 14 +++ g_code/src/config/mod.rs | 8 +- g_code/src/machine.rs | 6 ++ g_code/src/tests.rs | 4 + g_code/src/turtle.rs | 168 +++++++++++++++++++++++++++++--- star/src/lower/visit.rs | 16 ++- star/src/turtle/dpi.rs | 4 +- star/src/turtle/elements/mod.rs | 11 +++ star/src/turtle/mod.rs | 10 +- web/src/main.rs | 2 + 10 files changed, 219 insertions(+), 24 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 16f9c2e..32a6acf 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -103,6 +103,12 @@ struct Opt { /// Z height for drawing moves (pen/tool lowered position) z_path: Option, #[arg(long)] + /// Z height at maximum emphasis (deepest/most pressure, for thick strokes) + z_emphasis: Option, + #[arg(long)] + /// Stroke width in mm at which z_emphasis is fully applied; interpolates below + emphasis_stroke_width: Option, + #[arg(long)] /// Reorder paths to minimize travel time optimize_path_order: Option, /// CSS selector to filter which SVG elements are converted. @@ -214,6 +220,12 @@ fn main() -> io::Result<()> { if let Some(z_path) = opt.z_path { settings.machine.z_path = Some(z_path); } + if let Some(z_emphasis) = opt.z_emphasis { + settings.machine.z_emphasis = Some(z_emphasis); + } + if let Some(esw) = opt.emphasis_stroke_width { + settings.machine.emphasis_stroke_width = Some(esw); + } settings.conversion.inner.extra_attribute_name = opt.extra_attribute_name; if let Some(optimize_path_order) = opt.optimize_path_order { @@ -337,6 +349,8 @@ fn main() -> io::Result<()> { program_end_sequence, settings.machine.z_travel, settings.machine.z_path, + settings.machine.z_emphasis, + settings.machine.emphasis_stroke_width, ) } else { use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; diff --git a/g_code/src/config/mod.rs b/g_code/src/config/mod.rs index a84f55e..6676b5f 100644 --- a/g_code/src/config/mod.rs +++ b/g_code/src/config/mod.rs @@ -43,9 +43,15 @@ pub struct MachineConfig { /// Z coordinate for rapid travel moves (pen/tool raised position) #[cfg_attr(feature = "serde", serde(default))] pub z_travel: Option, - /// Z coordinate for drawing moves (pen/tool lowered position) + /// Z coordinate for drawing moves (pen/tool lowered) #[cfg_attr(feature = "serde", serde(default))] pub z_path: Option, + /// Z coordinate at maximum emphasis (deepest/most pressure, for thick strokes) + #[cfg_attr(feature = "serde", serde(default))] + pub z_emphasis: Option, + /// Stroke width in mm at which zemphasis is fully applied; interpolates below + #[cfg_attr(feature = "serde", serde(default))] + pub emphasis_stroke_width: Option, } #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/g_code/src/machine.rs b/g_code/src/machine.rs index d05fa5a..b97d1c1 100644 --- a/g_code/src/machine.rs +++ b/g_code/src/machine.rs @@ -35,6 +35,8 @@ pub struct Machine<'input> { empty_snippet: Snippet<'input>, pub z_travel: Option, pub z_path: Option, + pub z_emphasis: Option, + pub emphasis_stroke_width: Option, } impl<'input> Machine<'input> { @@ -46,6 +48,8 @@ impl<'input> Machine<'input> { program_end_sequence: Option>, z_travel: Option, z_path: Option, + z_emphasis: Option, + emphasis_stroke_width: Option, ) -> Self { let empty_snippet = snippet_parser("").expect("empty string is a valid snippet"); Self { @@ -59,6 +63,8 @@ impl<'input> Machine<'input> { distance_mode: Default::default(), z_travel, z_path, + z_emphasis, + emphasis_stroke_width, } } diff --git a/g_code/src/tests.rs b/g_code/src/tests.rs index fd6766f..45dcf1d 100644 --- a/g_code/src/tests.rs +++ b/g_code/src/tests.rs @@ -37,6 +37,8 @@ fn get_actual( None, None, None, + None, + None, ); svg_to_gcode(&document, &gcode_config, options, machine) } @@ -282,6 +284,8 @@ fn issue_105_optimize_path_order_does_not_shrink_output() { None, None, None, + None, + None, ); let normal = svg_to_gcode( &document, diff --git a/g_code/src/turtle.rs b/g_code/src/turtle.rs index 43f557c..6ff5418 100644 --- a/g_code/src/turtle.rs +++ b/g_code/src/turtle.rs @@ -77,12 +77,50 @@ impl<'input> GCodeTurtle<'input> { } } - fn line_to(&self, to: Point) -> Vec> { - if let Some(z_path) = self.machine.z_path { + /// Computes the Z for a drawing move given a stroke's SVG width. + /// + /// Returns: + /// if z_path is not configured: None + /// if z_emphasis or emphasis_stroke_width are not configured: z_path + /// if stroke-width = 1: z_path + /// if stroke-width > 1: interpolate toward z_emphasis. Clamped at z_emphasis if stroke-width >= emphasis_stroke_width. + /// if stroke-width < 1: extrapolate backward using the same slope. Clamped at z_travel. + fn z_for_stroke_width(&self, width: f64) -> Option { + let z_path = self.machine.z_path?; + let emphasis = match (self.machine.z_emphasis, self.machine.emphasis_stroke_width) { + (Some(ze), Some(esw)) if esw > 1.0 => Some((ze, esw)), + _ => None, + }; + + if width >= 1.0 { + match emphasis { + Some((z_emphasis, esw)) => { + let t = ((width - 1.0) / (esw - 1.0)).min(1.0); + Some(z_path + (z_emphasis - z_path) * t) + } + None => Some(z_path), + } + } else { + match emphasis { + Some((z_emphasis, esw)) => { + let slope = (z_emphasis - z_path) / (esw - 1.0); + let z_raw = z_path + slope * (width.max(0.0) - 1.0); + Some(match self.machine.z_travel { + Some(z_travel) => z_raw.clamp(z_travel.min(z_path), z_travel.max(z_path)), + None => z_raw, + }) + } + None => Some(z_path), + } + } + } + + fn line_to(&self, to: Point, z: Option) -> Vec> { + if let Some(z) = z { command!(LinearInterpolation { X: self.round(to.x), Y: self.round(to.y), - Z: z_path, + Z: z, F: self.feedrate, }) .into_token_vec() @@ -149,6 +187,8 @@ impl<'input> Turtle for GCodeTurtle<'input> { } fn stroke(&mut self, stroke: Stroke) { + let stroke_width = stroke.width; + let z_draw = self.z_for_stroke_width(stroke_width); let start = stroke.start_point(); let mut commands = stroke.into_commands().peekable(); @@ -189,10 +229,10 @@ impl<'input> Turtle for GCodeTurtle<'input> { ); } - if let Some(z_path) = self.machine.z_path { + if let Some(z) = z_draw { self.program.append( &mut command!(RapidPositioning { - Z: z_path, + Z: z, }) .into_token_vec(), ); @@ -203,7 +243,7 @@ impl<'input> Turtle for GCodeTurtle<'input> { for command in commands { match command { DrawCommand::LineTo { from: _, to } => { - self.program.append(&mut self.line_to(to)); + self.program.append(&mut self.line_to(to, z_draw)); } DrawCommand::Arc(svg_arc) => { if self @@ -218,14 +258,14 @@ impl<'input> Turtle for GCodeTurtle<'input> { self.program.append(&mut self.circular_interpolation(arc)) } ArcOrLineSegment::Line(line) => { - self.line_to(line.to); + self.program.append(&mut self.line_to(line.to, z_draw)); } }); } else { svg_arc .to_arc() .flattened(self.flattening_tolerance()) - .for_each(|point| self.program.append(&mut self.line_to(point))); + .for_each(|point| self.program.append(&mut self.line_to(point, z_draw))); }; } DrawCommand::CubicBezier(cbs) => { @@ -241,12 +281,12 @@ impl<'input> Turtle for GCodeTurtle<'input> { self.program.append(&mut self.circular_interpolation(arc)) } ArcOrLineSegment::Line(line) => { - self.program.append(&mut self.line_to(line.to)) + self.program.append(&mut self.line_to(line.to, z_draw)) } }); } else { cbs.flattened(self.flattening_tolerance()) - .for_each(|point| self.program.append(&mut self.line_to(point))); + .for_each(|point| self.program.append(&mut self.line_to(point, z_draw))); }; } DrawCommand::QuadraticBezier(qbs) => { @@ -265,12 +305,12 @@ impl<'input> Turtle for GCodeTurtle<'input> { self.program.append(&mut self.circular_interpolation(arc)) } ArcOrLineSegment::Line(line) => { - self.program.append(&mut self.line_to(line.to)) + self.program.append(&mut self.line_to(line.to, z_draw)) } }); } else { qbs.flattened(self.flattening_tolerance()) - .for_each(|point| self.program.append(&mut self.line_to(point))); + .for_each(|point| self.program.append(&mut self.line_to(point, z_draw))); }; } DrawCommand::Comment(comment) => { @@ -292,3 +332,107 @@ impl<'input> Turtle for GCodeTurtle<'input> { // TODO } } + +#[cfg(test)] +mod tests { + use crate::config::SupportedFunctionality; + + use super::*; + + fn machine_with_z( + z_travel: Option, + z_path: Option, + z_emphasis: Option, + emphasis_stroke_width: Option, + ) -> Machine<'static> { + Machine::new( + SupportedFunctionality::default(), + None, None, None, None, + z_travel, z_path, z_emphasis, emphasis_stroke_width, + ) + } + + fn turtle_with_z( + z_travel: Option, + z_path: Option, + z_emphasis: Option, + emphasis_stroke_width: Option, + ) -> GCodeTurtle<'static> { + GCodeTurtle { + machine: machine_with_z(z_travel, z_path, z_emphasis, emphasis_stroke_width), + tolerance: 0.1, + feedrate: 300.0, + program: vec![], + } + } + + #[test] + fn z_for_stroke_width_none_when_z_path_unset() { + let t = turtle_with_z(Some(5.0), None, Some(-2.0), Some(3.0)); + assert_eq!(t.z_for_stroke_width(0.5), None); + assert_eq!(t.z_for_stroke_width(1.0), None); + assert_eq!(t.z_for_stroke_width(2.0), None); + } + + #[test] + fn z_for_stroke_width_one_is_z_path() { + // width=1 is always z_path regardless of emphasis config + let t = turtle_with_z(Some(5.0), Some(0.0), Some(-2.0), Some(3.0)); + assert_eq!(t.z_for_stroke_width(1.0), Some(0.0)); + } + + #[test] + fn z_for_stroke_width_returns_z_path_when_z_emphasis_unset() { + // z_emphasis unset → z_path for all widths including sub-1 + let t = turtle_with_z(Some(5.0), Some(0.0), None, None); + assert_eq!(t.z_for_stroke_width(0.01), Some(0.0)); + assert_eq!(t.z_for_stroke_width(0.5), Some(0.0)); + assert_eq!(t.z_for_stroke_width(1.0), Some(0.0)); + assert_eq!(t.z_for_stroke_width(5.0), Some(0.0)); + } + + #[test] + fn z_for_stroke_width_interpolates_above_one() { + // z_path=0, z_emphasis=-2, esw=3: slope=-1/unit above 1 + // width=1→0, width=2→-1, width=3→-2, width=4→-2 (clamped) + let t = turtle_with_z(None, Some(0.0), Some(-2.0), Some(3.0)); + assert_eq!(t.z_for_stroke_width(1.0), Some(0.0)); + assert_eq!(t.z_for_stroke_width(2.0), Some(-1.0)); + assert_eq!(t.z_for_stroke_width(3.0), Some(-2.0)); + assert_eq!(t.z_for_stroke_width(4.0), Some(-2.0)); + } + + #[test] + fn z_for_stroke_width_extrapolates_below_one() { + // Same slope as above-1 region, extended backward from z_path at width=1 + // z_path=0, z_emphasis=-2, esw=3: slope=-1/unit + // width=0.5 → z_raw = 0 + (-1)*(0.5-1) = 0.5, no z_travel → 0.5 + // width=0.01 → z_raw = 0 + (-1)*(0.01-1) = 0.99 + let t = turtle_with_z(None, Some(0.0), Some(-2.0), Some(3.0)); + assert!((t.z_for_stroke_width(0.5).unwrap() - 0.5).abs() < 1e-9); + assert!((t.z_for_stroke_width(0.01).unwrap() - 0.99).abs() < 1e-9); + } + + #[test] + fn z_for_stroke_width_sub_one_clamped_at_z_travel() { + // z_path=0, z_emphasis=-10, esw=2: slope=-10/unit + // width=0.5 → z_raw = 0 + (-10)*(0.5-1) = 5 → clamped at z_travel=3 + // clamp triggers at width > 0.7 (z_raw=3 exactly when width=0.7) + let t = turtle_with_z(Some(3.0), Some(0.0), Some(-10.0), Some(2.0)); + assert_eq!(t.z_for_stroke_width(0.5), Some(3.0)); + assert!((t.z_for_stroke_width(0.7).unwrap() - 3.0).abs() < 1e-9); + assert!((t.z_for_stroke_width(0.9).unwrap() - 1.0).abs() < 1e-9); + } + + #[test] + fn z_for_stroke_width_returns_z_path_when_esw_at_or_below_one() { + // esw ≤ 1.0 makes no sense for this model; falls back to z_path for all widths + let t = turtle_with_z(None, Some(0.0), Some(-2.0), Some(1.0)); + assert_eq!(t.z_for_stroke_width(0.5), Some(0.0)); + assert_eq!(t.z_for_stroke_width(1.0), Some(0.0)); + assert_eq!(t.z_for_stroke_width(2.0), Some(0.0)); + let t = turtle_with_z(None, Some(0.0), Some(-2.0), Some(0.5)); + assert_eq!(t.z_for_stroke_width(0.5), Some(0.0)); + assert_eq!(t.z_for_stroke_width(1.0), Some(0.0)); + } +} diff --git a/star/src/lower/visit.rs b/star/src/lower/visit.rs index 4b74776..8de49ee 100644 --- a/star/src/lower/visit.rs +++ b/star/src/lower/visit.rs @@ -390,6 +390,12 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { .map(|v| v != "none") .unwrap_or(true); + // SVG default stroke-width is 1 user unit + let stroke_width = calculate_presentation_attr(node, "stroke-width", &self.css_rules) + .and_then(|v| LengthListParser::from(v).next()?.ok()) + .map(|l| self.length_to_user_units(l, DimensionHint::Other)) + .unwrap_or(1.0); + match node.tag_name().name() { PATH_TAG_NAME => { let Some(d) = node.attribute("d") else { @@ -405,7 +411,7 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { self.terrarium.apply_polygon(segments.clone(), fill_rule); } if has_stroke_attr { - self.terrarium.apply_path(segments); + self.terrarium.apply_path(segments, stroke_width); } } } @@ -431,7 +437,7 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { self.terrarium.apply_polygon(segments.clone(), fill_rule); } if has_stroke_attr { - self.terrarium.apply_path(segments); + self.terrarium.apply_path(segments, stroke_width); } } } @@ -521,7 +527,7 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { self.terrarium.apply_polygon(segments.clone(), fill_rule); } if has_stroke_attr { - self.terrarium.apply_path(segments); + self.terrarium.apply_path(segments, stroke_width); } } } @@ -589,7 +595,7 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { self.terrarium.apply_polygon(segments, fill_rule); } if has_stroke_attr { - self.terrarium.apply_path(segments); + self.terrarium.apply_path(segments, stroke_width); } } } @@ -616,7 +622,7 @@ impl<'a, 'input, T: Turtle> XmlVisitor for ConversionVisitor<'a, 'input, T> { x: x2, y: y2, }, - ]); + ], stroke_width); } } #[cfg(feature = "image")] diff --git a/star/src/turtle/dpi.rs b/star/src/turtle/dpi.rs index f124e13..8683c12 100644 --- a/star/src/turtle/dpi.rs +++ b/star/src/turtle/dpi.rs @@ -45,7 +45,8 @@ impl DpiConvertingTurtle { } fn stroke_to_mm(&self, stroke: Stroke) -> Stroke { - Stroke::new( + let width = stroke.width; + Stroke::with_width( self.point_to_mm(stroke.start_point()), stroke .into_commands() @@ -88,6 +89,7 @@ impl DpiConvertingTurtle { DrawCommand::Comment(s) => DrawCommand::Comment(s), }) .collect(), + width, ) } } diff --git a/star/src/turtle/elements/mod.rs b/star/src/turtle/elements/mod.rs index 9e4f3be..bec5188 100644 --- a/star/src/turtle/elements/mod.rs +++ b/star/src/turtle/elements/mod.rs @@ -125,6 +125,8 @@ impl DrawCommand { pub struct Stroke { start_point: Point, commands: Vec, + /// SVG stroke-width in user units (not DPI-converted). Zero means no explicit width. + pub width: f64, } impl Stroke { @@ -132,6 +134,15 @@ impl Stroke { Self { start_point, commands, + width: 0.0, + } + } + + pub fn with_width(start_point: Point, commands: Vec, width: f64) -> Self { + Self { + start_point, + commands, + width, } } diff --git a/star/src/turtle/mod.rs b/star/src/turtle/mod.rs index d7636a7..dc7c3cb 100644 --- a/star/src/turtle/mod.rs +++ b/star/src/turtle/mod.rs @@ -443,7 +443,7 @@ impl Terrarium { } /// Maps [PathSegments](PathSegment) into concrete operations. - pub fn apply_path(&mut self, path: impl IntoIterator) { + pub fn apply_path(&mut self, path: impl IntoIterator, stroke_width: f64) { use PathSegment::*; self.begin(); self.reset(); @@ -457,7 +457,7 @@ impl Terrarium { MoveTo { abs, x, y } => { if !commands.is_empty() { self.turtle - .stroke(Stroke::new(start_point, std::mem::take(&mut commands))); + .stroke(Stroke::with_width(start_point, std::mem::take(&mut commands), stroke_width)); } start_point = self.move_to(abs, x, y); if let Some(comment) = pending_comment.take() { @@ -470,7 +470,7 @@ impl Terrarium { } if !commands.is_empty() { self.turtle - .stroke(Stroke::new(start_point, std::mem::take(&mut commands))); + .stroke(Stroke::with_width(start_point, std::mem::take(&mut commands), stroke_width)); } } LineTo { abs, x, y } => { @@ -540,7 +540,7 @@ impl Terrarium { if !commands.is_empty() { self.turtle - .stroke(Stroke::new(start_point, std::mem::take(&mut commands))); + .stroke(Stroke::with_width(start_point, std::mem::take(&mut commands), stroke_width)); } } @@ -572,7 +572,7 @@ impl Terrarium { current_bounds: self.current_bounds, bounds_stack: vec![], }; - sub.apply_path(segments); + sub.apply_path(segments, 0.0); let segments = sub.finish().into_strokes(); for polygon in self::elements::fill::into_fill_polygons(segments, fill_rule) { self.turtle.fill_polygon(polygon); diff --git a/web/src/main.rs b/web/src/main.rs index 4443fcf..59a5522 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -110,6 +110,8 @@ fn app() -> Html { .unwrap(), app_store.settings.machine.z_travel, app_store.settings.machine.z_path, + app_store.settings.machine.z_emphasis, + app_store.settings.machine.emphasis_stroke_width, ); let document = Document::parse_with_options( svg.content.as_str(),