cnccoder/
program.rs

1//! The program module contains the highest level components. They are used to
2//! structure the a CNC programs, store and order the cuts and tools.
3//!
4//! Programs are built by extending a program with various tool contexts.
5//! Once a program is complete it can be converted to G-code with
6//! the [.to_gcode()](struct.Program.html#method.to_gcode) method or written
7//! to disk with the [write_project](../filesystem/fn.write_project.html)
8//! function.
9//!
10//! Example:
11//! ```
12//! use anyhow::Result;
13//! use cnccoder::prelude::*;
14//!
15//! fn main() -> Result<()> {
16//!     let mut program = Program::new(
17//!         Units::Metric,
18//!         10.0,
19//!         50.0,
20//!     );
21//!
22//!     program.set_name("cylinder plane");
23//!
24//!     let tool = Tool::cylindrical(
25//!         Units::Metric,
26//!         20.0,
27//!         10.0,
28//!         Direction::Clockwise,
29//!         20000.0,
30//!         5_000.0
31//!     );
32//!
33//!     let mut context = program.context(tool);
34//!
35//!     context.append_cut(Cut::plane(
36//!         Vector3::new(0.0, 0.0, 3.0),
37//!         Vector2::new(100.0, 100.0),
38//!         0.0,
39//!         1.0,
40//!     ));
41//!
42//!     println!("G-code: {}", program.to_gcode()?);
43//!
44//!     write_project(&program, 0.5)?;
45//!
46//!     Ok(())
47//! }
48//! ```
49
50use std::cell::RefCell;
51use std::collections::hash_map::Entry::Vacant;
52use std::collections::HashMap;
53use std::rc::Rc;
54use std::time::Duration;
55
56use anyhow::{anyhow, Result};
57use time::OffsetDateTime;
58
59use crate::cuts::*;
60use crate::instructions::*;
61use crate::prelude::round_precision;
62use crate::tools::*;
63use crate::types::*;
64use crate::utils::scale;
65
66fn format_number(value: f64) -> String {
67    if value.is_finite() {
68        let new_value = round_precision(value);
69        if new_value.is_finite() {
70            new_value
71        } else {
72            0.0
73        }
74    } else {
75        0.0
76    }
77    .to_string()
78}
79
80/// A high level respresentation of a CNC program operation, Cut, Comment, Message, or Empty.
81#[derive(Debug, Clone)]
82pub enum Operation {
83    /// A high level cut operation.
84    Cut(Cut),
85    /// An empty operation.
86    Empty(Empty),
87    /// A program comment.
88    Comment(Comment),
89    /// A program message.
90    Message(Message),
91}
92
93impl Operation {
94    /// The bounds of the operation.
95    pub fn bounds(&self) -> Bounds {
96        match self {
97            Self::Cut(o) => o.bounds(),
98            Self::Empty(_) => Bounds::default(),
99            Self::Comment(_) => Bounds::default(),
100            Self::Message(_) => Bounds::default(),
101        }
102    }
103
104    /// Converts operation to G-code instructions.
105    pub fn to_instructions(&self, context: InnerContext) -> Result<Vec<Instruction>> {
106        match self {
107            Self::Cut(o) => o.to_instructions(context),
108            Self::Empty(_) => Ok(vec![Instruction::Empty(Empty {})]),
109            Self::Comment(i) => Ok(vec![Instruction::Comment(i.clone())]),
110            Self::Message(i) => Ok(vec![Instruction::Message(i.clone())]),
111        }
112    }
113}
114
115/// A program context that keeps the state data for operations paired with a specific tool.
116/// The reason for grouping the operations per tool is to reduce the amound of tool
117/// changes, which is expecially useful for CNC machines that needs manual tool changes.
118///
119/// This struct is mainly for internal use, most of the time you would use the ToolContext
120/// struct instead.
121#[doc(hidden)]
122#[derive(Debug, Clone)]
123pub struct InnerContext {
124    units: Units,
125    tool: Tool,
126    z_safe: f64,
127    z_tool_change: f64,
128    operations: Vec<Operation>,
129}
130
131impl InnerContext {
132    /// Creates a new `Context` struct.
133    pub fn new(units: Units, tool: &Tool, z_safe: f64, z_tool_change: f64) -> Self {
134        Self {
135            units,
136            tool: *tool,
137            z_safe,
138            z_tool_change,
139            operations: vec![],
140        }
141    }
142
143    /// Applies operations from one context to this context.
144    ///
145    /// Returns error if tool or units are not the same in both contexts.
146    pub fn merge(&mut self, context: InnerContext) -> Result<()> {
147        if self.units != context.units {
148            return Err(anyhow!("Failed to merge due to mismatching units"));
149        }
150
151        if self.tool != context.tool {
152            return Err(anyhow!("Failed to merge due to mismatching tools"));
153        }
154
155        self.z_safe = context.z_safe;
156        self.z_tool_change = context.z_tool_change;
157
158        for operation in context.operations {
159            self.operations.push(operation);
160        }
161
162        Ok(())
163    }
164
165    /// Appends an operation to the context.
166    pub fn append(&mut self, operation: Operation) {
167        self.operations.push(operation);
168    }
169
170    /// Appends a cut operation to the context.
171    pub fn append_cut(&mut self, cut: Cut) {
172        self.append(Operation::Cut(cut));
173    }
174
175    /// Returns the units used by the context.
176    pub fn units(&self) -> Units {
177        self.units
178    }
179
180    /// Returns the tool used by the context.
181    pub fn tool(&self) -> Tool {
182        self.tool
183    }
184
185    /// Returns the z safe value set for this context.
186    ///
187    /// The value indicates the z height where the machine tool can safely travel
188    /// in the x and y axis without colliding with the workpiece.
189    pub fn z_safe(&self) -> f64 {
190        self.z_safe
191    }
192
193    /// Returns the z height position used for manual tool change.
194    pub fn z_tool_change(&self) -> f64 {
195        self.z_tool_change
196    }
197
198    /// Returns the bounds for the context
199    pub fn bounds(&self) -> Bounds {
200        let mut bounds = Bounds::minmax();
201
202        for operation in self.operations.iter() {
203            let operation_bounds = operation.bounds();
204            bounds.min.x = if bounds.min.x > operation_bounds.min.x {
205                operation_bounds.min.x
206            } else {
207                bounds.min.x
208            };
209            bounds.min.y = if bounds.min.y > operation_bounds.min.y {
210                operation_bounds.min.y
211            } else {
212                bounds.min.y
213            };
214            bounds.min.z = if bounds.min.z > operation_bounds.min.z {
215                operation_bounds.min.z
216            } else {
217                bounds.min.z
218            };
219            bounds.max.x = if bounds.max.x < operation_bounds.max.x {
220                operation_bounds.max.x
221            } else {
222                bounds.max.x
223            };
224            bounds.max.y = if bounds.max.y < operation_bounds.max.y {
225                operation_bounds.max.y
226            } else {
227                bounds.max.y
228            };
229            bounds.max.z = if bounds.max.z < operation_bounds.max.z {
230                operation_bounds.max.z
231            } else {
232                bounds.max.z
233            };
234        }
235
236        bounds
237    }
238
239    /// Returns all operations for this context.
240    pub fn operations(&self) -> Vec<Operation> {
241        self.operations.clone()
242    }
243
244    /// Converts context to G-code instructions.
245    pub fn to_instructions(&self) -> Result<Vec<Instruction>> {
246        let mut instructions = vec![];
247
248        for operation in &self.operations {
249            instructions.append(&mut operation.to_instructions((*self).clone())?);
250        }
251
252        Ok(instructions)
253    }
254}
255
256/// A program tool context that updates the state data for operations paired with a specific
257/// tool. The reason for grouping the operations per tool is to reduce the amound of tool
258/// changes, which is expecially useful for CNC machines that needs manual tool changes.
259#[derive(Debug, Clone)]
260pub struct Context<'a> {
261    tool: Tool,
262    program: Rc<RefCell<&'a Program>>,
263}
264
265impl<'a> Context<'a> {
266    /// Applies operations from one context to this context.
267    ///
268    /// Returns error if tool or units are not the same in both contexts.
269    pub fn merge(&mut self, context: Context) -> Result<()> {
270        let program = self.program.borrow();
271
272        let mut binding = program.contexts.borrow_mut();
273        let program_context = binding.get_mut(&self.tool).unwrap();
274
275        let binding = context.program.borrow().contexts.borrow();
276        let merge_context = binding.get(&context.tool()).unwrap();
277
278        program_context.merge(merge_context.clone())
279    }
280
281    /// Appends an operation to the context.
282    pub fn append(&mut self, operation: Operation) {
283        let program = self.program.borrow();
284        let mut binding = program.contexts.borrow_mut();
285        let context = binding.get_mut(&self.tool).unwrap();
286        context.append(operation);
287    }
288
289    /// Appends a cut operation to the context.
290    pub fn append_cut(&mut self, cut: Cut) {
291        self.append(Operation::Cut(cut));
292    }
293
294    /// Returns the units used by the context.
295    pub fn units(&self) -> Units {
296        let program = self.program.borrow();
297        let mut binding = program.contexts.borrow_mut();
298        let context = binding.get_mut(&self.tool).unwrap();
299        context.units()
300    }
301
302    /// Returns the tool used by the context.
303    pub fn tool(&self) -> Tool {
304        self.tool
305    }
306
307    /// Returns the z safe value set for this context.
308    ///
309    /// The value indicates the z height where the machine tool can safely travel
310    /// in the x and y axis without colliding with the workpiece.
311    pub fn z_safe(&self) -> f64 {
312        let program = self.program.borrow();
313        let mut binding = program.contexts.borrow_mut();
314        let context = binding.get_mut(&self.tool).unwrap();
315        context.z_safe()
316    }
317
318    /// Returns the z height position used for manual tool change.
319    pub fn z_tool_change(&self) -> f64 {
320        let program = self.program.borrow();
321        let mut binding = program.contexts.borrow_mut();
322        let context = binding.get_mut(&self.tool).unwrap();
323        context.z_tool_change()
324    }
325
326    /// Returns the bounds for this context.
327    pub fn bounds(&self) -> Bounds {
328        let program = self.program.borrow();
329        let mut binding = program.contexts.borrow_mut();
330        let context = binding.get_mut(&self.tool).unwrap();
331        context.bounds()
332    }
333
334    /// Returns all operations for this context.
335    pub fn operations(&self) -> Vec<Operation> {
336        let program = self.program.borrow();
337        let mut binding = program.contexts.borrow_mut();
338        let context = binding.get_mut(&self.tool).unwrap();
339        context.operations()
340    }
341
342    /// Converts context to G-code instructions.
343    pub fn to_instructions(&self) -> Result<Vec<Instruction>> {
344        let program = self.program.borrow();
345        let mut binding = program.contexts.borrow_mut();
346        let context = binding.get_mut(&self.tool).unwrap();
347        context.to_instructions()
348    }
349}
350
351#[derive(Debug, Clone)]
352struct ProgramMeta {
353    name: String,
354    description: Vec<String>,
355    created_on: OffsetDateTime,
356    created_by: String,
357    generator: String,
358}
359
360impl ProgramMeta {
361    fn to_instructions(&self) -> Vec<Instruction> {
362        let mut instructions = vec![];
363
364        instructions.push(Instruction::Comment(Comment {
365            text: format!("Name: {}", self.name),
366        }));
367
368        instructions.push(Instruction::Comment(Comment {
369            text: format!("Created on: {}", self.created_on),
370        }));
371
372        instructions.push(Instruction::Comment(Comment {
373            text: format!("Created by: {}", self.created_by),
374        }));
375
376        instructions.push(Instruction::Comment(Comment {
377            text: format!("Generator: {}", self.generator),
378        }));
379
380        for description in &self.description {
381            instructions.push(Instruction::Comment(Comment {
382                text: format!("Description: {}", description),
383            }));
384        }
385
386        instructions
387    }
388}
389
390impl Default for ProgramMeta {
391    fn default() -> Self {
392        let username = username::get_user_name().unwrap_or("unknown".into());
393        let hostname = hostname::get()
394            .unwrap_or("unknown".into())
395            .to_string_lossy()
396            .to_string();
397
398        let args: Vec<String> = std::env::args().collect();
399
400        Self {
401            name: moby_name_gen::random_name(),
402            description: Vec::new(),
403            created_on: OffsetDateTime::now_local().unwrap_or(OffsetDateTime::now_utc()),
404            created_by: format!("{username}@{hostname}").to_string(),
405            generator: args.join(" "),
406        }
407    }
408}
409
410/// A program that stores information about all structs and tools used in a project. Several programs can
411/// also be merged into a single one.
412#[derive(Debug, Clone)]
413pub struct Program {
414    z_safe: f64,
415    z_tool_change: f64,
416    meta: ProgramMeta,
417    units: Units,
418    contexts: Rc<RefCell<HashMap<Tool, InnerContext>>>,
419    tool_ordering: Rc<RefCell<ToolOrdering>>,
420}
421
422impl Program {
423    /// Creates a new `Program` struct.
424    #[must_use]
425    pub fn new(units: Units, z_safe: f64, z_tool_change: f64) -> Self {
426        Self {
427            z_safe,
428            z_tool_change,
429            meta: ProgramMeta::default(),
430            units,
431            contexts: Rc::new(RefCell::new(HashMap::new())),
432            tool_ordering: Rc::new(RefCell::new(ToolOrdering::default())),
433        }
434    }
435
436    /// Creates a new empty `Program` with the same same settings as the supplied one.
437    #[must_use]
438    pub fn new_empty_from(program: &Self) -> Self {
439        Self {
440            z_safe: program.z_safe,
441            z_tool_change: program.z_tool_change,
442            meta: ProgramMeta::default(),
443            units: program.units,
444            contexts: Rc::new(RefCell::new(HashMap::new())),
445            tool_ordering: Rc::new(RefCell::new(ToolOrdering::default())),
446        }
447    }
448
449    /// Set the name of the program
450    pub fn set_name(&mut self, name: &str) {
451        self.meta.name = name.into();
452    }
453
454    /// Get the name of the program
455    #[must_use]
456    pub fn name(&self) -> &str {
457        self.meta.name.as_str()
458    }
459
460    /// Add to program description
461    pub fn add_description(&mut self, description: &str) {
462        self.meta.description.push(description.into());
463    }
464
465    /// Get program description
466    #[must_use]
467    pub fn description(&self) -> &[String] {
468        &self.meta.description
469    }
470
471    /// Returns the z safe value set for this context.
472    ///
473    /// The value indicates the z height where the machine tool can safely travel
474    /// in the x and y axis without colliding with the workpiece.
475    #[must_use]
476    pub fn z_safe(&self) -> f64 {
477        self.z_safe
478    }
479
480    /// Returns the z height position used for manual tool change.
481    #[must_use]
482    pub fn z_tool_change(&self) -> f64 {
483        self.z_tool_change
484    }
485
486    /// Returns the tools position in a program, this number will then be used in the G-code T commands
487    /// (T1 is the first tool, T2 is the second tool and so on).
488    #[must_use]
489    pub fn tool_ordering(&self, tool: &Tool) -> Option<u8> {
490        let tool_ordering = self.tool_ordering.borrow();
491        tool_ordering.ordering(tool)
492    }
493
494    /// Allows setting the positional order for a tool, this will also automatically increment the position
495    /// of any tools that comes after the newly repositioned tool, resolving any ordering conflicts.
496    pub fn set_tool_ordering(&self, tool: &Tool, ordering: u8) {
497        let mut tool_ordering = self.tool_ordering.borrow_mut();
498        tool_ordering.set_ordering(tool, ordering);
499    }
500
501    fn create_context_if_missing_for_tool(&mut self, tool: &Tool) {
502        let mut contexts = self.contexts.borrow_mut();
503        if let Vacant(entry) = contexts.entry(*tool) {
504            let context = InnerContext::new(self.units, tool, self.z_safe, self.z_tool_change);
505            entry.insert(context);
506
507            let mut tool_ordering = self.tool_ordering.borrow_mut();
508            tool_ordering.auto_ordering(tool);
509        }
510    }
511
512    /// This is the main way of adding cuts to a program.
513    /// It returns a new tool context that can be used to extend the program.
514    ///
515    /// An example for adding cuts to a program:
516    /// ```
517    /// use cnccoder::prelude::*;
518    ///
519    /// let mut program = Program::default();
520    /// let tool = Tool::default();
521    ///
522    /// // Extend the program with new cuts
523    /// let mut context = program.context(tool);
524    ///
525    /// // Append the planing cuts to the cylindrical tool context
526    /// context.append_cut(Cut::plane(
527    ///     // Start at the x 0 mm, y 0 mm, z 3 mm coordinates
528    ///     Vector3::new(0.0, 0.0, 3.0),
529    ///     // Plane a 100 x 100 mm area
530    ///     Vector2::new(100.0, 100.0),
531    ///     // Plane down to 0 mm height (from 3 mm)
532    ///     0.0,
533    ///     // Cut at the most 1 mm per pass
534    ///     1.0,
535    /// ));
536    /// ```
537    pub fn context(&mut self, tool: Tool) -> Context {
538        self.create_context_if_missing_for_tool(&tool);
539        Context {
540            tool,
541            program: Rc::new(RefCell::new(self)),
542        }
543    }
544
545    /// This is the main way of adding cuts to a program.
546    /// It opens a new context for a tool where the program can be extended.
547    ///
548    /// An example for adding cuts to a program:
549    /// ```
550    /// use anyhow::Result;
551    /// use cnccoder::prelude::*;
552    ///
553    /// fn main() -> Result<()> {
554    ///     let mut program = Program::default();
555    ///     let tool = Tool::default();
556    ///
557    ///     // Extend the program with new cuts
558    ///     program.extend(&tool, |context| {
559    ///         // Append the planing cuts to the cylindrical tool context
560    ///         context.append_cut(Cut::plane(
561    ///             // Start at the x 0 mm, y 0 mm, z 3 mm coordinates
562    ///             Vector3::new(0.0, 0.0, 3.0),
563    ///             // Plane a 100 x 100 mm area
564    ///             Vector2::new(100.0, 100.0),
565    ///             // Plane down to 0 mm height (from 3 mm)
566    ///             0.0,
567    ///             // Cut at the most 1 mm per pass
568    ///             1.0,
569    ///         ));
570    ///
571    ///         Ok(())
572    ///     })?;
573    ///
574    ///     Ok(())
575    /// }
576    /// ```
577    #[deprecated(
578        since = "0.1.0",
579        note = "Replaced with the .context method that does not require operations to be added via closures."
580    )]
581    pub fn extend<Action>(&mut self, tool: &Tool, action: Action) -> Result<()>
582    where
583        Action: Fn(&mut InnerContext) -> Result<()>,
584    {
585        self.create_context_if_missing_for_tool(tool);
586        let mut contexts = self.contexts.borrow_mut();
587        let context = contexts.get_mut(tool).unwrap();
588        action(context)
589    }
590
591    /// Merges another program into this program.
592    ///
593    /// Returns error if tool or units are not the same in both programs.
594    pub fn merge(&mut self, program: &Program) -> Result<()> {
595        if self.units != program.units {
596            return Err(anyhow!("Failed to merge due to mismatching units"));
597        }
598
599        self.z_safe = self.z_safe.min(program.z_safe);
600        self.z_tool_change = self.z_tool_change.min(program.z_tool_change);
601
602        for tool in program.tools() {
603            self.create_context_if_missing_for_tool(&tool);
604        }
605
606        let program_contexts = program.contexts.borrow();
607        let mut contexts = self.contexts.borrow_mut();
608
609        for tool in program.tools() {
610            let program_context = program_contexts.get(&tool).unwrap();
611            let context = &mut contexts.get_mut(&tool).unwrap();
612            context.merge(program_context.clone())?;
613        }
614
615        Ok(())
616    }
617
618    /// Returns an ordered vec with all tools used by a program.
619    #[must_use]
620    pub fn tools(&self) -> Vec<Tool> {
621        let tool_ordering = self.tool_ordering.borrow();
622        tool_ordering.tools_ordered()
623    }
624
625    /// Returns the bounds of the program.
626    #[must_use]
627    pub fn bounds(&self) -> Bounds {
628        let mut bounds = Bounds::minmax();
629        let contexts = self.contexts.borrow();
630        let tools = self.tools();
631
632        for tool in tools {
633            if let Some(context) = contexts.get(&tool) {
634                let context_bounds = context.bounds();
635                bounds.min.x = if bounds.min.x > context_bounds.min.x {
636                    context_bounds.min.x
637                } else {
638                    bounds.min.x
639                };
640                bounds.min.y = if bounds.min.y > context_bounds.min.y {
641                    context_bounds.min.y
642                } else {
643                    bounds.min.y
644                };
645                bounds.min.z = if bounds.min.z > context_bounds.min.z {
646                    context_bounds.min.z
647                } else {
648                    bounds.min.z
649                };
650                bounds.max.x = if bounds.max.x < context_bounds.max.x {
651                    context_bounds.max.x
652                } else {
653                    bounds.max.x
654                };
655                bounds.max.y = if bounds.max.y < context_bounds.max.y {
656                    context_bounds.max.y
657                } else {
658                    bounds.max.y
659                };
660                bounds.max.z = if bounds.max.z < context_bounds.max.z {
661                    context_bounds.max.z
662                } else {
663                    bounds.max.z
664                };
665            }
666        }
667
668        bounds
669    }
670
671    /// Converts a program to G-code instructions
672    pub fn to_instructions(&self) -> Result<Vec<Instruction>> {
673        let contexts = self.contexts.borrow();
674        let tools = self.tools();
675        let z_safe = self.z_safe();
676        let z_tool_change = self.z_tool_change();
677        let bounds = self.bounds();
678        let size = bounds.size();
679        let units = self.units;
680
681        if z_tool_change < z_safe {
682            return Err(anyhow!(
683                "z_tool_change {} {} must be larger than or equal to the z_safe value of {} {}",
684                z_tool_change,
685                units,
686                z_safe,
687                units
688            ));
689        }
690
691        if z_safe < bounds.max.z {
692            return Err(anyhow!(
693                "z_safe {} {} must be larger than or equal to the workpiece max z value of {} {}",
694                z_safe,
695                units,
696                bounds.max.z,
697                units
698            ));
699        }
700
701        let mut raw_instructions = self.meta.to_instructions();
702
703        raw_instructions.push(Instruction::Comment(Comment {
704            text: format!(
705                "Workarea: size_x = {} {units}, size_y = {} {units}, size_z = {} {units}, min_x = {} {units}, min_y = {} {units}, max_z = {} {units}, z_safe = {} {units}, z_tool_change = {} {units}",
706               format_number(size.x),
707               format_number(size.y),
708               format_number(size.z),
709               format_number(bounds.min.x),
710               format_number(bounds.min.y),
711               format_number(bounds.max.z),
712               format_number(z_safe),
713               format_number(z_tool_change),
714            )
715        }));
716
717        raw_instructions.push(Instruction::Empty(Empty {}));
718        raw_instructions.push(Instruction::G17(G17 {}));
719
720        for tool in tools {
721            if let Some(context) = contexts.get(&tool) {
722                let tool_number = self.tool_ordering(&tool).unwrap();
723
724                raw_instructions.push(Instruction::Empty(Empty {}));
725
726                // Tool change
727                raw_instructions.append(&mut vec![
728                    Instruction::Comment(Comment {
729                        text: format!("Tool change: {}", tool),
730                    }),
731                    match context.units {
732                        Units::Metric => Instruction::G21(G21 {}),
733                        Units::Imperial => Instruction::G20(G20 {}),
734                    },
735                    Instruction::G0(G0 {
736                        x: None,
737                        y: None,
738                        z: Some(context.z_tool_change),
739                    }),
740                    Instruction::M5(M5 {}),
741                    Instruction::M6(M6 { t: tool_number }),
742                    Instruction::S(S {
743                        x: tool.spindle_speed(),
744                    }),
745                    if tool.direction() == Direction::Clockwise {
746                        Instruction::M3(M3 {})
747                    } else {
748                        Instruction::M4(M4 {})
749                    },
750                    Instruction::G4(G4 {
751                        p: Duration::from_secs(
752                            scale(tool.spindle_speed(), 0.0, 50_000.0, 3.0, 20.0) as u64,
753                        ),
754                    }),
755                ]);
756
757                // Add tool instructions
758                raw_instructions.append(&mut context.to_instructions()?);
759            }
760        }
761
762        // End program
763        raw_instructions.push(Instruction::G0(G0 {
764            x: None,
765            y: None,
766            z: Some(self.z_tool_change),
767        }));
768        raw_instructions.push(Instruction::Empty(Empty {}));
769        raw_instructions.push(Instruction::M2(M2 {}));
770
771        // Trim duplicated instructions
772        let mut workplane = Instruction::Empty(Empty {});
773        let raw_length = raw_instructions.len();
774        let mut instructions = vec![];
775        for (index, instruction) in raw_instructions.iter().enumerate() {
776            if *instruction == Instruction::G17(G17 {})
777                || *instruction == Instruction::G18(G18 {})
778                || *instruction == Instruction::G19(G19 {})
779            {
780                if *instruction == workplane {
781                    continue;
782                } else {
783                    workplane = instruction.clone();
784                }
785            }
786
787            if index < raw_length - 1 && instruction == &raw_instructions[index + 1] {
788                continue;
789            }
790
791            instructions.push(instruction.clone());
792        }
793
794        Ok(instructions)
795    }
796
797    /// Converts program to G-code
798    pub fn to_gcode(&self) -> Result<String> {
799        Ok(self
800            .to_instructions()?
801            .iter()
802            .map(|instruction| instruction.to_gcode())
803            .collect::<Vec<String>>()
804            .join("\n"))
805    }
806}
807
808impl Default for Program {
809    fn default() -> Self {
810        Self {
811            z_safe: 50.0,
812            z_tool_change: 100.0,
813            meta: ProgramMeta::default(),
814            units: Units::default(),
815            contexts: Rc::new(RefCell::new(HashMap::new())),
816            tool_ordering: Rc::new(RefCell::new(ToolOrdering::default())),
817        }
818    }
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824
825    fn mask_non_pure_comments(gcode: &str) -> String {
826        let pattern =
827            regex::Regex::new(r"(Created\s+on|Created\s+by|Generator):\s*[^\)]+").unwrap();
828        let gcode = pattern.replace_all(gcode, "$1: MASKED");
829
830        gcode.to_string()
831    }
832
833    #[test]
834    fn test_program_new() {
835        let program = Program::new(Units::Metric, 10.0, 50.0);
836        assert_eq!(program.z_safe, 10.0);
837        assert_eq!(program.z_tool_change, 50.0);
838    }
839
840    #[test]
841    fn test_program_empty() -> Result<()> {
842        let mut program = Program::new(Units::Metric, 10.0, 50.0);
843        program.set_name("empty");
844
845        let tool = Tool::cylindrical(
846            Units::Metric,
847            50.0,
848            4.0,
849            Direction::Clockwise,
850            5_000.0,
851            400.0,
852        );
853
854        let mut context = program.context(tool);
855        context.append_cut(Cut::drill(Vector3::default(), -1.0));
856
857        assert_eq!(program.tools().len(), 1);
858
859        let mut instructions = program.to_instructions()?;
860
861        for i in instructions.iter_mut() {
862            if let Instruction::Comment(comment) = i {
863                comment.text = mask_non_pure_comments(&comment.text);
864            }
865        }
866
867        assert_eq!(instructions, vec![
868            Instruction::Comment(Comment { text: "Name: empty".into() }),
869            Instruction::Comment(Comment { text: "Created on: MASKED".into()  }),
870            Instruction::Comment(Comment { text: "Created by: MASKED".into()  }),
871            Instruction::Comment(Comment { text: "Generator: MASKED" .into() }),
872            Instruction::Comment(Comment { text: "Workarea: size_x = 0 mm, size_y = 0 mm, size_z = 1 mm, min_x = 0 mm, min_y = 0 mm, max_z = 0 mm, z_safe = 10 mm, z_tool_change = 50 mm".into() }),
873            Instruction::Empty(Empty {}),
874            Instruction::G17(G17 {}),
875            Instruction::Empty(Empty {}),
876            Instruction::Comment(Comment { text: "Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min".to_string() }),
877            Instruction::G21(G21 {}),
878            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
879            Instruction::M5(M5 {}),
880            Instruction::M6(M6 { t: 1 }),
881            Instruction::S(S { x: 5_000.0 }),
882            Instruction::M3(M3 {}),
883            Instruction::G4(G4 { p: Duration::from_secs(4) }),
884            Instruction::Empty(Empty {}),
885            Instruction::Comment(Comment { text: "Drill hole at: x = 0, y = 0".to_string() }),
886            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
887            Instruction::G0(G0 { x: Some(0.0), y: Some(0.0), z: None }),
888            Instruction::G1(G1 { x: None, y: None, z: Some(-1.0), f: Some(400.0) }),
889            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
890            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
891            Instruction::Empty(Empty {}),
892            Instruction::M2(M2 {}),
893        ]);
894
895        let mut other_program = Program::new_empty_from(&program);
896        other_program.set_name("empty2");
897
898        assert_eq!(other_program.z_safe, 10.0);
899        assert_eq!(other_program.z_tool_change, 50.0);
900        assert_eq!(other_program.tools().len(), 0);
901
902        let mut instructions = other_program.to_instructions()?;
903
904        for i in instructions.iter_mut() {
905            if let Instruction::Comment(comment) = i {
906                comment.text = mask_non_pure_comments(&comment.text);
907            }
908        }
909
910        assert_eq!(instructions, vec![
911            Instruction::Comment(Comment { text: "Name: empty2".into() }),
912            Instruction::Comment(Comment { text: "Created on: MASKED".into()  }),
913            Instruction::Comment(Comment { text: "Created by: MASKED".into()  }),
914            Instruction::Comment(Comment { text: "Generator: MASKED" .into() }),
915            Instruction::Comment(Comment { text: "Workarea: size_x = 0 mm, size_y = 0 mm, size_z = 0 mm, min_x = 0 mm, min_y = 0 mm, max_z = 0 mm, z_safe = 10 mm, z_tool_change = 50 mm".into() }),
916            Instruction::Empty(Empty {}),
917                Instruction::G17(G17 {}),
918                Instruction::G0(G0 {
919                    x: None,
920                    y: None,
921                    z: Some(50.0)
922                }),
923                Instruction::Empty(Empty {}),
924                Instruction::M2(M2 {}),
925            ]
926        );
927
928        Ok(())
929    }
930
931    #[test]
932    #[allow(deprecated)]
933    fn test_program_extend() -> Result<()> {
934        let mut program = Program::new(Units::Metric, 10.0, 50.0);
935
936        let tool1 = Tool::cylindrical(
937            Units::Metric,
938            50.0,
939            4.0,
940            Direction::Clockwise,
941            5_000.0,
942            400.0,
943        );
944
945        let tool2 = Tool::conical(
946            Units::Metric,
947            45.0,
948            15.0,
949            Direction::Clockwise,
950            5_000.0,
951            400.0,
952        );
953
954        program.extend(&tool1, |context| {
955            context.append_cut(Cut::path(
956                Vector3::new(0.0, 0.0, 3.0),
957                vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
958                -0.1,
959                1.0,
960            ));
961
962            Ok(())
963        })?;
964
965        program.extend(&tool2, |context| {
966            context.append_cut(Cut::path(
967                Vector3::new(5.0, 10.0, 3.0),
968                vec![Segment::line(
969                    Vector2::new(5.0, 10.0),
970                    Vector2::new(15.0, 10.0),
971                )],
972                -0.1,
973                1.0,
974            ));
975
976            Ok(())
977        })?;
978
979        let tools = program.tools();
980        assert_eq!(tools, vec![tool1, tool2]);
981
982        program.set_tool_ordering(&tool2, 0);
983
984        let tools = program.tools();
985        assert_eq!(tools, vec![tool2, tool1]);
986
987        Ok(())
988    }
989
990    #[test]
991    fn test_program_tools() -> Result<()> {
992        let mut program = Program::new(Units::Metric, 10.0, 50.0);
993
994        let tool1 = Tool::cylindrical(
995            Units::Metric,
996            50.0,
997            4.0,
998            Direction::Clockwise,
999            5_000.0,
1000            400.0,
1001        );
1002
1003        let tool2 = Tool::conical(
1004            Units::Metric,
1005            45.0,
1006            15.0,
1007            Direction::Clockwise,
1008            5_000.0,
1009            400.0,
1010        );
1011
1012        let mut tool1_context = program.context(tool1);
1013        tool1_context.append_cut(Cut::path(
1014            Vector3::new(0.0, 0.0, 3.0),
1015            vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
1016            -0.1,
1017            1.0,
1018        ));
1019
1020        let mut tool2_context = program.context(tool2);
1021        tool2_context.append_cut(Cut::path(
1022            Vector3::new(5.0, 10.0, 3.0),
1023            vec![Segment::line(
1024                Vector2::new(5.0, 10.0),
1025                Vector2::new(15.0, 10.0),
1026            )],
1027            -0.1,
1028            1.0,
1029        ));
1030
1031        let tools = program.tools();
1032        assert_eq!(tools, vec![tool1, tool2]);
1033
1034        program.set_tool_ordering(&tool2, 0);
1035
1036        let tools = program.tools();
1037        assert_eq!(tools, vec![tool2, tool1]);
1038
1039        Ok(())
1040    }
1041
1042    #[test]
1043    fn test_program_to_instructions() -> Result<()> {
1044        let mut program = Program::new(Units::Metric, 10.0, 50.0);
1045        program.set_name("program to instructions");
1046
1047        let tool1 = Tool::cylindrical(
1048            Units::Metric,
1049            50.0,
1050            4.0,
1051            Direction::Clockwise,
1052            5_000.0,
1053            400.0,
1054        );
1055
1056        let tool2 = Tool::conical(
1057            Units::Imperial,
1058            45.0,
1059            1.0,
1060            Direction::Clockwise,
1061            5_000.0,
1062            400.0,
1063        );
1064
1065        let mut tool1_context = program.context(tool1);
1066        tool1_context.append_cut(Cut::path(
1067            Vector3::new(0.0, 0.0, 3.0),
1068            vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
1069            -0.1,
1070            1.0,
1071        ));
1072
1073        let mut tool2_context = program.context(tool2);
1074        tool2_context.append_cut(Cut::path(
1075            Vector3::new(5.0, 10.0, 3.0),
1076            vec![Segment::line(
1077                Vector2::new(5.0, 10.0),
1078                Vector2::new(15.0, 10.0),
1079            )],
1080            -0.1,
1081            1.0,
1082        ));
1083
1084        let mut instructions = program.to_instructions()?;
1085
1086        let expected_output = vec![
1087            Instruction::Comment(Comment { text: "Name: program to instructions".into() }),
1088            Instruction::Comment(Comment { text: "Created on: MASKED".into()  }),
1089            Instruction::Comment(Comment { text: "Created by: MASKED".into()  }),
1090            Instruction::Comment(Comment { text: "Generator: MASKED" .into() }),
1091            Instruction::Comment(Comment { text: "Workarea: size_x = 20 mm, size_y = 20 mm, size_z = 3.1 mm, min_x = 0 mm, min_y = 0 mm, max_z = 3 mm, z_safe = 10 mm, z_tool_change = 50 mm".into() }),
1092            Instruction::Empty(Empty {}),
1093            Instruction::G17(G17 {}),
1094            Instruction::Empty(Empty {}),
1095            Instruction::Comment(Comment { text: "Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min".to_string() }),
1096            Instruction::G21(G21 {}),
1097            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1098            Instruction::M5(M5 {}),
1099            Instruction::M6(M6 { t: 1 }),
1100            Instruction::S(S { x: 5_000.0 }),
1101            Instruction::M3(M3 {}),
1102            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1103            Instruction::Empty(Empty {}),
1104            Instruction::Comment(Comment { text: "Cut path at: x = 0, y = 0".to_string() }),
1105            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1106            Instruction::G0(G0 { x: Some(0.0), y: Some(0.0), z: None }),
1107            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1108            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(3.0), f: None }),
1109            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(2.0), f: None }),
1110            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(2.0), f: None }),
1111            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(1.0), f: None }),
1112            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(1.0), f: None }),
1113            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(0.0), f: None }),
1114            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(-0.1), f: None }),
1115            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(-0.1), f: None }),
1116            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1117            Instruction::Empty(Empty {}),
1118            Instruction::Comment(Comment { text: "Tool change: type = Conical, angle = 45°, diameter = 1\", length = 1.207\", direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400\"/min".to_string() }),
1119            Instruction::G21(G21 {}),
1120            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1121            Instruction::M5(M5 {}),
1122            Instruction::M6(M6 { t: 2 }),
1123            Instruction::S(S { x: 5_000.0 }),
1124            Instruction::M3(M3 {}),
1125            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1126            Instruction::Empty(Empty {}),
1127            Instruction::Comment(Comment { text: "Cut path at: x = 5, y = 10".to_string() }),
1128            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1129            Instruction::G0(G0 { x: Some(10.0), y: Some(20.0), z: None }),
1130            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1131            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(3.0), f: None }),
1132            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(2.0), f: None }),
1133            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(2.0), f: None }),
1134            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(1.0), f: None }),
1135            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(1.0), f: None }),
1136            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(0.0), f: None }),
1137            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(-0.1), f: None }),
1138            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(-0.1), f: None }),
1139            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1140            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1141            Instruction::Empty(Empty {}),
1142            Instruction::M2(M2 {}),
1143        ];
1144
1145        for i in instructions.iter_mut() {
1146            if let Instruction::Comment(comment) = i {
1147                comment.text = mask_non_pure_comments(&comment.text);
1148            }
1149        }
1150
1151        assert_eq!(instructions, expected_output);
1152
1153        program.set_tool_ordering(&tool2, 1);
1154
1155        let mut instructions = program.to_instructions()?;
1156
1157        let expected_output = vec![
1158            Instruction::Comment(Comment { text: "Name: program to instructions".into() }),
1159            Instruction::Comment(Comment { text: "Created on: MASKED".into()  }),
1160            Instruction::Comment(Comment { text: "Created by: MASKED".into()  }),
1161            Instruction::Comment(Comment { text: "Generator: MASKED" .into() }),
1162            Instruction::Comment(Comment { text: "Workarea: size_x = 20 mm, size_y = 20 mm, size_z = 3.1 mm, min_x = 0 mm, min_y = 0 mm, max_z = 3 mm, z_safe = 10 mm, z_tool_change = 50 mm".into() }),
1163            Instruction::Empty(Empty {}),
1164            Instruction::G17(G17 {}),
1165            Instruction::Empty(Empty {}),
1166            Instruction::Comment(Comment { text: "Tool change: type = Conical, angle = 45°, diameter = 1\", length = 1.207\", direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400\"/min".to_string() }),
1167            Instruction::G21(G21 {}),
1168            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1169            Instruction::M5(M5 {}),
1170            Instruction::M6(M6 { t: 1 }),
1171            Instruction::S(S { x: 5_000.0 }),
1172            Instruction::M3(M3 {}),
1173            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1174            Instruction::Empty(Empty {}),
1175            Instruction::Comment(Comment { text: "Cut path at: x = 5, y = 10".to_string() }),
1176            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1177            Instruction::G0(G0 { x: Some(10.0), y: Some(20.0), z: None }),
1178            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1179            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(3.0), f: None }),
1180            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(2.0), f: None }),
1181            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(2.0), f: None }),
1182            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(1.0), f: None }),
1183            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(1.0), f: None }),
1184            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(0.0), f: None }),
1185            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(-0.1), f: None }),
1186            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(-0.1), f: None }),
1187            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1188            Instruction::Empty(Empty {}),
1189            Instruction::Comment(Comment { text: "Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min".to_string() }),
1190            Instruction::G21(G21 {}),
1191            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1192            Instruction::M5(M5 {}),
1193            Instruction::M6(M6 { t: 2 }),
1194            Instruction::S(S { x: 5_000.0 }),
1195            Instruction::M3(M3 {}),
1196            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1197            Instruction::Empty(Empty {}),
1198            Instruction::Comment(Comment { text: "Cut path at: x = 0, y = 0".to_string() }),
1199            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1200            Instruction::G0(G0 { x: Some(0.0), y: Some(0.0), z: None }),
1201            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1202            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(3.0), f: None }),
1203            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(2.0), f: None }),
1204            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(2.0), f: None }),
1205            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(1.0), f: None }),
1206            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(1.0), f: None }),
1207            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(0.0), f: None }),
1208            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(-0.1), f: None }),
1209            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(-0.1), f: None }),
1210            Instruction::G0(G0 { x: None, y: None, z: Some(10.0) }),
1211            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1212            Instruction::Empty(Empty {}),
1213            Instruction::M2(M2 {}),
1214        ];
1215
1216        for i in instructions.iter_mut() {
1217            if let Instruction::Comment(comment) = i {
1218                comment.text = mask_non_pure_comments(&comment.text);
1219            }
1220        }
1221
1222        assert_eq!(instructions, expected_output);
1223
1224        Ok(())
1225    }
1226
1227    #[test]
1228    fn test_merge_programs() -> Result<()> {
1229        let tool1 = Tool::cylindrical(
1230            Units::Metric,
1231            50.0,
1232            4.0,
1233            Direction::Clockwise,
1234            5_000.0,
1235            400.0,
1236        );
1237
1238        let tool2 = Tool::conical(
1239            Units::Imperial,
1240            45.0,
1241            1.0,
1242            Direction::Clockwise,
1243            5_000.0,
1244            400.0,
1245        );
1246
1247        let mut program1 = Program::new(Units::Metric, 10.0, 40.0);
1248        program1.set_name("program1");
1249
1250        let mut program1_tool1_context = program1.context(tool1);
1251        program1_tool1_context.append_cut(Cut::path(
1252            Vector3::new(0.0, 0.0, 3.0),
1253            vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
1254            -0.1,
1255            1.0,
1256        ));
1257
1258        let mut program2 = Program::new(Units::Metric, 5.0, 50.0);
1259
1260        let mut program2_tool1_context = program2.context(tool1);
1261        program2_tool1_context.append_cut(Cut::path(
1262            Vector3::new(10.0, 10.0, 3.0),
1263            vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
1264            -0.1,
1265            1.0,
1266        ));
1267
1268        let mut program2_tool2_context = program2.context(tool2);
1269        program2_tool2_context.append_cut(Cut::path(
1270            Vector3::new(5.0, 10.0, 3.0),
1271            vec![Segment::line(
1272                Vector2::new(5.0, 10.0),
1273                Vector2::new(15.0, 10.0),
1274            )],
1275            -0.1,
1276            1.0,
1277        ));
1278
1279        program1.merge(&program2)?;
1280
1281        let mut instructions = program1.to_instructions()?;
1282
1283        let expected_output = vec![
1284            Instruction::Comment(Comment { text: "Name: program1".into() }),
1285            Instruction::Comment(Comment { text: "Created on: MASKED".into()  }),
1286            Instruction::Comment(Comment { text: "Created by: MASKED".into()  }),
1287            Instruction::Comment(Comment { text: "Generator: MASKED" .into() }),
1288            Instruction::Comment(Comment { text: "Workarea: size_x = 20 mm, size_y = 20 mm, size_z = 3.1 mm, min_x = 0 mm, min_y = 0 mm, max_z = 3 mm, z_safe = 5 mm, z_tool_change = 40 mm".into() }),
1289            Instruction::Empty(Empty {}),
1290            Instruction::G17(G17 {}),
1291            Instruction::Empty(Empty {}),
1292            Instruction::Comment(Comment { text: "Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min".to_string() }),
1293            Instruction::G21(G21 {}),
1294            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1295            Instruction::M5(M5 {}),
1296            Instruction::M6(M6 { t: 1 }),
1297            Instruction::S(S { x: 5_000.0 }),
1298            Instruction::M3(M3 {}),
1299            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1300            Instruction::Empty(Empty {}),
1301            Instruction::Comment(Comment { text: "Cut path at: x = 0, y = 0".to_string() }),
1302            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1303            Instruction::G0(G0 { x: Some(0.0), y: Some(0.0), z: None }),
1304            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1305            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(3.0), f: None }),
1306            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(2.0), f: None }),
1307            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(2.0), f: None }),
1308            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(1.0), f: None }),
1309            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(1.0), f: None }),
1310            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(0.0), f: None }),
1311            Instruction::G1(G1 { x: Some(0.0), y: Some(0.0), z: Some(-0.1), f: None }),
1312            Instruction::G1(G1 { x: Some(5.0), y: Some(10.0), z: Some(-0.1), f: None }),
1313            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1314            Instruction::Empty(Empty {}),
1315            Instruction::Comment(Comment { text: "Cut path at: x = 10, y = 10".to_string() }),
1316            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1317            Instruction::G0(G0 { x: Some(10.0), y: Some(10.0), z: None }),
1318            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1319            Instruction::G1(G1 { x: Some(10.0), y: Some(10.0), z: Some(3.0), f: None }),
1320            Instruction::G1(G1 { x: Some(15.0), y: Some(20.0), z: Some(2.0), f: None }),
1321            Instruction::G1(G1 { x: Some(10.0), y: Some(10.0), z: Some(2.0), f: None }),
1322            Instruction::G1(G1 { x: Some(15.0), y: Some(20.0), z: Some(1.0), f: None }),
1323            Instruction::G1(G1 { x: Some(10.0), y: Some(10.0), z: Some(1.0), f: None }),
1324            Instruction::G1(G1 { x: Some(15.0), y: Some(20.0), z: Some(0.0), f: None }),
1325            Instruction::G1(G1 { x: Some(10.0), y: Some(10.0), z: Some(-0.1), f: None }),
1326            Instruction::G1(G1 { x: Some(15.0), y: Some(20.0), z: Some(-0.1), f: None }),
1327            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1328            Instruction::Empty(Empty {}),
1329            Instruction::Comment(Comment { text: "Tool change: type = Conical, angle = 45°, diameter = 1\", length = 1.207\", direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400\"/min".to_string() }),
1330            Instruction::G21(G21 {}),
1331            Instruction::G0(G0 { x: None, y: None, z: Some(50.0) }),
1332            Instruction::M5(M5 {}),
1333            Instruction::M6(M6 { t: 2 }),
1334            Instruction::S(S { x: 5_000.0 }),
1335            Instruction::M3(M3 {}),
1336            Instruction::G4(G4 { p: Duration::from_secs(4) }),
1337            Instruction::Empty(Empty {}),
1338            Instruction::Comment(Comment { text: "Cut path at: x = 5, y = 10".to_string() }),
1339            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1340            Instruction::G0(G0 { x: Some(10.0), y: Some(20.0), z: None }),
1341            Instruction::G1(G1 { x: None, y: None, z: Some(3.0), f: Some(400.0) }),
1342            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(3.0), f: None }),
1343            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(2.0), f: None }),
1344            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(2.0), f: None }),
1345            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(1.0), f: None }),
1346            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(1.0), f: None }),
1347            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(0.0), f: None }),
1348            Instruction::G1(G1 { x: Some(10.0), y: Some(20.0), z: Some(-0.1), f: None }),
1349            Instruction::G1(G1 { x: Some(20.0), y: Some(20.0), z: Some(-0.1), f: None }),
1350            Instruction::G0(G0 { x: None, y: None, z: Some(5.0) }),
1351            Instruction::G0(G0 { x: None, y: None, z: Some(40.0) }),
1352            Instruction::Empty(Empty {}),
1353            Instruction::M2(M2 {}),
1354        ];
1355
1356        for i in instructions.iter_mut() {
1357            if let Instruction::Comment(comment) = i {
1358                comment.text = mask_non_pure_comments(&comment.text);
1359            }
1360        }
1361
1362        assert_eq!(instructions, expected_output);
1363
1364        Ok(())
1365    }
1366
1367    #[test]
1368    fn test_program_to_gcode() -> Result<()> {
1369        let mut program = Program::new(Units::Imperial, 10.0, 50.0);
1370        program.set_name("a test program");
1371
1372        let tool1 = Tool::cylindrical(
1373            Units::Metric,
1374            50.0,
1375            4.0,
1376            Direction::Clockwise,
1377            5_000.0,
1378            400.0,
1379        );
1380
1381        let tool2 = Tool::conical(
1382            Units::Imperial,
1383            45.0,
1384            1.0,
1385            Direction::Clockwise,
1386            5_000.0,
1387            400.0,
1388        );
1389
1390        let mut tool1_context = program.context(tool1);
1391        tool1_context.append_cut(Cut::path(
1392            Vector3::new(0.0, 0.0, 3.0),
1393            vec![Segment::line(Vector2::default(), Vector2::new(5.0, 10.0))],
1394            -0.1,
1395            1.0,
1396        ));
1397
1398        let mut tool2_context = program.context(tool2);
1399        tool2_context.append_cut(Cut::path(
1400            Vector3::new(5.0, 10.0, 3.0),
1401            vec![Segment::line(
1402                Vector2::new(5.0, 10.0),
1403                Vector2::new(15.0, 10.0),
1404            )],
1405            -0.1,
1406            1.0,
1407        ));
1408
1409        program.set_tool_ordering(&tool2, 0);
1410
1411        let gcode = mask_non_pure_comments(&program.to_gcode()?);
1412
1413        let expected_output = vec![
1414            ";(Name: a test program)",
1415            ";(Created on: MASKED)",
1416            ";(Created by: MASKED)",
1417            ";(Generator: MASKED)",
1418            ";(Workarea: size_x = 20 \", size_y = 20 \", size_z = 3.1 \", min_x = 0 \", min_y = 0 \", max_z = 3 \", z_safe = 10 \", z_tool_change = 50 \")",
1419            "",
1420            "G17",
1421            "",
1422            ";(Tool change: type = Conical, angle = 45°, diameter = 1\", length = 1.207\", direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400\"/min)",
1423            "G20",
1424            "G0 Z50",
1425            "M5",
1426            "T1 M6",
1427            "S5000",
1428            "M3",
1429            "G4 P4",
1430            "",
1431            ";(Cut path at: x = 5, y = 10)",
1432            "G0 Z10",
1433            "G0 X10 Y20",
1434            "G1 Z3 F400",
1435            "G1 X10 Y20 Z3",
1436            "G1 X20 Y20 Z2",
1437            "G1 X10 Y20 Z2",
1438            "G1 X20 Y20 Z1",
1439            "G1 X10 Y20 Z1",
1440            "G1 X20 Y20 Z0",
1441            "G1 X10 Y20 Z-0.1",
1442            "G1 X20 Y20 Z-0.1",
1443            "G0 Z10",
1444            "",
1445            ";(Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min)",
1446            "G20",
1447            "G0 Z50",
1448            "M5",
1449            "T2 M6",
1450            "S5000",
1451            "M3",
1452            "G4 P4",
1453            "",
1454            ";(Cut path at: x = 0, y = 0)",
1455            "G0 Z10",
1456            "G0 X0 Y0",
1457            "G1 Z3 F400",
1458            "G1 X0 Y0 Z3",
1459            "G1 X5 Y10 Z2",
1460            "G1 X0 Y0 Z2",
1461            "G1 X5 Y10 Z1",
1462            "G1 X0 Y0 Z1",
1463            "G1 X5 Y10 Z0",
1464            "G1 X0 Y0 Z-0.1",
1465            "G1 X5 Y10 Z-0.1",
1466            "G0 Z10",
1467            "G0 Z50",
1468            "",
1469            "M2",
1470        ].join("\n");
1471
1472        assert_eq!(gcode, expected_output);
1473
1474        Ok(())
1475    }
1476
1477    #[test]
1478    fn test_program_bounds() -> Result<()> {
1479        let mut program = Program::new(Units::Metric, 10.0, 50.0);
1480        program.set_name("program bounds");
1481
1482        let tool = Tool::cylindrical(
1483            Units::Metric,
1484            50.0,
1485            4.0,
1486            Direction::Clockwise,
1487            5_000.0,
1488            400.0,
1489        );
1490
1491        let mut context = program.context(tool);
1492
1493        context.append_cut(Cut::path(
1494            Vector3::new(0.0, 0.0, 3.0),
1495            vec![Segment::line(
1496                Vector2::default(),
1497                Vector2::new(-28.0, -30.0),
1498            )],
1499            -0.1,
1500            1.0,
1501        ));
1502
1503        context.append_cut(Cut::path(
1504            Vector3::new(0.0, 0.0, 3.0),
1505            vec![
1506                Segment::line(Vector2::new(23.0, 12.0), Vector2::new(5.0, 10.0)),
1507                Segment::line(Vector2::new(5.0, 10.0), Vector2::new(67.0, 102.0)),
1508                Segment::line(Vector2::new(67.0, 102.0), Vector2::new(23.0, 12.0)),
1509            ],
1510            -0.1,
1511            1.0,
1512        ));
1513
1514        let bounds = program.bounds();
1515
1516        assert_eq!(
1517            bounds,
1518            Bounds {
1519                min: Vector3::new(-28.0, -30.0, -0.1),
1520                max: Vector3::new(67.0, 102.0, 3.0),
1521            }
1522        );
1523
1524        Ok(())
1525    }
1526}