Skip to main content

gcode_nom/binary/gcode_block/
svg.rs

1use core::f64;
2use core::f64::consts::TAU;
3use core::fmt::Display;
4
5use crate::ArcParams;
6use crate::MM_PER_ARC_SEGMENT;
7use crate::PositionMode;
8use crate::command::Command;
9use crate::compute_arc;
10use crate::params::head::PosVal;
11
12/// SVG representation of a G-Code file.
13///
14/// wraps the min and max x, y values of the SVG.
15#[derive(Debug, Clone, PartialEq)]
16pub struct Svg {
17    min_x: f64,
18    min_y: f64,
19    max_x: f64,
20    max_y: f64,
21    parts: Vec<String>,
22}
23
24impl Default for Svg {
25    fn default() -> Self {
26        Self {
27            min_x: f64::INFINITY,
28            max_x: -f64::INFINITY,
29            min_y: f64::INFINITY,
30            max_y: -f64::INFINITY,
31            parts: Vec::default(),
32        }
33    }
34}
35
36impl Svg {
37    fn update_view_box(&mut self, proj_x: f64, proj_y: f64) {
38        // Record min max x, y
39        if proj_x > self.max_x {
40            self.max_x = proj_x;
41        }
42        if proj_x < self.min_x {
43            self.min_x = proj_x;
44        }
45        if proj_y > self.max_y {
46            self.max_y = proj_y;
47        }
48        if proj_y < self.min_y {
49            self.min_y = proj_y;
50        }
51    }
52}
53// A line could not be decoded as an G-Code command
54// #[derive(Debug, Clone)]
55// struct GCodeError;
56
57// impl std::fmt::Display for GCodeError {
58//     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59//         write!(f, "invalid g-code statement")
60//     }
61// }
62
63impl Display for Svg {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        let width = self.max_x - self.min_x;
66        let height = self.max_y - self.min_y;
67        // An empty gcode file will not change min_x/y or max_x/y from
68        // its default of +/-INF respectively.
69        //
70        let parameters = if width.is_finite() && height.is_finite() {
71            format!(
72                "width=\"{width}\" height=\"{height}\" viewBox=\"{} {} {} {}\"",
73                self.min_x, self.min_y, width, height
74            )
75        } else {
76            // In this case silently fail by returning a empty SVG element, without a viewBox parameter.
77            String::new()
78        };
79        writeln!(
80            f,
81            "<svg xmlns=\"http://www.w3.org/2000/svg\" {parameters} >"
82        )?;
83        write!(f, "  <path d=\"")?;
84
85        for part in &self.parts {
86            write!(f, "{part}")?;
87        }
88        write!(
89            f,
90            r#""
91style="fill:none;stroke:green;stroke-width:0.05" />
92 </svg>"#
93        )?;
94        Ok(())
95    }
96}
97
98/// Returns a SVG given a collection of G-Code commands.
99///
100/// TODO: Want to iterate over something more flexible
101/// ie. Drop String for something more generic `AsRef<&str>`?
102impl FromIterator<String> for Svg {
103    fn from_iter<I>(iter: I) -> Self
104    where
105        I: IntoIterator<Item = String>,
106    {
107        let mut svg = Self::default();
108
109        // Invalid if the <path>'s d string does not start with a move.
110        svg.parts.push("M0 0".to_string());
111
112        let part_id = Some(0);
113        let mut is_extruding = false;
114        // Positioning mode for all axes (A, B, C), (U, V, W),  (X, Y, Z).
115        let mut position_mode = PositionMode::default();
116        // X and Y position of tool head (before projection).
117        let mut current_x = 0_f64;
118        let mut current_y = 0_f64;
119        let mut current_z = 0_f64;
120
121        let mut origin_x = 0_f64;
122        let mut origin_y = 0_f64;
123        let mut origin_z = 0_f64;
124
125        for line in iter {
126            let (_, command) = Command::parse_line(&line).expect("Command is not parsable");
127
128            match command {
129                // Treat G0 and G1 command identically.
130                //
131                // A G0 is a non-printing move but E is present in files seen in the wild.
132                // (In the assets directory see the gears and benchy2 files.)
133                Command::G0(mut payload) | Command::G1(mut payload) => {
134                    // Candidate value of params X<number> Y<number>
135                    let mut x_param = f64::NAN;
136                    let mut y_param = f64::NAN;
137                    let mut z_param = f64::NAN;
138
139                    for param in payload.drain() {
140                        match param {
141                            PosVal::X(val) => x_param = val,
142                            PosVal::Y(val) => y_param = val,
143                            PosVal::Z(val) => z_param = val,
144                            // Negative values the extruder is "wiping"
145                            // or sucking filament back into the extruder.
146                            PosVal::E(val) => is_extruding = val > 0_f64,
147                            _ => {}
148                        }
149                    }
150
151                    if !x_param.is_nan() {
152                        current_x = match position_mode {
153                            PositionMode::Absolute => x_param,
154                            PositionMode::Relative => current_x + x_param,
155                        }
156                    }
157
158                    if !y_param.is_nan() {
159                        current_y = match position_mode {
160                            PositionMode::Absolute => y_param,
161                            PositionMode::Relative => current_y + y_param,
162                        }
163                    }
164
165                    if !z_param.is_nan() {
166                        current_z = match position_mode {
167                            PositionMode::Absolute => z_param,
168                            PositionMode::Relative => current_z + z_param,
169                        };
170                    }
171
172                    let proj_x =
173                        f64::midpoint(origin_y, current_y) + f64::midpoint(origin_x, current_x);
174                    let proj_y = -(origin_z + current_z) - f64::midpoint(origin_y, current_y)
175                        + f64::midpoint(origin_x, current_x);
176                    svg.update_view_box(proj_x, proj_y);
177
178                    if is_extruding && part_id.is_some() {
179                        svg.parts.push(format!("L{proj_x:.3} {proj_y:.3}"));
180                    } else {
181                        svg.parts.push(format!("M{proj_x:.3} {proj_y:.3}"));
182                    }
183                }
184                Command::G2(arc_form) => {
185                    // Clockwise arc
186                    let ArcParams {
187                        center,
188                        radius,
189                        mut theta_start,
190                        theta_end,
191                    } = compute_arc(current_x, current_y, &arc_form);
192
193                    // Regarding the Ambiguity/Equivalence  of the angles 0 and 2PI
194                    // All values here are in the range 0<=theta<2PI
195                    // We are rotating clockwise
196                    // in this cased the start angle of 0 should be read as 2PI
197                    if theta_start == 0_f64 {
198                        theta_start = TAU;
199                    }
200
201                    let delta_theta = if theta_start < theta_end {
202                        // Adjust for zero crossing
203                        // say 115 -> 304 degrees
204                        // delta_theta = 115 + (360 - 304 ) = 170
205                        theta_start + (TAU - theta_end)
206                    } else {
207                        theta_start - theta_end
208                    };
209                    let total_arc_length = delta_theta * radius;
210                    // n_steps must be a number > 0
211                    let n_steps = (total_arc_length / MM_PER_ARC_SEGMENT).ceil();
212                    let theta_step = delta_theta / n_steps;
213
214                    // x,y are the position of the head in absolute units.
215                    let mut x = f64::NAN;
216                    let mut y = f64::NAN;
217                    // For loop: f64 has a problem with numerical accuracy
218                    // specifically the comparing limit.
219                    // rust idiomatically insists on indexed here
220                    for i in 0..=n_steps as u64 {
221                        let theta = (i as f64).mul_add(-theta_step, theta_start) % TAU;
222                        x = radius.mul_add(theta.cos(), center.0);
223                        y = radius.mul_add(theta.sin(), center.1);
224
225                        let proj_x = (origin_x + x + origin_y + y) / 2_f64;
226                        let proj_y = -(origin_z + current_z) - f64::midpoint(origin_y, y)
227                            + f64::midpoint(origin_x, x);
228                        svg.update_view_box(proj_x, proj_y);
229                        match position_mode {
230                            PositionMode::Absolute => {
231                                svg.parts.push(format!("L{proj_x:.3} {proj_y:.3}"));
232                            }
233                            PositionMode::Relative => {
234                                svg.parts.push(format!("l{proj_x:.3} {proj_y:.3}"));
235                            }
236                        }
237                    }
238
239                    current_x = x;
240                    current_y = y;
241                }
242                Command::G3(arc_form) => {
243                    // Anti-Clockwise arc
244                    let ArcParams {
245                        center,
246                        radius,
247                        theta_start,
248                        mut theta_end,
249                    } = compute_arc(current_x, current_y, &arc_form);
250
251                    // Regarding the Ambiguity/Equivalence  of the angles 0 and 2PI
252                    // All values here are in the range 0<=theta<2PI
253                    // We are rotating anticlockwise
254                    // in this cased the final angle of 0 should be read as 2PI
255                    if theta_end == 0_f64 {
256                        theta_end = TAU;
257                    }
258
259                    let delta_theta = if theta_start > theta_end {
260                        // Adjust for zero crossing
261                        // say 306 -> 115 degrees
262                        // delta_theta = (360 - 305 ) + 115 = 170
263                        TAU - theta_start + theta_end
264                    } else {
265                        theta_end - theta_start
266                    };
267                    let total_arc_length = delta_theta * radius;
268                    // n_steps must be a number > 0
269                    let n_steps = (total_arc_length / MM_PER_ARC_SEGMENT).ceil();
270                    let theta_step = delta_theta / n_steps;
271
272                    // For loop with f64 have a problem with numerical accuracy
273                    // specifically the comparing limit.
274                    // rust idiomatically insists on indexed here
275                    let mut x = f64::NAN;
276                    let mut y = f64::NAN;
277                    for i in 0..=n_steps as u64 {
278                        let theta = (i as f64).mul_add(theta_step, theta_start) % TAU;
279                        x = radius.mul_add(theta.cos(), center.0);
280                        y = radius.mul_add(theta.sin(), center.1);
281
282                        let proj_x = (origin_x + x + origin_y + y) / 2.;
283                        let proj_y = -(origin_z + current_z) - f64::midpoint(origin_y, y)
284                            + f64::midpoint(origin_x, x);
285                        svg.update_view_box(proj_x, proj_y);
286                        match position_mode {
287                            PositionMode::Absolute => {
288                                svg.parts.push(format!("L{proj_x:.3} {proj_y:.3}"));
289                            }
290                            PositionMode::Relative => {
291                                svg.parts.push(format!("l{proj_x:.3} {proj_y:.3}"));
292                            }
293                        }
294                    }
295
296                    current_x = x;
297                    current_y = y;
298                }
299                Command::G90 => position_mode = PositionMode::Absolute,
300                Command::G91 => position_mode = PositionMode::Relative,
301
302                // If the current position is at X=4 and G92 X7 is programmed,
303                //  the current position is redefined as X=7, effectively
304                // moving the origin of the coordinate system -3 units in X.""
305                Command::G92(mut params) => {
306                    // The extrude rate is going to zero
307                    // enter MoveMode ..ie not laying down filament.
308                    for param in params.drain() {
309                        match param {
310                            PosVal::E(val) => {
311                                // Negative values the extruder is "wiping"
312                                // or sucking filament back into the extruder.
313                                is_extruding = val > 0_f64;
314                            }
315                            PosVal::X(val) => match position_mode {
316                                PositionMode::Absolute => {
317                                    origin_x = current_x - val;
318                                    current_x = val;
319                                }
320                                PositionMode::Relative => {
321                                    unimplemented!("Relative position mode origin adjust ");
322                                }
323                            },
324                            PosVal::Y(val) => match position_mode {
325                                PositionMode::Absolute => {
326                                    origin_y = current_x - val;
327                                    current_y = val;
328                                }
329                                PositionMode::Relative => {
330                                    unimplemented!("Relative position mode origin adjust ");
331                                }
332                            },
333                            PosVal::Z(val) => match position_mode {
334                                PositionMode::Absolute => {
335                                    origin_z = current_z - val;
336                                    current_z = val;
337                                }
338                                PositionMode::Relative => {
339                                    unimplemented!("Relative position mode origin adjust ");
340                                }
341                            },
342                            _ => { /* Silently drop. */ }
343                        }
344                    }
345
346                    // Set Position is by definition a move only.
347                    let proj_x = (origin_x + current_x + origin_y + current_y) / 2.;
348                    let proj_y = -(origin_z + current_z) - f64::midpoint(origin_y, current_y)
349                        + f64::midpoint(origin_x, current_x);
350                    svg.update_view_box(proj_x, proj_y);
351
352                    svg.parts.push(format!("M{proj_x} {proj_y}"));
353                }
354                _ => {}
355            }
356        }
357
358        svg
359    }
360}
361
362#[cfg(test)]
363mod svg {
364    use super::*;
365    use crate::command::Command;
366    use insta::assert_debug_snapshot;
367
368    // The first few lines of assets/3dBench.gcode
369    static INPUT: &str = r"
370; generated by Slic3r 1.2.9 on 2015-10-01 at 20:51:53
371
372; external perimeters extrusion width = 0.40mm
373; perimeters extrusion width = 0.67mm
374; infill extrusion width = 0.67mm
375; solid infill extrusion width = 0.67mm
376; top infill extrusion width = 0.67mm
377
378M107
379M190 S65 ; set bed temperature
380M104 S205 ; set temperature
381G28 ; home all axes
382G1 Z5 F5000 ; lift nozzle
383M109 S205 ; wait for temperature to be reached
384G21 ; set units to millimeters
385G90 ; use absolute coordinates
386M82 ; use absolute distances for extrusion
387G92 E0
388G1 E-1.00000 F1800.00000
389G92 E0
390G1 Z0.350 F7800.000
391";
392
393    #[test]
394    fn nothing_unhandled() {
395        // The first few lines of the benchy file must be recognized.
396        for line in INPUT.lines() {
397            assert!(Command::parse_line(line).is_ok());
398        }
399    }
400
401    #[test]
402    fn arc_clockwise() {
403        // SNAPSHOT tests
404        //
405        // Simple pattern essential for code coverage
406        //
407        // Ensures calculated theta values are in the range 0..360
408        // as measured in anticlockwise from the positive x-axis.
409        //
410        // 0 and 360 are the same point
411        // This test asserts that the cases where 360 must be used are correct.
412        let buffer = include_str!("../../../../assets/g3_box_rounded_anticlockwise.gcode");
413        let svg = buffer
414            .lines()
415            .map(std::string::ToString::to_string)
416            .collect::<Svg>();
417        assert_debug_snapshot!(svg);
418    }
419
420    #[test]
421    fn arc_anti_clockwise() {
422        // SNAPSHOT tests
423        //
424        // Simple pattern essential for code coverage
425        //
426        // Ensures calculated theta values are in the range 0..360
427        // as measured in anticlockwise from the positive x-axis.
428        //
429        // 0 and 360 are the same point
430        // This test asserts that the cases where 360 must be used are correct.
431        let buffer = include_str!("../../../../assets/g2_box_nibble_clockwise.gcode");
432        let svg = buffer
433            .lines()
434            .map(std::string::ToString::to_string)
435            .collect::<Svg>();
436        assert_debug_snapshot!(svg);
437    }
438
439    #[test]
440    fn arc_demo() {
441        // SNAPSHOT tests
442        let buffer = include_str!("../../../../assets/arc_demo.gcode");
443        let svg = buffer
444            .lines()
445            .map(std::string::ToString::to_string)
446            .collect::<Svg>();
447        assert_debug_snapshot!(svg);
448    }
449
450    #[test]
451    fn zero_crossing() {
452        // SNAPSHOT tests
453        //
454        // Complex model with lots of curves.
455        //
456        // NB This is the only test that covers both clockwise and anticlockwise
457        // zero crossings.
458        let buffer = include_str!("../../../../assets/both.gcode");
459        let svg = buffer
460            .lines()
461            .map(std::string::ToString::to_string)
462            .collect::<Svg>();
463        assert_debug_snapshot!(svg);
464    }
465}