1use std::{collections::HashMap, io::Write};
2
3use anyhow::{bail, Result};
4use nalgebra::{Matrix3, Point2};
5
6const RETRACT: f64 = 0.25;
7
8pub struct GcodeState {
9 pub stepover: f64,
10 pub depth_per_pass: f64,
11 pub feed_rate: f64,
12 pub plunge_rate: f64,
13 pub cutter_diameter: f64,
14
15 pub transformation: Matrix3<f64>,
16
17 program: Vec<GCode>,
18}
19
20impl GcodeState {
21 pub fn new() -> GcodeState {
22 GcodeState {
23 stepover: 0.0,
24 depth_per_pass: 0.0,
25 feed_rate: 0.0,
26 plunge_rate: 0.0,
27 cutter_diameter: 0.0,
28
29 transformation: Matrix3::identity(),
30
31 program: Vec::new(),
32 }
33 }
34
35 pub fn write_header(&mut self) {
36 self.program.push(GCode::AbsoluteDistanceMode);
37 self.program.push(GCode::MetricUnits);
38 self.program.push(GCode::Comment("Move to safe Z".to_string()));
39 self.program.push(GCode::MoveInAbsoluteCoordinates(Box::new(GCode::RapidMove {
40 x: None,
41 y: None,
42 z: Some(-5.0),
43 })));
44 self.program.push(GCode::SpindleStop);
45 }
46
47 pub fn set_rpm(&mut self, rpm: f64) {
48 self.program.push(GCode::SpindleOnCW { rpm });
49 }
50
51 pub fn write_comment(&mut self, comment: &str) {
52 self.program.push(GCode::Comment(comment.to_string()));
53 }
54
55 pub fn cutting_move(&mut self, x: f64, y: f64, z: Option<f64>) {
56 let xy = Point2::new(x, y);
57 let xy = self.transformation.transform_point(&xy);
58
59 self.program.push(GCode::LinearMove {
60 x: Some(xy.x),
61 y: Some(xy.y),
62 z,
63 feed: self.feed_rate,
64 });
65 }
66
67 pub fn plunge(&mut self, z: f64) {
68 self.program.push(GCode::LinearMove {
69 x: None,
70 y: None,
71 z: Some(z),
72 feed: self.plunge_rate,
73 });
74 }
75
76 pub fn rapid_move(&mut self, x: f64, y: f64, z: Option<f64>) {
77 let xy = Point2::new(x, y);
78 let xy = self.transformation.transform_point(&xy);
79
80 self.program.push(GCode::RapidMove {
81 x: Some(xy.x),
82 y: Some(xy.y),
83 z,
84 });
85 }
86
87 pub fn rapid_move_xy(&mut self, x: f64, y: f64) {
88 self.rapid_move(x, y, None)
89 }
90
91 pub fn arc_cut(&mut self, x: f64, y: f64, cx: f64, cy: f64) {
92 let xy = self.transformation.transform_point(&Point2::new(x, y));
93 let cxy = self.transformation.transform_point(&Point2::new(cx, cy));
94
95 self.program.push(GCode::CounterClockwiseArc {
96 x: xy.x,
97 y: xy.y,
98 cx: cxy.x,
99 cy: cxy.y,
100 feed: self.feed_rate,
101 });
102 }
103
104 pub fn drill(&mut self, x: f64, y: f64, depth: f64) {
105 self.rapid_move_xy(x, y);
106 self.rapid_move(x, y, Some(0.25));
107 self.plunge(-depth);
108 self.rapid_move(x, y, Some(5.0));
109 }
110
111 pub fn contour_line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, depth: f64) -> Result<()> {
112 if self.depth_per_pass <= 0.0 {
113 bail!("Invalid depth per pass");
114 }
115
116 let n_passes = (depth / self.depth_per_pass).ceil() as i64;
117
118 for layer in 1..=n_passes {
119 let z = -(depth * layer as f64 / n_passes as f64);
120 self.rapid_move_xy(x1, y1);
121 self.plunge(z);
122 self.cutting_move(x2, y2, None);
123 self.rapid_move(x2, y2, Some(5.0));
124 }
125
126 Ok(())
127 }
128
129 pub fn circle_pocket(&mut self, cx: f64, cy: f64, diameter: f64, depth: f64) -> Result<()> {
130 if diameter <= self.cutter_diameter {
131 bail!("Diameter must be greater than cutter diameter");
132 }
133
134 if self.depth_per_pass <= 0.0 {
135 bail!("Invalid depth per pass: {}", self.depth_per_pass);
136 }
137
138 if self.cutter_diameter <= 0.0 {
139 bail!("Invalid cutter diameter: {}", self.cutter_diameter);
140 }
141
142 let n_circles = (diameter / self.cutter_diameter).floor() as i64;
143 let n_passes = (depth / self.depth_per_pass).ceil() as i64;
144 let x_offset = (diameter / 2.0) - (self.cutter_diameter * n_circles as f64 / 2.0);
145
146 self.rapid_move_xy(cx + x_offset, cy);
147 self.plunge(2.5);
148
149 for i in 1..=n_passes {
150 self.plunge(-(depth * i as f64 / n_passes as f64));
151
152 for j in 1..=n_circles {
153 self.arc_cut(cx - x_offset - self.cutter_diameter * (j - 1) as f64 / 2.0, cy, cx, cy);
154
155 if j == n_circles {
156 self.arc_cut(cx + x_offset + self.cutter_diameter * (j - 1) as f64 / 2.0, cy, cx, cy);
157 } else {
158 self.arc_cut(cx + x_offset + self.cutter_diameter * j as f64 / 2.0, cy, cx + self.cutter_diameter / 4.0, cy);
159 }
160 }
161
162 if i < n_passes {
163 self.cutting_move(cx + x_offset, cy, None);
164 }
165 }
166
167 self.rapid_move(cx + x_offset + self.cutter_diameter * (n_circles - 1) as f64 / 2.0, cy, Some(5.0));
168
169 Ok(())
170 }
171
172 pub fn finish<W: Write>(&mut self, writer: W) -> Result<()> {
173 self.program.push(GCode::ProgramEnd);
174 self.write_program(writer)
175 }
176
177 fn write_program<W: Write>(&self, mut writer: W) -> Result<()> {
178 let mut last_command = None;
179 let mut state = HashMap::new();
180
181 for line in &self.program {
182 if let GCode::Comment(comment) = &line {
183 writer.write_all(format!("({})\n", comment).as_bytes())?;
184 continue;
185 }
186 let words = line.to_words(state.get(&'X').cloned(), state.get(&'Y').cloned())?;
187 let mut pieces = Vec::new();
188 let mut g53 = false;
189
190 for word in &words {
191 match word {
192 GcodeWord::G(g) => {
193 if *g == 53 {
194 g53 = true;
195 last_command = None;
196 }
197
198 if last_command != Some(*word) {
199 pieces.push(*word);
200 }
201 },
202 GcodeWord::M(_) => {
203 if last_command != Some(*word) {
204 pieces.push(*word);
205 }
206 },
207 GcodeWord::X(v) | GcodeWord::Y(v) | GcodeWord::Z(v) | GcodeWord::I(v) | GcodeWord::J(v) | GcodeWord::F(v) | GcodeWord::S(v) => {
208 if g53 || state.get(&word.to_char()) != Some(v) {
209 pieces.push(*word);
210 }
211 },
212 }
213 }
214
215 if pieces.is_empty() || line.is_empty(&pieces) {
217 continue;
218 }
219
220 writer.write_all(pieces.iter().map(|w| w.to_string()).collect::<Vec<String>>().join(" ").as_bytes())?;
221 writer.write_all(b"\n")?;
222
223 for word in pieces {
225 match word {
226 GcodeWord::G(_) | GcodeWord::M(_) => {
227 if !g53 {
228 last_command = Some(word)
229 }
230 },
231 GcodeWord::X(v) | GcodeWord::Y(v) | GcodeWord::Z(v) | GcodeWord::I(v) | GcodeWord::J(v) | GcodeWord::F(v) | GcodeWord::S(v) => {
232 if !g53 {
233 state.insert(word.to_char(), v);
234 } else {
235 state.remove(&word.to_char());
237 }
238 },
239 }
240 }
241 }
242
243 Ok(())
244 }
245
246 pub fn groove_pocket(&mut self, x: f64, y: f64, width: f64, height: f64, depth: f64) -> Result<()> {
249 if self.stepover <= 0.0 {
250 bail!("Invalid stepover: {}", self.stepover);
251 }
252
253 if self.depth_per_pass <= 0.0 {
254 bail!("Invalid depth per pass: {}", self.depth_per_pass);
255 }
256
257 let mut pattern = Vec::new();
259
260 let mut c_x = x + self.cutter_diameter / 2.0;
261 let mut c_y = y + self.cutter_diameter / 2.0;
262 let mut c_width = width - self.cutter_diameter;
263 let mut c_height = height - self.cutter_diameter;
264 let n_passes = (depth / self.depth_per_pass).ceil() as i64;
265 let n_loops = 1 + (((width / 2.0) - self.cutter_diameter) / self.stepover).ceil() as i64;
266
267 for _ in 0..n_loops {
268 pattern.push((c_x, c_y));
269 c_x += c_width;
270 pattern.push((c_x, c_y));
271 c_y += c_height;
272 pattern.push((c_x, c_y));
273 c_x -= c_width;
274 pattern.push((c_x, c_y));
275 c_y -= c_height;
276 pattern.push((c_x, c_y));
277 c_x += self.stepover;
278 c_y += self.stepover;
279 c_width -= 2.0 * self.stepover;
280 c_height -= 2.0 * self.stepover;
281 }
282
283 pattern.reverse();
284
285 for layer in 1..=n_passes {
286 let z = -(depth * layer as f64 / n_passes as f64);
287 let (x, y) = pattern[0];
288
289 if layer == 1 {
290 self.rapid_move_xy(x, y);
291 self.rapid_move(x, y, Some(5.0));
292 self.plunge(z);
293 } else {
294 self.rapid_move_xy(x, y);
295 self.plunge(z);
296 }
297
298 for (x, y) in pattern.iter().skip(1) {
299 self.cutting_move(*x, *y, None);
300 }
301
302 if layer == n_passes {
303 self.rapid_move(x, y, Some(5.0));
304 } else {
305 self.rapid_move(x, y, Some(z + RETRACT));
306 }
307 }
308
309 Ok(())
310 }
311}
312
313
314fn format_number(f: f64) -> String {
315 let mut s = format!("{:.3}", f);
316 let t = s.trim_end_matches('0').trim_end_matches('.').len();
317 s.truncate(t);
318 s
319}
320
321
322#[derive(PartialEq, Clone, Debug)]
323enum GCode {
324 Comment(String),
325 RapidMove {
326 x: Option<f64>,
327 y: Option<f64>,
328 z: Option<f64>,
329 }, LinearMove {
331 x: Option<f64>,
332 y: Option<f64>,
333 z: Option<f64>,
334 feed: f64,
335 }, CounterClockwiseArc {
337 x: f64,
338 y: f64,
339 cx: f64,
340 cy: f64,
341 feed: f64,
342 }, MetricUnits, MoveInAbsoluteCoordinates(Box<Self>), AbsoluteDistanceMode, ProgramEnd, SpindleOnCW {
349 rpm: f64,
350 }, SpindleStop, }
353
354#[derive(PartialEq, Clone, Debug, Copy)]
355enum GcodeWord {
356 G(u8),
357 M(u8),
358 F(f64),
359 I(f64),
360 J(f64),
361 S(f64),
362 X(f64),
363 Y(f64),
364 Z(f64),
365}
366
367impl GCode {
368 fn to_words(&self, current_x: Option<f64>, current_y: Option<f64>) -> Result<Vec<GcodeWord>> {
369 Ok(match self {
370 GCode::RapidMove { x, y, z } => vec![Some(GcodeWord::G(0)), x.map(GcodeWord::X), y.map(GcodeWord::Y), z.map(GcodeWord::Z)]
371 .into_iter()
372 .flatten()
373 .collect(),
374 GCode::LinearMove { x, y, z, feed } => vec![
375 Some(GcodeWord::G(1)),
376 x.map(GcodeWord::X),
377 y.map(GcodeWord::Y),
378 z.map(GcodeWord::Z),
379 Some(GcodeWord::F(*feed)),
380 ]
381 .into_iter()
382 .flatten()
383 .collect(),
384 GCode::CounterClockwiseArc { x, y, cx, cy, feed } => {
385 if let (Some(current_x), Some(current_y)) = (current_x, current_y) {
386 vec![
387 Some(GcodeWord::G(3)),
388 Some(GcodeWord::X(*x)),
389 Some(GcodeWord::Y(*y)),
390 Some(GcodeWord::I(*cx - current_x)),
391 Some(GcodeWord::J(*cy - current_y)),
392 Some(GcodeWord::F(*feed)),
393 ]
394 .into_iter()
395 .flatten()
396 .collect()
397 } else {
398 bail!("Cannot generate G3 arc without current position");
399 }
400 },
401 GCode::MetricUnits => vec![GcodeWord::G(21)],
402 GCode::MoveInAbsoluteCoordinates(gcode) => {
403 let mut words = gcode.to_words(current_x, current_y)?;
404 words.insert(0, GcodeWord::G(53));
405 words
406 },
407 GCode::AbsoluteDistanceMode => vec![GcodeWord::G(90)],
408 GCode::ProgramEnd => vec![GcodeWord::M(2)],
409 GCode::SpindleOnCW { rpm } => vec![GcodeWord::M(3), GcodeWord::S(*rpm)],
410 GCode::SpindleStop => vec![GcodeWord::M(5)],
411 GCode::Comment(_) => unreachable!(),
412 })
413 }
414
415 fn is_empty(&self, words: &[GcodeWord]) -> bool {
416 let pos_present = words.iter().any(|w| matches!(w, GcodeWord::X(_) | GcodeWord::Y(_) | GcodeWord::Z(_)));
417
418 let s_present = words.iter().any(|w| matches!(w, GcodeWord::S(_)));
419
420 match self {
421 GCode::Comment(_) => unreachable!(),
422 GCode::RapidMove { x: _, y: _, z: _ } => !pos_present,
423 GCode::LinearMove { x: _, y: _, z: _, feed: _ } => !pos_present,
424 GCode::CounterClockwiseArc {
425 x: _,
426 y: _,
427 cx: _,
428 cy: _,
429 feed: _,
430 } => !pos_present,
431 GCode::MetricUnits | GCode::AbsoluteDistanceMode | GCode::ProgramEnd | GCode::SpindleStop | GCode::MoveInAbsoluteCoordinates(_) => false,
432 GCode::SpindleOnCW { rpm: _ } => !s_present,
433 }
434 }
435}
436
437impl ToString for GcodeWord {
438 fn to_string(&self) -> String {
439 match self {
440 GcodeWord::G(n) => format!("G{}", n),
441 GcodeWord::M(n) => format!("M{:02}", n),
442 GcodeWord::F(n) => format!("F{}", format_number(*n)),
443 GcodeWord::I(n) => format!("I{}", format_number(*n)),
444 GcodeWord::J(n) => format!("J{}", format_number(*n)),
445 GcodeWord::S(n) => format!("S{}", format_number(*n)),
446 GcodeWord::X(n) => format!("X{}", format_number(*n)),
447 GcodeWord::Y(n) => format!("Y{}", format_number(*n)),
448 GcodeWord::Z(n) => format!("Z{}", format_number(*n)),
449 }
450 }
451}
452
453impl GcodeWord {
454 fn to_char(self) -> char {
455 match self {
456 GcodeWord::G(_) => 'G',
457 GcodeWord::M(_) => 'M',
458 GcodeWord::F(_) => 'F',
459 GcodeWord::I(_) => 'I',
460 GcodeWord::J(_) => 'J',
461 GcodeWord::S(_) => 'S',
462 GcodeWord::X(_) => 'X',
463 GcodeWord::Y(_) => 'Y',
464 GcodeWord::Z(_) => 'Z',
465 }
466 }
467}