cnccoder/
filesystem.rs

1//! Provides helpers for writing G-code and project files to disk.
2
3use std::{fs::File, io::Write};
4
5use anyhow::Result;
6
7use crate::{camotics::*, program::*};
8
9/// Writes .gcode and .camotics files from a program to disk.
10///
11/// Example for planing a surface area to a specific height (z axis is vertical):
12/// ```
13/// use anyhow::Result;
14/// use cnccoder::prelude::*;
15///
16/// fn main() -> Result<()> {
17///     let mut program = Program::new(
18///         Units::Metric,
19///         10.0,
20///         50.0,
21///     );
22///     program.set_name("planing");
23///
24///     let tool = Tool::cylindrical(
25///         Units::Metric,
26///         20.0,
27///         10.0,
28///         Direction::Clockwise,
29///         20000.0,
30///         5000.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///     write_project(&program, 0.5)?;
43///
44///     Ok(())
45/// }
46/// ```
47pub fn write_project(program: &Program, camotics_resolution: f64) -> Result<()> {
48    let name = program.name();
49    let camotics = Camotics::from_program(name, program, camotics_resolution);
50    let gcode = program.to_gcode()?;
51
52    let mut camotics_file = File::create(format!("{}.camotics", name))?;
53    camotics_file.write_all(camotics.to_json_string().as_bytes())?;
54    camotics_file.sync_all()?;
55
56    let mut gcode_file = File::create(format!("{}.gcode", name))?;
57    gcode_file.write_all(gcode.as_bytes())?;
58    gcode_file.sync_all()?;
59
60    Ok(())
61}
62
63#[cfg(test)]
64mod tests {
65    use std::fs::{read_to_string, remove_file};
66
67    use anyhow::Result;
68    use serde_json::Value;
69
70    use crate::{cuts::*, tools::*, types::*};
71
72    use super::*;
73
74    #[test]
75    fn test_camotics_from_program() -> Result<()> {
76        let mut program = Program::new(Units::Metric, 10.0, 50.0);
77        program.set_name("test-temp");
78
79        let tool = Tool::cylindrical(
80            Units::Metric,
81            50.0,
82            4.0,
83            Direction::Clockwise,
84            5000.0,
85            400.0,
86        );
87
88        let mut context = program.context(tool);
89
90        context.append_cut(Cut::path(
91            Vector3::new(0.0, 0.0, 3.0),
92            vec![Segment::line(
93                Vector2::default(),
94                Vector2::new(-28.0, -30.0),
95            )],
96            -0.1,
97            1.0,
98        ));
99
100        context.append_cut(Cut::path(
101            Vector3::new(0.0, 0.0, 3.0),
102            vec![
103                Segment::line(Vector2::new(23.0, 12.0), Vector2::new(5.0, 10.0)),
104                Segment::line(Vector2::new(5.0, 10.0), Vector2::new(67.0, 102.0)),
105                Segment::line(Vector2::new(67.0, 102.0), Vector2::new(23.0, 12.0)),
106            ],
107            -0.1,
108            1.0,
109        ));
110
111        write_project(&program, 0.5)?;
112
113        let camotics: Value = serde_json::from_str(&read_to_string("test-temp.camotics")?)?;
114        remove_file("test-temp.camotics")?;
115
116        let expected_camotics_output: Value = serde_json::from_str(
117            r#"{
118            "units": "metric",
119            "resolution-mode": "manual",
120            "resolution": 0.5,
121            "tools": {
122                "1": {
123                "units": "metric",
124                "length": 50.0,
125                "diameter": 4.0,
126                "number": 1,
127                "shape": "cylindrical"
128                }
129            },
130            "workpiece": {
131                "automatic": false,
132                "margin": 0.0,
133                "bounds": {
134                "min": [
135                    -28.0,
136                    -30.0,
137                    -0.1
138                ],
139                "max": [
140                    67.0,
141                    102.0,
142                    3.0
143                ]
144                }
145            },
146            "files": [
147                "test-temp.gcode"
148            ]
149        }"#,
150        )?;
151
152        assert_eq!(camotics, expected_camotics_output);
153
154        let gcode = read_to_string("test-temp.gcode")?;
155        remove_file("test-temp.gcode")?;
156
157        let pattern =
158            regex::Regex::new(r"\;\((Created\s+on|Created\s+by|Generator):\s*[^\)]+\)").unwrap();
159        let gcode = pattern.replace_all(&gcode, ";($1: MASKED)");
160
161        assert_eq!(gcode, r#"
162;(Name: test-temp)
163;(Created on: MASKED)
164;(Created by: MASKED)
165;(Generator: MASKED)
166;(Workarea: size_x = 95 mm, size_y = 132 mm, size_z = 3.1 mm, min_x = -28 mm, min_y = -30 mm, max_z = 3 mm, z_safe = 10 mm, z_tool_change = 50 mm)
167
168G17
169
170;(Tool change: type = Cylindrical, diameter = 4 mm, length = 50 mm, direction = clockwise, spindle_speed = 5000 rpm, feed_rate = 400 mm/min)
171G21
172G0 Z50
173M5
174T1 M6
175S5000
176M3
177G4 P4
178
179;(Cut path at: x = 0, y = 0)
180G0 Z10
181G0 X0 Y0
182G1 Z3 F400
183G1 X0 Y0 Z3
184G1 X-28 Y-30 Z2
185G1 X0 Y0 Z2
186G1 X-28 Y-30 Z1
187G1 X0 Y0 Z1
188G1 X-28 Y-30 Z0
189G1 X0 Y0 Z-0.1
190G1 X-28 Y-30 Z-0.1
191G0 Z10
192
193;(Cut path at: x = 0, y = 0)
194G0 Z10
195G0 X23 Y12
196G1 Z3 F400
197G1 X23 Y12 Z3
198G1 X5 Y10 Z2.95
199G1 X67 Y102 Z2.451
200G1 X23 Y12 Z2
201G1 X23 Y12 Z2
202G1 X5 Y10 Z1.95
203G1 X67 Y102 Z1.451
204G1 X23 Y12 Z1
205G1 X5 Y10 Z0.95
206G1 X67 Y102 Z0.451
207G1 X23 Y12 Z-0
208G1 X23 Y12 Z-0.1
209G1 X5 Y10 Z-0.1
210G1 X67 Y102 Z-0.1
211G1 X23 Y12 Z-0.1
212G0 Z10
213G0 Z50
214
215M2"#.to_string().trim());
216
217        Ok(())
218    }
219}