g_code/emit/
format.rs

1use rust_decimal::prelude::ToPrimitive;
2
3use std::borrow::Borrow;
4use std::fmt::{self, Write as FmtWrite};
5use std::io::Write as IoWrite;
6
7use super::token::Flag;
8use super::{Field, Token, Value};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13struct XorAndPipe<W> {
14    acc: u8,
15    downstream: W,
16}
17
18impl<W> IoWrite for XorAndPipe<W>
19where
20    W: IoWrite,
21{
22    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
23        self.acc = buf.iter().fold(self.acc, |acc, b| acc ^ b);
24        self.downstream.write(buf)
25    }
26
27    fn flush(&mut self) -> std::io::Result<()> {
28        self.downstream.flush()
29    }
30}
31
32impl<W> FmtWrite for XorAndPipe<W>
33where
34    W: FmtWrite,
35{
36    fn write_str(&mut self, s: &str) -> fmt::Result {
37        self.acc = s.bytes().fold(self.acc, |acc, b| acc ^ b);
38        self.downstream.write_str(s)
39    }
40}
41
42impl<W> XorAndPipe<W> {
43    pub fn new(downstream: W) -> Self {
44        Self { acc: 0, downstream }
45    }
46
47    pub fn reset(&mut self) {
48        self.acc = 0;
49    }
50
51    pub fn checksum(&self) -> u8 {
52        self.acc
53    }
54}
55
56#[derive(Debug, Clone, Default)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub struct FormatOptions {
59    /// Include checksums
60    pub checksums: bool,
61    /// Add line numbers
62    pub line_numbers: bool,
63    /// Delimit the start and end of data with percent symbols
64    pub delimit_with_percent: bool,
65    /// Whether to add a newline before each comment
66    ///
67    /// Some g-code viewers like [NCViewer](https://ncviewer.com/)
68    /// do not correctly handle comments on the same line as g-code commands
69    #[cfg_attr(feature = "serde", serde(default))]
70    pub newline_before_comment: bool,
71}
72
73macro_rules! formatter_core {
74    ($program: expr, $opts: ident, $downstream: ident) => {
75        use Token::*;
76        let mut preceded_by_newline = true;
77        let mut line_number = 0usize;
78
79        let mut w = XorAndPipe::new($downstream);
80        if $opts.delimit_with_percent {
81            writeln!(w, "%")?;
82            w.reset();
83        }
84
85        for token in $program {
86            let token = token.borrow();
87            if let Token::Field(ref f) = token {
88                // Can't handle user-provided line numbers
89                if preceded_by_newline && f.letters == "N" {
90                    continue;
91                }
92            }
93
94            if $opts.line_numbers && preceded_by_newline {
95                write!(w, "N{line_number} ")?;
96            }
97
98            match token {
99                Field(f) => {
100                    if !preceded_by_newline {
101                        if matches!(f.letters.as_ref(), "G" | "g" | "M" | "m" | "D" | "d") {
102                            if $opts.checksums {
103                                write!(w, "*{}", w.checksum())?;
104                            }
105                            line_number += 1;
106                            writeln!(w)?;
107                            w.reset();
108                            if $opts.line_numbers {
109                                write!(w, "N{line_number} ")?;
110                            }
111                        } else {
112                            write!(w, " ")?;
113                        }
114                    }
115                    write!(w, "{f}")?;
116                    preceded_by_newline = false;
117                }
118                Flag(f) => {
119                    if !preceded_by_newline {
120                        write!(w, " ")?;
121                    }
122                    write!(w, "{f}")?;
123                }
124                Comment {
125                    is_inline: true,
126                    inner,
127                } => {
128                    write!(w, "({inner})")?;
129                    preceded_by_newline = false;
130                }
131                Comment {
132                    is_inline: false,
133                    inner,
134                } => {
135                    if $opts.checksums {
136                        write!(w, "*{}", w.checksum())?;
137                    }
138                    if !preceded_by_newline && $opts.newline_before_comment {
139                        line_number += 1;
140                        writeln!(w)?;
141                        w.reset();
142                        if $opts.line_numbers {
143                            write!(w, "N{line_number} ")?;
144                        }
145                        if $opts.checksums {
146                            write!(w, "*{}", w.checksum())?;
147                        }
148                    }
149                    line_number += 1;
150                    writeln!(w, ";{inner}")?;
151                    w.reset();
152                    preceded_by_newline = true;
153                }
154            }
155        }
156        // Ensure presence of trailing newline
157        if !preceded_by_newline {
158            if $opts.checksums {
159                write!(w, "*{}", w.checksum())?;
160                w.reset();
161            }
162            writeln!(w)?;
163        }
164        if $opts.delimit_with_percent {
165            write!(w, "%")?;
166        }
167    };
168}
169
170/// Write GCode tokens to an [IoWrite] in a nicely formatted manner
171pub fn format_gcode_io<'a: 'b, 'b, W, I, T>(
172    program: I,
173    opts: FormatOptions,
174    w: W,
175) -> std::io::Result<()>
176where
177    W: IoWrite,
178    I: IntoIterator<Item = T>,
179    T: Borrow<Token<'a>> + 'b,
180{
181    formatter_core!(program.into_iter(), opts, w);
182    Ok(())
183}
184
185/// Write formatted GCode to a [FmtWrite] in a nicely formatted manner
186pub fn format_gcode_fmt<'a: 'b, 'b, W, I, T>(program: I, opts: FormatOptions, w: W) -> fmt::Result
187where
188    W: FmtWrite,
189    I: IntoIterator<Item = T>,
190    T: Borrow<Token<'a>> + 'b,
191{
192    formatter_core!(program.into_iter(), opts, w);
193    Ok(())
194}
195
196impl fmt::Display for Token<'_> {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        use Token::*;
199        match self {
200            Field(field) => write!(f, "{field}"),
201            Flag(flag) => write!(f, "{flag}"),
202            Comment { is_inline, inner } => match is_inline {
203                true => write!(f, "({inner})"),
204                false => write!(f, ";{inner}"),
205            },
206        }
207    }
208}
209
210impl<'a> fmt::Display for Field<'a> {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        write!(f, "{}{}", self.letters, self.value)
213    }
214}
215
216impl<'a> fmt::Display for Flag<'a> {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        f.write_str(&self.letter)
219    }
220}
221
222impl fmt::Display for Value<'_> {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
224        match self {
225            Self::Rational(r) => {
226                // The only way this could've been interpreted
227                // as rational is if there is a trailing decimal point,
228                // so add it back in.
229                if r.fract().is_zero() {
230                    if let Some(unsigned_rep) = r.to_u128() {
231                        return write!(f, "{unsigned_rep}.");
232                    } else if let Some(signed_rep) = r.to_i128() {
233                        return write!(f, "{signed_rep}.");
234                    }
235                }
236                write!(f, "{r}")
237            }
238            Self::Float(float) => write!(f, "{float}"),
239            Self::Integer(i) => write!(f, "{i}"),
240            Self::String(s) => write!(f, "\"{s}\""),
241        }
242    }
243}