1use 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#[derive(Debug, Clone)]
82pub enum Operation {
83 Cut(Cut),
85 Empty(Empty),
87 Comment(Comment),
89 Message(Message),
91}
92
93impl Operation {
94 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 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#[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 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 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 pub fn append(&mut self, operation: Operation) {
167 self.operations.push(operation);
168 }
169
170 pub fn append_cut(&mut self, cut: Cut) {
172 self.append(Operation::Cut(cut));
173 }
174
175 pub fn units(&self) -> Units {
177 self.units
178 }
179
180 pub fn tool(&self) -> Tool {
182 self.tool
183 }
184
185 pub fn z_safe(&self) -> f64 {
190 self.z_safe
191 }
192
193 pub fn z_tool_change(&self) -> f64 {
195 self.z_tool_change
196 }
197
198 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 pub fn operations(&self) -> Vec<Operation> {
241 self.operations.clone()
242 }
243
244 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#[derive(Debug, Clone)]
260pub struct Context<'a> {
261 tool: Tool,
262 program: Rc<RefCell<&'a Program>>,
263}
264
265impl<'a> Context<'a> {
266 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 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 pub fn append_cut(&mut self, cut: Cut) {
291 self.append(Operation::Cut(cut));
292 }
293
294 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 pub fn tool(&self) -> Tool {
304 self.tool
305 }
306
307 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 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 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 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 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#[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 #[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 #[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 pub fn set_name(&mut self, name: &str) {
451 self.meta.name = name.into();
452 }
453
454 #[must_use]
456 pub fn name(&self) -> &str {
457 self.meta.name.as_str()
458 }
459
460 pub fn add_description(&mut self, description: &str) {
462 self.meta.description.push(description.into());
463 }
464
465 #[must_use]
467 pub fn description(&self) -> &[String] {
468 &self.meta.description
469 }
470
471 #[must_use]
476 pub fn z_safe(&self) -> f64 {
477 self.z_safe
478 }
479
480 #[must_use]
482 pub fn z_tool_change(&self) -> f64 {
483 self.z_tool_change
484 }
485
486 #[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 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 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 #[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 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 #[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 #[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 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 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 raw_instructions.append(&mut context.to_instructions()?);
759 }
760 }
761
762 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 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 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}