cnccoder/
camotics.rs

1//! Helper module for generating Camotics project files.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::{program::*, tools::*, types::*};
8
9/// Resolution mode, when creating a Camotics struct `ResolutionMode::Manual`
10/// is used by default to allow setting a custom resolution for the simulation.
11#[derive(Serialize, Deserialize, Debug, PartialEq)]
12#[serde(rename_all = "lowercase")]
13pub enum ResolutionMode {
14    /// Corresponds to a resolution of 0.116348.
15    High,
16    /// Corresponds to a resolution of 0.428631.
17    Low,
18    /// Allows for custom resolution values to be set.
19    Manual,
20}
21
22/// Defines the size of the workpiece, when creating a Camotics struct these
23/// values are calculated from the program.
24#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
25pub struct Workpiece {
26    /// Indicates of bounds should be calculated by Camotics automatically.
27    pub automatic: bool,
28    /// Extra margin added to the Camotics atumated calculation.
29    pub margin: f64,
30    /// Manual bounds for the workpiece, will be automatically calculated
31    /// from the program.
32    pub bounds: Bounds,
33}
34
35/// Tool shape, will be derived from the [tools](../tools/index.html) used in the program.
36#[derive(Serialize, Deserialize, Debug, PartialEq)]
37#[serde(rename_all = "lowercase")]
38pub enum CamoticsToolShape {
39    /// Cylindrical tool
40    Cylindrical,
41    /// Ballnose tool
42    Ballnose,
43    /// Conical tool
44    Conical,
45}
46
47impl Default for CamoticsToolShape {
48    fn default() -> Self {
49        Self::Cylindrical
50    }
51}
52
53/// Tool definition in the format required by Camotics, will be derived from the
54/// [tools](../tools/index.html) used in the program.
55#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
56pub struct CamoticsTool {
57    /// Measurement units of the tool
58    pub units: Units,
59    /// Angle of a conical tool
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub angle: Option<f64>,
62    /// Cutter length of the tool
63    pub length: f64,
64    /// Cutter diameter of the tool
65    pub diameter: f64,
66    /// The tool number/identifier
67    pub number: u8,
68    /// The shape of the tool
69    pub shape: CamoticsToolShape,
70}
71
72impl CamoticsTool {
73    /// Creates a new `CamoticsTool` from a program [Tool](../tools/enum.Tool.html).
74    #[must_use]
75    pub fn from_tool(tool: Tool, number: u8) -> Self {
76        match tool {
77            Tool::Cylindrical(t) => CamoticsTool {
78                units: t.units,
79                angle: None,
80                length: t.length,
81                diameter: t.diameter,
82                number,
83                shape: CamoticsToolShape::Cylindrical,
84            },
85            Tool::Ballnose(t) => CamoticsTool {
86                units: t.units,
87                angle: None,
88                length: t.length,
89                diameter: t.diameter,
90                number,
91                shape: CamoticsToolShape::Ballnose,
92            },
93            Tool::Conical(t) => CamoticsTool {
94                units: t.units,
95                angle: Some(t.angle),
96                length: t.length,
97                diameter: t.diameter,
98                number,
99                shape: CamoticsToolShape::Conical,
100            },
101        }
102    }
103}
104
105/// Representation for a [Camotics](https://camotics.org/) project file,
106/// running `.to_json_string()` outputs a project file that can be opened
107/// directly by Camotics.
108///
109/// To write a camotics file and a gcode file in one go, see
110/// [write_project](../filesystem/fn.write_project.html).
111#[derive(Serialize, Deserialize, Debug, PartialEq)]
112pub struct Camotics {
113    /// The name of the project.
114    #[serde(skip_serializing)]
115    pub name: String,
116    /// The units used by the project.
117    pub units: Units,
118    /// The resolution mode used by the project, will be `ResolutionMode::Manual`
119    /// by default.
120    #[serde(rename(serialize = "resolution-mode"))]
121    pub resolution_mode: ResolutionMode,
122    /// The resolution used for the simulation, a higher value uses more system
123    /// memory and takes longer/more CPU to simulate. Suggested value is between
124    /// 0.5 and 1.0 depending on the detail value required. A lower value equals
125    /// more detail.
126    pub resolution: f64,
127    /// Tools used in the project, when using
128    /// [Camotics::from_program](struct.Camotics.html#method.new) the
129    /// program tools passed in will be converted to `CamoticsTool` instances.
130    pub tools: HashMap<u8, CamoticsTool>,
131    /// The size of the workpiece for the project.
132    pub workpiece: Workpiece,
133    /// The G-code files used by this project. When using
134    /// [Camotics::from_program](struct.Camotics.html#method.new)
135    /// the program G-code filename will be added from the name argument.
136    pub files: Vec<String>,
137}
138
139impl Camotics {
140    /// Creates a new `Camotics` project struct from a name, program tools with ordering, bounds, and resolution.
141    #[must_use]
142    pub fn new(name: &str, tools: &HashMap<Tool, u8>, workpiece: Bounds, resolution: f64) -> Self {
143        let mut tools_map = HashMap::new();
144        for (_, (tool, number)) in tools.iter().enumerate() {
145            tools_map.insert(*number, CamoticsTool::from_tool(*tool, *number));
146        }
147
148        Self {
149            name: name.to_string(),
150            units: Units::Metric,
151            resolution_mode: ResolutionMode::Manual,
152            resolution,
153            tools: tools_map,
154            workpiece: Workpiece {
155                automatic: false,
156                margin: 0.0,
157                bounds: workpiece,
158            },
159            files: vec![format!("{}.gcode", name)],
160        }
161    }
162
163    /// Creates a new `Camotics` struct from a name, program, and resolution.
164    #[must_use]
165    pub fn from_program(name: &str, program: &Program, resolution: f64) -> Self {
166        let mut tools = HashMap::new();
167
168        for tool in program.tools() {
169            tools.insert(tool, program.tool_ordering(&tool).unwrap());
170        }
171
172        let workpiece = program.bounds();
173        Self::new(name, &tools, workpiece, resolution)
174    }
175
176    /// Serializes the Camotics struct to the JSON format used by the Camotics
177    /// application when loading a project.
178    #[must_use]
179    pub fn to_json_string(&self) -> String {
180        serde_json::to_string_pretty(&self).unwrap()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use serde_json::Value;
187
188    use super::*;
189    use crate::cuts::*;
190
191    #[test]
192    fn test_serialization() {
193        let mut tools = HashMap::new();
194
195        tools.insert(
196            1,
197            CamoticsTool {
198                number: 1,
199                angle: None,
200                units: Units::Metric,
201                shape: CamoticsToolShape::Cylindrical,
202                length: 50.0,
203                diameter: 4.0,
204            },
205        );
206
207        let camotics = Camotics {
208            name: "testing".to_string(),
209            units: Units::Metric,
210            resolution_mode: ResolutionMode::Manual,
211            resolution: 0.3,
212            tools,
213            workpiece: Workpiece {
214                automatic: false,
215                margin: 5.0,
216                bounds: Bounds {
217                    min: Vector3 {
218                        x: -60.5,
219                        y: -60.5,
220                        z: -3.0,
221                    },
222                    max: Vector3 {
223                        x: 119.5,
224                        y: 60.5,
225                        z: 0.0,
226                    },
227                },
228            },
229            files: vec!["file.gcode".to_string()],
230        };
231
232        let serialized = serde_json::to_string(&camotics).unwrap();
233        let output: Value = serde_json::from_str(&serialized).unwrap();
234
235        let expected: Value = serde_json::from_str(
236            r#"
237            {
238                "units": "metric",
239                "resolution-mode": "manual",
240                "resolution": 0.3,
241                "tools": {
242                    "1": {
243                        "number": 1,
244                        "units": "metric",
245                        "shape": "cylindrical",
246                        "length": 50.0,
247                        "diameter": 4.0
248                    }
249                },
250                "workpiece": {
251                    "automatic": false,
252                    "margin": 5.0,
253                    "bounds": {
254                        "min": [-60.5, -60.5, -3.0],
255                        "max": [119.5, 60.5, 0.0]
256                    }
257                },
258                "files": [
259                    "file.gcode"
260                ]
261            }"#,
262        )
263        .unwrap();
264
265        assert_eq!(output, expected);
266    }
267
268    #[test]
269    fn test_camotics_from_program() {
270        let mut program = Program::new(Units::Metric, 10.0, 50.0);
271
272        let tool = Tool::cylindrical(
273            Units::Metric,
274            50.0,
275            4.0,
276            Direction::Clockwise,
277            5000.0,
278            400.0,
279        );
280
281        let mut context = program.context(tool);
282
283        context.append_cut(Cut::path(
284            Vector3::new(0.0, 0.0, 3.0),
285            vec![Segment::line(
286                Vector2::default(),
287                Vector2::new(-28.0, -30.0),
288            )],
289            -0.1,
290            1.0,
291        ));
292
293        context.append_cut(Cut::path(
294            Vector3::new(0.0, 0.0, 3.0),
295            vec![
296                Segment::line(Vector2::new(23.0, 12.0), Vector2::new(5.0, 10.0)),
297                Segment::line(Vector2::new(5.0, 10.0), Vector2::new(67.0, 102.0)),
298                Segment::line(Vector2::new(67.0, 102.0), Vector2::new(23.0, 12.0)),
299            ],
300            -0.1,
301            1.0,
302        ));
303
304        let camotics = Camotics::from_program("test-project", &program, 1.0);
305
306        let mut tools = HashMap::new();
307        tools.insert(1, CamoticsTool::from_tool(tool, 1));
308
309        assert_eq!(
310            camotics,
311            Camotics {
312                name: "test-project".to_string(),
313                units: Units::Metric,
314                resolution_mode: ResolutionMode::Manual,
315                resolution: 1.0,
316                tools,
317                workpiece: Workpiece {
318                    automatic: false,
319                    margin: 0.0,
320                    bounds: program.bounds()
321                },
322                files: vec!["test-project.gcode".to_string()]
323            }
324        );
325    }
326}