|
| 1 | +use std::fmt::Write; |
| 2 | + |
| 3 | +use lyon_geom::{Box2D, CubicBezierSegment, Point, QuadraticBezierSegment, SvgArc}; |
| 4 | + |
| 5 | +use super::Turtle; |
| 6 | + |
| 7 | +/// Builds an SVG preview of the toolpath: |
| 8 | +/// - Red solid: tool-on moves (line_to, arc, cubic_bezier, quadratic_bezier) |
| 9 | +/// - Green dashed: rapid/tool-off moves (move_to, when position changes) |
| 10 | +/// |
| 11 | +/// Coordinates arrive in GCode space (Y-flipped, mm). The viewBox is derived |
| 12 | +/// from the accumulated bounding box, so the image is self-consistent. |
| 13 | +#[derive(Debug, Default)] |
| 14 | +pub struct SvgPreviewTurtle { |
| 15 | + tool_on_paths: String, |
| 16 | + rapid_paths: String, |
| 17 | + bounding_box: Option<Box2D<f64>>, |
| 18 | + current_pos: Point<f64>, |
| 19 | + current_tool_on_d: String, |
| 20 | +} |
| 21 | + |
| 22 | +impl SvgPreviewTurtle { |
| 23 | + fn add_box(&mut self, bb: Box2D<f64>) { |
| 24 | + self.bounding_box = Some( |
| 25 | + self.bounding_box |
| 26 | + // Box2D::union discards empty boxes |
| 27 | + .map(|existing| Box2D::from_points([existing.min, existing.max, bb.min, bb.max])) |
| 28 | + .unwrap_or(bb), |
| 29 | + ); |
| 30 | + } |
| 31 | + |
| 32 | + fn add_point(&mut self, p: Point<f64>) { |
| 33 | + self.add_box(Box2D { min: p, max: p }); |
| 34 | + } |
| 35 | + |
| 36 | + fn flush_tool_on(&mut self) { |
| 37 | + if !self.current_tool_on_d.is_empty() { |
| 38 | + writeln!( |
| 39 | + self.tool_on_paths, |
| 40 | + "<path d=\"{}\" stroke=\"red\" fill=\"none\" stroke-width=\"1\" vector-effect=\"non-scaling-stroke\"/>", |
| 41 | + self.current_tool_on_d |
| 42 | + ) |
| 43 | + .unwrap(); |
| 44 | + self.current_tool_on_d.clear(); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + pub fn into_preview(mut self) -> String { |
| 49 | + self.flush_tool_on(); |
| 50 | + const PADDING: f64 = 2.0; |
| 51 | + match self.bounding_box { |
| 52 | + None => { |
| 53 | + "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1 1\"></svg>\n".to_string() |
| 54 | + } |
| 55 | + Some(bb) => { |
| 56 | + let vb_x = bb.min.x - PADDING; |
| 57 | + let vb_y = bb.min.y - PADDING; |
| 58 | + let vb_w = (bb.max.x - bb.min.x + 2.0 * PADDING).max(1.0); |
| 59 | + let vb_h = (bb.max.y - bb.min.y + 2.0 * PADDING).max(1.0); |
| 60 | + let flip_ty = -(bb.min.y + bb.max.y); |
| 61 | + format!( |
| 62 | + "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{vb_x} {vb_y} {vb_w} {vb_h}\">\n<g transform=\"scale(1,-1) translate(0,{flip_ty})\">\n{}{}</g>\n</svg>\n", |
| 63 | + self.rapid_paths, self.tool_on_paths, |
| 64 | + ) |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +impl Turtle for SvgPreviewTurtle { |
| 71 | + fn begin(&mut self) {} |
| 72 | + |
| 73 | + fn end(&mut self) { |
| 74 | + self.flush_tool_on(); |
| 75 | + } |
| 76 | + |
| 77 | + fn comment(&mut self, _: String) {} |
| 78 | + |
| 79 | + fn move_to(&mut self, to: Point<f64>) { |
| 80 | + self.flush_tool_on(); |
| 81 | + if to != self.current_pos { |
| 82 | + writeln!( |
| 83 | + self.rapid_paths, |
| 84 | + "<path d=\"M {},{} L {},{}\" stroke=\"green\" fill=\"none\" stroke-width=\"1\" stroke-dasharray=\"4 3\" vector-effect=\"non-scaling-stroke\"/>", |
| 85 | + self.current_pos.x, self.current_pos.y, to.x, to.y, |
| 86 | + ) |
| 87 | + .unwrap(); |
| 88 | + self.add_point(to); |
| 89 | + } |
| 90 | + self.current_pos = to; |
| 91 | + write!(self.current_tool_on_d, "M {},{} ", to.x, to.y).unwrap(); |
| 92 | + } |
| 93 | + |
| 94 | + fn line_to(&mut self, to: Point<f64>) { |
| 95 | + write!(self.current_tool_on_d, "L {},{} ", to.x, to.y).unwrap(); |
| 96 | + self.add_point(to); |
| 97 | + self.current_pos = to; |
| 98 | + } |
| 99 | + |
| 100 | + fn arc(&mut self, svg_arc: SvgArc<f64>) { |
| 101 | + if svg_arc.is_straight_line() { |
| 102 | + self.line_to(svg_arc.to); |
| 103 | + return; |
| 104 | + } |
| 105 | + write!( |
| 106 | + self.current_tool_on_d, |
| 107 | + "A {},{} {} {} {} {},{} ", |
| 108 | + svg_arc.radii.x, |
| 109 | + svg_arc.radii.y, |
| 110 | + svg_arc.x_rotation.to_degrees(), |
| 111 | + if svg_arc.flags.large_arc { 1 } else { 0 }, |
| 112 | + if svg_arc.flags.sweep { 1 } else { 0 }, |
| 113 | + svg_arc.to.x, |
| 114 | + svg_arc.to.y, |
| 115 | + ) |
| 116 | + .unwrap(); |
| 117 | + self.add_box(svg_arc.to_arc().bounding_box()); |
| 118 | + self.current_pos = svg_arc.to; |
| 119 | + } |
| 120 | + |
| 121 | + fn cubic_bezier(&mut self, cbs: CubicBezierSegment<f64>) { |
| 122 | + write!( |
| 123 | + self.current_tool_on_d, |
| 124 | + "C {},{} {},{} {},{} ", |
| 125 | + cbs.ctrl1.x, cbs.ctrl1.y, cbs.ctrl2.x, cbs.ctrl2.y, cbs.to.x, cbs.to.y, |
| 126 | + ) |
| 127 | + .unwrap(); |
| 128 | + self.add_box(cbs.bounding_box()); |
| 129 | + self.current_pos = cbs.to; |
| 130 | + } |
| 131 | + |
| 132 | + fn quadratic_bezier(&mut self, qbs: QuadraticBezierSegment<f64>) { |
| 133 | + write!( |
| 134 | + self.current_tool_on_d, |
| 135 | + "Q {},{} {},{} ", |
| 136 | + qbs.ctrl.x, qbs.ctrl.y, qbs.to.x, qbs.to.y, |
| 137 | + ) |
| 138 | + .unwrap(); |
| 139 | + self.add_box(qbs.bounding_box()); |
| 140 | + self.current_pos = qbs.to; |
| 141 | + } |
| 142 | +} |
0 commit comments