1use std::{fs::File, io::Write};
4
5use anyhow::Result;
6
7use crate::{camotics::*, program::*};
8
9pub 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}