1mod arc;
3mod converter;
5mod machine;
7mod postprocess;
10mod turtle;
13
14pub use converter::{ConversionConfig, ConversionOptions, svg2program};
15pub use machine::{Machine, MachineConfig, SupportedFunctionality};
16pub use postprocess::PostprocessConfig;
17pub use turtle::Turtle;
18
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[derive(Debug, Default, Clone, PartialEq)]
22pub struct Settings {
23 pub conversion: ConversionConfig,
24 pub machine: MachineConfig,
25 pub postprocess: PostprocessConfig,
26 #[cfg_attr(feature = "serde", serde(default = "Version::unknown"))]
27 pub version: Version,
28}
29
30impl Settings {
31 pub fn try_upgrade(&mut self) -> Result<(), &'static str> {
38 loop {
39 match self.version {
40 Version::V0 => {
42 self.machine.end_sequence = Some(format!(
43 "{} M2",
44 self.machine.end_sequence.take().unwrap_or_default()
45 ));
46 self.version = Version::V5;
47 }
48 Version::V5 => break Ok(()),
49 Version::Unknown(_) => break Err("cannot upgrade unknown version"),
50 }
51 }
52 }
53}
54
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
60pub enum Version {
61 V0,
63 V5,
65 #[cfg_attr(feature = "serde", serde(untagged))]
66 Unknown(String),
67}
68
69impl Version {
70 pub const fn latest() -> Self {
72 Self::V5
73 }
74
75 pub const fn unknown() -> Self {
77 Self::V0
78 }
79}
80
81impl std::fmt::Display for Version {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Version::V0 => f.write_str("V0"),
85 Version::V5 => f.write_str("V5"),
86 Version::Unknown(unknown) => f.write_str(unknown),
87 }
88 }
89}
90
91impl Default for Version {
92 fn default() -> Self {
93 Self::latest()
94 }
95}
96
97#[cfg(test)]
98mod test {
99 use g_code::emit::{FormatOptions, Token};
100 use pretty_assertions::assert_eq;
101 use roxmltree::ParsingOptions;
102 use svgtypes::{Length, LengthUnit};
103
104 use super::*;
105
106 const TOLERANCE: f64 = 1E-10;
109
110 fn get_actual(
111 input: &str,
112 circular_interpolation: bool,
113 dimensions: [Option<Length>; 2],
114 ) -> Vec<Token<'_>> {
115 let config = ConversionConfig::default();
116 let options = ConversionOptions { dimensions };
117 let document = roxmltree::Document::parse_with_options(
118 input,
119 ParsingOptions {
120 allow_dtd: true,
121 ..Default::default()
122 },
123 )
124 .unwrap();
125
126 let machine = Machine::new(
127 SupportedFunctionality {
128 circular_interpolation,
129 },
130 None,
131 None,
132 None,
133 None,
134 );
135 converter::svg2program(&document, &config, options, machine)
136 }
137
138 fn assert_close(left: Vec<Token<'_>>, right: Vec<Token<'_>>) {
139 let mut code = String::new();
140 g_code::emit::format_gcode_fmt(left.iter(), FormatOptions::default(), &mut code).unwrap();
141 assert_eq!(left.len(), right.len(), "{code}");
142 for (i, pair) in left.into_iter().zip(right.into_iter()).enumerate() {
143 match pair {
144 (Token::Field(l), Token::Field(r)) => {
145 assert_eq!(l.letters, r.letters);
146 if let (Some(l_value), Some(r_value)) = (l.value.as_f64(), r.value.as_f64()) {
147 assert!(
148 (l_value - r_value).abs() < TOLERANCE,
149 "Values differ significantly at {i}: {l} vs {r} ({})",
150 (l_value - r_value).abs()
151 );
152 } else {
153 assert_eq!(l, r);
154 }
155 }
156 (l, r) => {
157 assert_eq!(l, r, "Differs at {i}");
158 }
159 }
160 }
161 }
162
163 #[test]
164 fn square_produces_expected_gcode() {
165 let expected = g_code::parse::file_parser(include_str!("../tests/square.gcode"))
166 .unwrap()
167 .iter_emit_tokens()
168 .collect::<Vec<_>>();
169 let actual = get_actual(include_str!("../tests/square.svg"), false, [None; 2]);
170
171 assert_close(actual, expected);
172 }
173
174 #[test]
175 fn square_dimension_override_produces_expected_gcode() {
176 let side_length = Length {
177 number: 10.,
178 unit: LengthUnit::Mm,
179 };
180
181 let expected = g_code::parse::file_parser(include_str!("../tests/square.gcode"))
182 .unwrap()
183 .iter_emit_tokens()
184 .collect::<Vec<_>>();
185
186 for square in [
187 include_str!("../tests/square.svg"),
188 include_str!("../tests/square_dimensionless.svg"),
189 ] {
190 assert_close(
191 get_actual(square, false, [Some(side_length); 2]),
192 expected.clone(),
193 );
194 assert_close(
195 get_actual(square, false, [Some(side_length), None]),
196 expected.clone(),
197 );
198 assert_close(
199 get_actual(square, false, [None, Some(side_length)]),
200 expected.clone(),
201 );
202 }
203 }
204
205 #[test]
206 fn square_transformed_produces_expected_gcode() {
207 let square_transformed = include_str!("../tests/square_transformed.svg");
208 let expected =
209 g_code::parse::file_parser(include_str!("../tests/square_transformed.gcode"))
210 .unwrap()
211 .iter_emit_tokens()
212 .collect::<Vec<_>>();
213 let actual = get_actual(square_transformed, false, [None; 2]);
214
215 assert_close(actual, expected)
216 }
217
218 #[test]
219 fn square_transformed_nested_produces_expected_gcode() {
220 let square_transformed = include_str!("../tests/square_transformed_nested.svg");
221 let expected =
222 g_code::parse::file_parser(include_str!("../tests/square_transformed_nested.gcode"))
223 .unwrap()
224 .iter_emit_tokens()
225 .collect::<Vec<_>>();
226 let actual = get_actual(square_transformed, false, [None; 2]);
227
228 assert_close(actual, expected)
229 }
230
231 #[test]
232 fn square_viewport_produces_expected_gcode() {
233 let square_viewport = include_str!("../tests/square_viewport.svg");
234 let expected = g_code::parse::file_parser(include_str!("../tests/square_viewport.gcode"))
235 .unwrap()
236 .iter_emit_tokens()
237 .collect::<Vec<_>>();
238 let actual = get_actual(square_viewport, false, [None; 2]);
239
240 assert_close(actual, expected);
241 }
242
243 #[test]
244 fn circular_interpolation_produces_expected_gcode() {
245 let circular_interpolation = include_str!("../tests/circular_interpolation.svg");
246 let expected =
247 g_code::parse::file_parser(include_str!("../tests/circular_interpolation.gcode"))
248 .unwrap()
249 .iter_emit_tokens()
250 .collect::<Vec<_>>();
251 let actual = get_actual(circular_interpolation, true, [None; 2]);
252
253 assert_close(actual, expected)
254 }
255
256 #[test]
257 fn svg_with_smooth_curves_produces_expected_gcode() {
258 let svg = include_str!("../tests/smooth_curves.svg");
259
260 let expected = g_code::parse::file_parser(include_str!("../tests/smooth_curves.gcode"))
261 .unwrap()
262 .iter_emit_tokens()
263 .collect::<Vec<_>>();
264
265 let file = if cfg!(debug) {
266 include_str!("../tests/smooth_curves_circular_interpolation.gcode")
267 } else {
268 include_str!("../tests/smooth_curves_circular_interpolation_release.gcode")
269 };
270 let expected_circular_interpolation = g_code::parse::file_parser(file)
271 .unwrap()
272 .iter_emit_tokens()
273 .collect::<Vec<_>>();
274 assert_close(get_actual(svg, false, [None; 2]), expected);
275
276 assert_close(
277 get_actual(svg, true, [None; 2]),
278 expected_circular_interpolation,
279 );
280 }
281
282 #[test]
283 fn shapes_produces_expected_gcode() {
284 let shapes = include_str!("../tests/shapes.svg");
285 let expected = g_code::parse::file_parser(include_str!("../tests/shapes.gcode"))
286 .unwrap()
287 .iter_emit_tokens()
288 .collect::<Vec<_>>();
289 let actual = get_actual(shapes, false, [None; 2]);
290
291 assert_close(actual, expected)
292 }
293
294 #[test]
295 fn use_defs_produces_expected_gcode() {
296 let svg = include_str!("../tests/use_defs.svg");
297 let expected = g_code::parse::file_parser(include_str!("../tests/use_defs.gcode"))
298 .unwrap()
299 .iter_emit_tokens()
300 .collect::<Vec<_>>();
301 let actual = get_actual(svg, false, [None; 2]);
302
303 assert_close(actual, expected)
304 }
305
306 #[test]
307 fn use_xlink_href_produces_expected_gcode() {
308 let svg = include_str!("../tests/use_xlink_href.svg");
309 let expected = g_code::parse::file_parser(include_str!("../tests/use_xlink_href.gcode"))
310 .unwrap()
311 .iter_emit_tokens()
312 .collect::<Vec<_>>();
313 let actual = get_actual(svg, false, [None; 2]);
314
315 assert_close(actual, expected)
316 }
317
318 #[test]
319 fn use_symbol_produces_expected_gcode() {
320 let svg = include_str!("../tests/use_symbol.svg");
321 let expected = g_code::parse::file_parser(include_str!("../tests/use_symbol.gcode"))
322 .unwrap()
323 .iter_emit_tokens()
324 .collect::<Vec<_>>();
325 let actual = get_actual(svg, false, [None; 2]);
326
327 assert_close(actual, expected);
328 }
329
330 #[test]
331 fn transform_origin_produces_expected_gcode() {
332 let svg = include_str!("../tests/transform_origin.svg");
333 let expected = g_code::parse::file_parser(include_str!("../tests/transform_origin.gcode"))
334 .unwrap()
335 .iter_emit_tokens()
336 .collect::<Vec<_>>();
337 let actual = get_actual(svg, false, [None; 2]);
338 assert_close(actual, expected)
339 }
340
341 #[test]
344 fn transform_origin_matches_manual_equivalent() {
345 let with_origin = get_actual(
346 include_str!("../tests/transform_origin.svg"),
347 false,
348 [None; 2],
349 );
350 let manual = get_actual(
351 include_str!("../tests/transform_origin_equivalent.svg"),
352 false,
353 [None; 2],
354 );
355 assert_close(with_origin, manual)
356 }
357
358 #[test]
359 #[cfg(feature = "serde")]
360 fn deserialize_v1_config_succeeds() {
361 let json = r#"
362 {
363 "conversion": {
364 "tolerance": 0.002,
365 "feedrate": 300.0,
366 "dpi": 96.0
367 },
368 "machine": {
369 "supported_functionality": {
370 "circular_interpolation": true
371 },
372 "tool_on_sequence": null,
373 "tool_off_sequence": null,
374 "begin_sequence": null,
375 "end_sequence": null
376 },
377 "postprocess": {
378 "origin": [
379 0.0,
380 0.0
381 ]
382 }
383 }
384 "#;
385 serde_json::from_str::<Settings>(json).unwrap();
386 }
387
388 #[test]
389 #[cfg(feature = "serde")]
390 fn deserialize_v2_config_succeeds() {
391 let json = r#"
392 {
393 "conversion": {
394 "tolerance": 0.002,
395 "feedrate": 300.0,
396 "dpi": 96.0
397 },
398 "machine": {
399 "supported_functionality": {
400 "circular_interpolation": true
401 },
402 "tool_on_sequence": null,
403 "tool_off_sequence": null,
404 "begin_sequence": null,
405 "end_sequence": null
406 },
407 "postprocess": { }
408 }
409 "#;
410 serde_json::from_str::<Settings>(json).unwrap();
411 }
412
413 #[test]
414 #[cfg(feature = "serde")]
415 fn deserialize_v3_config_succeeds() {
416 let json = r#"
417 {
418 "conversion": {
419 "tolerance": 0.002,
420 "feedrate": 300.0,
421 "dpi": 96.0
422 },
423 "machine": {
424 "supported_functionality": {
425 "circular_interpolation": true
426 },
427 "tool_on_sequence": null,
428 "tool_off_sequence": null,
429 "begin_sequence": null,
430 "end_sequence": null
431 },
432 "postprocess": {
433 "checksums": false,
434 "line_numbers": false
435 }
436 }
437 "#;
438 serde_json::from_str::<Settings>(json).unwrap();
439 }
440
441 #[test]
442 #[cfg(feature = "serde")]
443 fn deserialize_v4_config_succeeds() {
444 let json = r#"
445 {
446 "conversion": {
447 "tolerance": 0.002,
448 "feedrate": 300.0,
449 "dpi": 96.0
450 },
451 "machine": {
452 "supported_functionality": {
453 "circular_interpolation": true
454 },
455 "tool_on_sequence": null,
456 "tool_off_sequence": null,
457 "begin_sequence": null,
458 "end_sequence": null
459 },
460 "postprocess": {
461 "checksums": false,
462 "line_numbers": false,
463 "newline_before_comment": false
464 }
465 }
466 "#;
467 serde_json::from_str::<Settings>(json).unwrap();
468 }
469
470 #[test]
471 #[cfg(feature = "serde")]
472 fn deserialize_v5_config_succeeds() {
473 let json = r#"
474 {
475 "conversion": {
476 "tolerance": 0.002,
477 "feedrate": 300.0,
478 "dpi": 96.0
479 },
480 "machine": {
481 "supported_functionality": {
482 "circular_interpolation": true
483 },
484 "tool_on_sequence": null,
485 "tool_off_sequence": null,
486 "begin_sequence": null,
487 "end_sequence": null
488 },
489 "postprocess": {
490 "checksums": false,
491 "line_numbers": false,
492 "newline_before_comment": false
493 },
494 "version": "V5"
495 }
496 "#;
497 serde_json::from_str::<Settings>(json).unwrap();
498 }
499}