From 1b1c21aae256b3a61858b4cae2a742f2647873df Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 12:10:07 +0100 Subject: [PATCH 01/17] test(seance): add unit tests for hpgl module --- seance/src/hpgl.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/seance/src/hpgl.rs b/seance/src/hpgl.rs index 56158ee..cb4a68d 100644 --- a/seance/src/hpgl.rs +++ b/seance/src/hpgl.rs @@ -111,3 +111,15 @@ fn trace_path(path: &ResolvedPath) -> String { hpgl } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pen_change() { + assert_eq!(&pen_change(3), "SP4;"); + assert_eq!(&pen_change(0), "SP1;"); + // TODO: what is the desired behaviour for usize::MAX ? + } +} From 8f6d5fa91690c044d174147ba87573d2f713917a Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 13:03:46 +0100 Subject: [PATCH 02/17] test(seance): add unit tests for paths module --- seance/src/paths.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/seance/src/paths.rs b/seance/src/paths.rs index e9b606f..52e1c17 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -20,6 +20,7 @@ const MM_PER_PLOTTER_UNIT: f32 = 0.025; /// This is a point that is along a path that we wish to trace with the tool. /// The units are HPGL/2 units, which are rather nebulous and may vary from /// machine to machine in terms of their translation to mm. +#[derive(Debug, PartialEq, Eq)] pub struct ResolvedPoint { /// Horizontal axis position. pub x: i16, @@ -202,7 +203,7 @@ pub fn convert_points_to_plotter_units( } /// A point in terms of mm. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct PointInMillimeters { /// Horizontal axis. pub x: f32, @@ -297,3 +298,83 @@ pub fn mm_to_hpgl_units(mm: f32, is_x_axis: bool) -> i16 { let position_mm = if is_x_axis { mm } else { BED_HEIGHT_MM - mm }; (position_mm / MM_PER_PLOTTER_UNIT).round() as i16 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mm_to_hpgl_units() { + assert_eq!(mm_to_hpgl_units(10.0, false), 18128, "10mm"); + assert_eq!(mm_to_hpgl_units(0.0, true), 0, "0mm"); + assert_eq!(mm_to_hpgl_units(-0.0, true), 0, "-0mm"); + + // extreme values + assert_eq!(mm_to_hpgl_units(f32::MAX, true), 32767, "f32::MAX mm"); + assert_eq!( + mm_to_hpgl_units(819.175, true), + 32767, + "approx maximum computable value" + ); + assert_eq!(mm_to_hpgl_units(f32::MIN, true), -32768, "f32::MIN mm"); + assert_eq!( + mm_to_hpgl_units(-820.0, true), + -32768, + "approx minimum computable value" + ); + } + + #[test] + fn test_points_in_mm_to_printer_units() { + let points = &[ + PointInMillimeters { x: 10.0, y: 10.0 }, + PointInMillimeters { x: 11.0, y: 10.0 }, + ]; + let expected = &[ + ResolvedPoint { x: 400, y: 18128 }, + ResolvedPoint { x: 440, y: 18128 }, + ]; + + assert_eq!(&points_in_mm_to_printer_units(points), expected); + } + + #[test] + fn test_filter_paths_to_tool_passes() { + let mut passes = crate::default_passes::default_passes(); + // enable black + passes[0].set_enabled(true); + + let mut paths = [ + // black, is enabled + ( + PathColour([0, 0, 0]), + vec![vec![PointInMillimeters { x: 15.0, y: 100.5 }]], + ), + // dark grey, not used as a tool pass + ( + PathColour([10, 10, 10]), + vec![vec![PointInMillimeters { x: 500.0, y: -10.0 }]], + ), + // white - present but not enabled + ( + PathColour([255, 255, 255]), + vec![vec![PointInMillimeters { + x: -1010.5, + y: -f32::MAX, + }]], + ), + ] + .into_iter() + .collect(); + + let expected = [( + PathColour([0, 0, 0]), + vec![vec![PointInMillimeters { x: 15.0, y: 100.5 }]], + )] + .into_iter() + .collect(); + + filter_paths_to_tool_passes(&mut paths, &passes); + assert_eq!(paths, expected) + } +} From 574a5dc5f136983447909c37853873940e664da3 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 13:12:21 +0100 Subject: [PATCH 03/17] test(seance): add unit tests for laser_passes module --- seance/src/laser_passes.rs | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/seance/src/laser_passes.rs b/seance/src/laser_passes.rs index 143bc8d..1d11ecc 100644 --- a/seance/src/laser_passes.rs +++ b/seance/src/laser_passes.rs @@ -124,3 +124,69 @@ impl ToolPass { self.enabled = new_state; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tool_pass_new() { + assert_eq!( + ToolPass::new("non-restricted pass".to_string(), 0, 0, 0, 500, 100, true), + ToolPass { + name: "non-restricted pass".to_string(), + colour: [0, 0, 0], + power: 500, + speed: 100, + enabled: true + } + ); + + assert_eq!( + ToolPass::new( + "truncated power & speed pass".to_string(), + 0, + 0, + 0, + 10_000, + u64::MAX, + true + ), + ToolPass { + name: "truncated power & speed pass".to_string(), + colour: [0, 0, 0], + power: 1000, + speed: 1000, + enabled: true + } + ); + } + + #[test] + fn test_tool_pass_set_speed() { + let mut pass = ToolPass::new("".to_string(), 0, 0, 0, 100, 100, false); + assert_eq!(pass.speed, 100); + + // should not truncate + pass.set_speed(500); + assert_eq!(pass.speed, 500); + + // should truncate + pass.set_speed(1_000_000); + assert_eq!(pass.speed, 1000); + } + + #[test] + fn test_tool_pass_set_power() { + let mut pass = ToolPass::new("".to_string(), 0, 0, 0, 100, 100, false); + assert_eq!(pass.power, 100); + + // should not truncate + pass.set_power(10); + assert_eq!(pass.power, 10); + + // should truncate + pass.set_power(1001); + assert_eq!(pass.power, 1000); + } +} From 9088659ce71ce52f98f5501da69b05953c2c1ab9 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 15:09:08 +0100 Subject: [PATCH 04/17] refactor(seance): make mpgl, paths and pcl modules public --- seance/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seance/src/lib.rs b/seance/src/lib.rs index 1360f96..7e18f68 100644 --- a/seance/src/lib.rs +++ b/seance/src/lib.rs @@ -3,10 +3,10 @@ //! A utility for talking to devices that speak HPGL. pub mod default_passes; -mod hpgl; +pub mod hpgl; mod laser_passes; -mod paths; -mod pcl; +pub mod paths; +pub mod pcl; pub mod svg; use std::{fs, path::PathBuf}; From 17a00079dd4ca24d5dc4ce3cfbf5cdf2af099de3 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 15:09:53 +0100 Subject: [PATCH 05/17] feat(seance): add integration test for york hackspace logo PCL --- Cargo.lock | 44 +++++++++++++++++++ Cargo.toml | 1 + seance/Cargo.toml | 3 ++ seance/tests/pcl_snapshots.rs | 24 ++++++++++ ...s__york hackspace logo SVG PCL output.snap | 5 +++ 5 files changed, 77 insertions(+) create mode 100644 seance/tests/pcl_snapshots.rs create mode 100644 seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap diff --git a/Cargo.lock b/Cargo.lock index dd95df6..7510a9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1250,6 +1262,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2218,6 +2236,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "pin-project", + "similar", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -2393,6 +2424,12 @@ dependencies = [ "redox_syscall 0.5.9", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3809,6 +3846,7 @@ name = "seance" version = "0.1.0" dependencies = [ "ascii", + "insta", "log", "lyon_algorithms", "serde", @@ -3974,6 +4012,12 @@ dependencies = [ "quote", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simple-easing" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index dad2a81..9b6b83e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ opt-level = 2 serde = { version = "1", features = ["derive"] } serde_json = "1.0.120" usvg = "0.45.0" +insta = "1.42.2" diff --git a/seance/Cargo.toml b/seance/Cargo.toml index c8d487b..3edd39e 100644 --- a/seance/Cargo.toml +++ b/seance/Cargo.toml @@ -19,3 +19,6 @@ log = "0.4" lyon_algorithms = "1.0.4" serde = { version = "1", features = ["derive"] } usvg.workspace = true + +[dev-dependencies] +insta.workspace = true diff --git a/seance/tests/pcl_snapshots.rs b/seance/tests/pcl_snapshots.rs new file mode 100644 index 0000000..794249f --- /dev/null +++ b/seance/tests/pcl_snapshots.rs @@ -0,0 +1,24 @@ +#[test] +fn hackspace_logo() { + let design_file = seance::svg::parse_svg( + &std::fs::read("../logo.svg").expect("failed to read logo SVG file"), + ) + .expect("failed to parse logo SVG data"); + + let design_name = "York Hackspace Logo"; + let offset = seance::DesignOffset { x: 0.0, y: 0.0 }; + + let mut tool_passes = seance::default_passes::default_passes(); + for tool in &mut tool_passes { + tool.set_enabled(true); + } + + let paths = seance::svg::get_paths_grouped_by_colour(&design_file); + let mut paths_in_mm = seance::resolve_paths(&paths, &offset, 1.0); + seance::paths::filter_paths_to_tool_passes(&mut paths_in_mm, &tool_passes); + let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm); + let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes); + let pcl = seance::pcl::wrap_hpgl_in_pcl(hpgl, design_name, &tool_passes); + + insta::assert_snapshot!("york hackspace logo SVG PCL output", pcl); +} diff --git a/seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap b/seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap new file mode 100644 index 0000000..3b240cb --- /dev/null +++ b/seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/hpgl_snapshots.rs +expression: pcl +--- +%-12345XE!m19NYork Hackspace Logo!v16R1111111111111111!v64I0400040004000400040004000400040004000400040004000400040004000400!v64V0020002000200020002000200020002000200020002000200020002000200020!v64P0100010001000100010001000100010001000100010001000100010001000100!v16D*t508R&u508R!r0N%1A!r1000I!r1000K!r500P*t508R&u508R!m0S!s1S%1BIN;SC;PU;SP1;LT;PU0,18528;PU0,18528;SP0;EC0;EC1;OE;%1AE%-12345X From a8f78f7644694d68b5c1d0dad920b8a48bbcea15 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 20:24:13 +0100 Subject: [PATCH 06/17] feat(seance): add integration test for york hackspace logo HPGL --- seance/tests/hpgl_snapshots.rs | 21 +++++++++++++++++++ ...__york hackspace logo SVG HPGL output.snap | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 seance/tests/hpgl_snapshots.rs create mode 100644 seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap diff --git a/seance/tests/hpgl_snapshots.rs b/seance/tests/hpgl_snapshots.rs new file mode 100644 index 0000000..0c00ce5 --- /dev/null +++ b/seance/tests/hpgl_snapshots.rs @@ -0,0 +1,21 @@ +#[test] +fn hackspace_logo() { + let design_file = seance::svg::parse_svg( + &std::fs::read("../logo.svg").expect("failed to read logo SVG file"), + ) + .expect("failed to parse logo SVG data"); + let offset = seance::DesignOffset { x: 0.0, y: 0.0 }; + + let mut tool_passes = seance::default_passes::default_passes(); + for tool in &mut tool_passes { + tool.set_enabled(true); + } + + let paths = seance::svg::get_paths_grouped_by_colour(&design_file); + let mut paths_in_mm = seance::resolve_paths(&paths, &offset, 1.0); + seance::paths::filter_paths_to_tool_passes(&mut paths_in_mm, &tool_passes); + let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm); + let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes); + + insta::assert_snapshot!("york hackspace logo SVG HPGL output", hpgl); +} diff --git a/seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap b/seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap new file mode 100644 index 0000000..a1c9b42 --- /dev/null +++ b/seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/hpgl_snapshots.rs +expression: hpgl +--- +IN;SC;PU;SP1;LT;PU0,18528;PU0,18528;SP0;EC0;EC1;OE; From b01f23867b52a634574812531d32f90135cee6e1 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 20:34:10 +0100 Subject: [PATCH 07/17] feat(seance): snapshot testing for all top-level stages --- seance/tests/hpgl_snapshots.rs | 21 ------------------- .../{pcl_snapshots.rs => snapshot_tests.rs} | 13 ++++++++++-- ...sts__york hackspace logo HPGL output.snap} | 4 ++-- ...ests__york hackspace logo PCL output.snap} | 4 ++-- ..._tests__york hackspace logo SVG paths.snap | 5 +++++ ...s__york hackspace logo filtered paths.snap | 5 +++++ ...ts__york hackspace logo plotted paths.snap | 5 +++++ ...s__york hackspace logo resolved paths.snap | 5 +++++ 8 files changed, 35 insertions(+), 27 deletions(-) delete mode 100644 seance/tests/hpgl_snapshots.rs rename seance/tests/{pcl_snapshots.rs => snapshot_tests.rs} (65%) rename seance/tests/snapshots/{hpgl_snapshots__york hackspace logo SVG HPGL output.snap => snapshot_tests__york hackspace logo HPGL output.snap} (50%) rename seance/tests/snapshots/{pcl_snapshots__york hackspace logo SVG PCL output.snap => snapshot_tests__york hackspace logo PCL output.snap} (88%) create mode 100644 seance/tests/snapshots/snapshot_tests__york hackspace logo SVG paths.snap create mode 100644 seance/tests/snapshots/snapshot_tests__york hackspace logo filtered paths.snap create mode 100644 seance/tests/snapshots/snapshot_tests__york hackspace logo plotted paths.snap create mode 100644 seance/tests/snapshots/snapshot_tests__york hackspace logo resolved paths.snap diff --git a/seance/tests/hpgl_snapshots.rs b/seance/tests/hpgl_snapshots.rs deleted file mode 100644 index 0c00ce5..0000000 --- a/seance/tests/hpgl_snapshots.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[test] -fn hackspace_logo() { - let design_file = seance::svg::parse_svg( - &std::fs::read("../logo.svg").expect("failed to read logo SVG file"), - ) - .expect("failed to parse logo SVG data"); - let offset = seance::DesignOffset { x: 0.0, y: 0.0 }; - - let mut tool_passes = seance::default_passes::default_passes(); - for tool in &mut tool_passes { - tool.set_enabled(true); - } - - let paths = seance::svg::get_paths_grouped_by_colour(&design_file); - let mut paths_in_mm = seance::resolve_paths(&paths, &offset, 1.0); - seance::paths::filter_paths_to_tool_passes(&mut paths_in_mm, &tool_passes); - let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm); - let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes); - - insta::assert_snapshot!("york hackspace logo SVG HPGL output", hpgl); -} diff --git a/seance/tests/pcl_snapshots.rs b/seance/tests/snapshot_tests.rs similarity index 65% rename from seance/tests/pcl_snapshots.rs rename to seance/tests/snapshot_tests.rs index 794249f..b36b290 100644 --- a/seance/tests/pcl_snapshots.rs +++ b/seance/tests/snapshot_tests.rs @@ -14,11 +14,20 @@ fn hackspace_logo() { } let paths = seance::svg::get_paths_grouped_by_colour(&design_file); + insta::assert_debug_snapshot!("york hackspace logo SVG paths", &paths); + let mut paths_in_mm = seance::resolve_paths(&paths, &offset, 1.0); + insta::assert_debug_snapshot!("york hackspace logo resolved paths", &paths_in_mm); + seance::paths::filter_paths_to_tool_passes(&mut paths_in_mm, &tool_passes); + insta::assert_debug_snapshot!("york hackspace logo filtered paths", &paths_in_mm); + let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm); + insta::assert_debug_snapshot!("york hackspace logo plotted paths", &resolved_paths); + let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes); - let pcl = seance::pcl::wrap_hpgl_in_pcl(hpgl, design_name, &tool_passes); + insta::assert_snapshot!("york hackspace logo HPGL output", &hpgl); - insta::assert_snapshot!("york hackspace logo SVG PCL output", pcl); + let pcl = seance::pcl::wrap_hpgl_in_pcl(hpgl, design_name, &tool_passes); + insta::assert_snapshot!("york hackspace logo PCL output", &pcl); } diff --git a/seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo HPGL output.snap similarity index 50% rename from seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap rename to seance/tests/snapshots/snapshot_tests__york hackspace logo HPGL output.snap index a1c9b42..d4d75d0 100644 --- a/seance/tests/snapshots/hpgl_snapshots__york hackspace logo SVG HPGL output.snap +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo HPGL output.snap @@ -1,5 +1,5 @@ --- -source: seance/tests/hpgl_snapshots.rs -expression: hpgl +source: seance/tests/snapshot_tests.rs +expression: "&hpgl" --- IN;SC;PU;SP1;LT;PU0,18528;PU0,18528;SP0;EC0;EC1;OE; diff --git a/seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo PCL output.snap similarity index 88% rename from seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap rename to seance/tests/snapshots/snapshot_tests__york hackspace logo PCL output.snap index 3b240cb..11b62d1 100644 --- a/seance/tests/snapshots/pcl_snapshots__york hackspace logo SVG PCL output.snap +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo PCL output.snap @@ -1,5 +1,5 @@ --- -source: seance/tests/hpgl_snapshots.rs -expression: pcl +source: seance/tests/snapshot_tests.rs +expression: "&pcl" --- %-12345XE!m19NYork Hackspace Logo!v16R1111111111111111!v64I0400040004000400040004000400040004000400040004000400040004000400!v64V0020002000200020002000200020002000200020002000200020002000200020!v64P0100010001000100010001000100010001000100010001000100010001000100!v16D*t508R&u508R!r0N%1A!r1000I!r1000K!r500P*t508R&u508R!m0S!s1S%1BIN;SC;PU;SP1;LT;PU0,18528;PU0,18528;SP0;EC0;EC1;OE;%1AE%-12345X diff --git a/seance/tests/snapshots/snapshot_tests__york hackspace logo SVG paths.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo SVG paths.snap new file mode 100644 index 0000000..ac555b9 --- /dev/null +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo SVG paths.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/snapshot_tests.rs +expression: "&paths" +--- +{} diff --git a/seance/tests/snapshots/snapshot_tests__york hackspace logo filtered paths.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo filtered paths.snap new file mode 100644 index 0000000..f6db5bb --- /dev/null +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo filtered paths.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/snapshot_tests.rs +expression: "&paths_in_mm" +--- +{} diff --git a/seance/tests/snapshots/snapshot_tests__york hackspace logo plotted paths.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo plotted paths.snap new file mode 100644 index 0000000..24a5199 --- /dev/null +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo plotted paths.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/snapshot_tests.rs +expression: "&resolved_paths" +--- +{} diff --git a/seance/tests/snapshots/snapshot_tests__york hackspace logo resolved paths.snap b/seance/tests/snapshots/snapshot_tests__york hackspace logo resolved paths.snap new file mode 100644 index 0000000..f6db5bb --- /dev/null +++ b/seance/tests/snapshots/snapshot_tests__york hackspace logo resolved paths.snap @@ -0,0 +1,5 @@ +--- +source: seance/tests/snapshot_tests.rs +expression: "&paths_in_mm" +--- +{} From ff6adeaa7ef8a4f539c874504f5cf8d4b20590e6 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 21:17:52 +0100 Subject: [PATCH 08/17] refactor!: replace hardcoded bed values with config struct --- app/src/app.rs | 9 +- app/src/app/preview.rs | 7 +- planchette/src/main.rs | 3 +- seance/src/bed.rs | 148 +++++++++++++++++++++++++++++++++ seance/src/hpgl.rs | 12 +-- seance/src/lib.rs | 25 ++---- seance/src/paths.rs | 75 ++--------------- seance/tests/snapshot_tests.rs | 6 +- 8 files changed, 179 insertions(+), 106 deletions(-) create mode 100644 seance/src/bed.rs diff --git a/app/src/app.rs b/app/src/app.rs index 61c5bac..dfe705b 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -28,9 +28,10 @@ use preview::{DesignPreview, MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL}; use planchette::{ seance::{ + bed::BED_GCC_SPIRIT, default_passes, svg::{parse_svg, SVG_UNITS_PER_MM}, - DesignFile, DesignOffset, ToolPass, BED_HEIGHT_MM, BED_WIDTH_MM, + DesignFile, DesignOffset, ToolPass, }, PrintJob, }; @@ -1342,7 +1343,7 @@ fn ui_main( tool_passes_widget(ui, ui_context, tool_passes, tool_pass_widget_states); }); strip.cell(|ui| { - let ratio = BED_HEIGHT_MM / BED_WIDTH_MM; + let ratio = BED_GCC_SPIRIT.height / BED_GCC_SPIRIT.width; let mut width = ui.available_width(); let mut height = width * ratio; let max_height = ui.available_height() * 0.8; @@ -1526,7 +1527,7 @@ fn design_preview_navigation( let mut offset_x = current_offset.x; let offset_x_slider = DragValue::new(&mut offset_x) .max_decimals(2) - .range(0.0..=BED_WIDTH_MM) + .range(0.0..=BED_GCC_SPIRIT.width) .clamp_existing_to_range(true); if ui.add(offset_x_slider).changed() { ui_context.send_ui_message(UIMessage::DesignOffsetChanged { @@ -1541,7 +1542,7 @@ fn design_preview_navigation( let mut offset_y = current_offset.y; let offset_y_slider = DragValue::new(&mut offset_y) .max_decimals(2) - .range(0.0..=BED_HEIGHT_MM) + .range(0.0..=BED_GCC_SPIRIT.height) .clamp_existing_to_range(true); if ui.add(offset_y_slider).changed() { ui_context.send_ui_message(UIMessage::DesignOffsetChanged { diff --git a/app/src/app/preview.rs b/app/src/app/preview.rs index fe723a9..22f5dab 100644 --- a/app/src/app/preview.rs +++ b/app/src/app/preview.rs @@ -11,8 +11,7 @@ use egui::{ColorImage, ImageData, TextureHandle, TextureOptions}; use oneshot::TryRecvError; use planchette::seance::{ - resolve_paths, svg::get_paths_grouped_by_colour, DesignFile, DesignOffset, BED_HEIGHT_MM, - BED_WIDTH_MM, + bed::BED_GCC_SPIRIT, resolve_paths, svg::get_paths_grouped_by_colour, DesignFile, DesignOffset, }; use super::DesignWithMeta; @@ -314,8 +313,8 @@ fn render_inner( let texture_height = zoomed_bounding_box_height.floor() as u32; // Work out how many pixels correspond to 1mm in each dimension. - let pixels_per_mm_x = zoomed_bounding_box_width / BED_WIDTH_MM; - let pixels_per_mm_y = zoomed_bounding_box_height / BED_HEIGHT_MM; + let pixels_per_mm_x = zoomed_bounding_box_width / BED_GCC_SPIRIT.width; + let pixels_per_mm_y = zoomed_bounding_box_height / BED_GCC_SPIRIT.height; // We want to place a marker every 10mm to give the user a point of reference, so we need to work out how many pixels correspond to 10mm. let pixels_per_10_mm_x = pixels_per_mm_x * 10.0; diff --git a/planchette/src/main.rs b/planchette/src/main.rs index 6334754..3ec4d38 100644 --- a/planchette/src/main.rs +++ b/planchette/src/main.rs @@ -11,7 +11,7 @@ use axum::{ Json, Router, }; use planchette::PrintJob; -use seance::{cut_file, svg::parse_svg, SendToDeviceError, ToolPass}; +use seance::{bed::BED_GCC_SPIRIT, cut_file, svg::parse_svg, SendToDeviceError, ToolPass}; #[tokio::main] async fn main() { @@ -62,6 +62,7 @@ async fn send_file_to_device(Json(mut payload): Json) -> impl IntoResp &tree, &payload.file_name, &payload.tool_passes, + &BED_GCC_SPIRIT, &PathBuf::from("/dev/usb/lp0"), &payload.offset, ) { diff --git a/seance/src/bed.rs b/seance/src/bed.rs new file mode 100644 index 0000000..3085c3d --- /dev/null +++ b/seance/src/bed.rs @@ -0,0 +1,148 @@ +use crate::paths::{PointInMillimeters, ResolvedPoint, MM_PER_PLOTTER_UNIT}; + +/// Dimensions and offset information for a given device's print bed. +/// +/// All measurements are in millimetres. +pub struct PrintBed { + /// Minimum X position of the X axis. + pub x_min: f32, + /// Minimum Y position of the Y axis. + pub y_min: f32, + /// Maximum X position of the X axis. + pub x_max: f32, + /// Maximum Y position of the Y axis. + pub y_max: f32, + /// Width of the cutting area. + pub width: f32, + /// Height of the cutting area. + pub height: f32, + // TODO: are these values meaningfully different to x_max and y_max? + /// Whether to "mirror" the X axis. + /// + /// This might be desirable because, for example, the GCC Spirit has x=0 at the bottom. + /// Generally we want 0,0 to be in the top-left, so we would mirror the x axis in this case. + pub mirror_x: bool, + /// Whether to "mirror" the Y axis. + /// + /// This might be desirable because, for example, the GCC Spirit has x=0 at the bottom. + /// Generally we want 0,0 to be in the top-left, so we would mirror the x axis in this case. + pub mirror_y: bool, +} + +/// Bed configuration for the [GCC Spirit Laser Engraver](https://www.gccworld.com/product/laser-engraver-supremacy/spirit). +pub const BED_GCC_SPIRIT: PrintBed = PrintBed { + // Actually -50.72 but the cutter refuses to move this far... + x_min: 0.0, + x_max: 901.52, + // Again, actually -4.80 but 🤷. + y_min: 0.0, + y_max: 463.20, + + width: 901.52, + height: 463.20, + + mirror_x: false, + mirror_y: true, +}; + +impl PrintBed { + /// Converts a mm value into HPGL/2 units. + /// + /// # Arguments + /// * `value`: The value in mm. + /// * `mirror`: `None` if the value is not to be mirrored, + /// `Some(max)` if the value is to be mirrored where `max` is the maximum value on that axis. + #[inline] + fn mm_to_hpgl_units(&self, mut value: f32, mirror: Option) -> i16 { + if let Some(max) = mirror { + value = max - value; + } + + (value / MM_PER_PLOTTER_UNIT).round() as i16 + } + + /// Converts a mm value on the X axis into the value in HPGL/2 units. + /// + /// # Arguments + /// * `value`: The value in mm. + #[allow(clippy::cast_possible_truncation)] + pub fn mm_to_hpgl_units_x(&self, value: f32) -> i16 { + self.mm_to_hpgl_units(value, self.mirror_x.then_some(self.width)) + } + + /// Converts a mm value on the Y axis into the value in HPGL/2 units. + /// + /// # Arguments + /// * `value`: The value in mm. + #[allow(clippy::cast_possible_truncation)] + pub fn mm_to_hpgl_units_y(&self, value: f32) -> i16 { + self.mm_to_hpgl_units(value, self.mirror_y.then_some(self.height)) + } + + /// Takes a vector of points expressed in mm and turns them into a vector of resolved points. + /// + /// # Arguments + /// * `points`: Points in mm to resolve. + /// + /// # Returns + /// The provided points converted to HPGL/2 machine units. + pub fn points_in_mm_to_printer_units( + &self, + points: &[PointInMillimeters], + ) -> Vec { + let mut resolved_points = Vec::with_capacity(points.len()); + + for point in points { + resolved_points.push(ResolvedPoint { + x: self.mm_to_hpgl_units_x(point.x), + y: self.mm_to_hpgl_units_y(point.y), + }); + } + + resolved_points + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mm_to_hpgl_units() { + let bed = BED_GCC_SPIRIT; + + assert_eq!(bed.mm_to_hpgl_units_y(10.0), 18128, "10mm"); + assert_eq!(bed.mm_to_hpgl_units_x(0.0), 0, "0mm"); + assert_eq!(bed.mm_to_hpgl_units_x(-0.0), 0, "-0mm"); + + // extreme values + assert_eq!(bed.mm_to_hpgl_units_x(f32::MAX), 32767, "f32::MAX mm"); + assert_eq!( + bed.mm_to_hpgl_units_x(819.175), + 32767, + "approx maximum computable value" + ); + assert_eq!(bed.mm_to_hpgl_units_x(f32::MIN), -32768, "f32::MIN mm"); + assert_eq!( + bed.mm_to_hpgl_units_x(-820.0), + -32768, + "approx minimum computable value" + ); + } + + #[test] + fn test_points_in_mm_to_printer_units() { + let bed = BED_GCC_SPIRIT; + + let points = &[ + PointInMillimeters { x: 10.0, y: 10.0 }, + PointInMillimeters { x: 11.0, y: 10.0 }, + ]; + let expected = &[ + ResolvedPoint { x: 400, y: 18128 }, + ResolvedPoint { x: 440, y: 18128 }, + ]; + + assert_eq!(&bed.points_in_mm_to_printer_units(points), expected); + } +} diff --git a/seance/src/hpgl.rs b/seance/src/hpgl.rs index cb4a68d..e2becf1 100644 --- a/seance/src/hpgl.rs +++ b/seance/src/hpgl.rs @@ -5,7 +5,8 @@ use std::collections::HashMap; use crate::{ - paths::{mm_to_hpgl_units, PathColour, ResolvedPath}, + bed::PrintBed, + paths::{PathColour, ResolvedPath}, ToolPass, }; @@ -21,6 +22,7 @@ use crate::{ pub fn generate_hpgl( resolved_paths: &HashMap>, tool_passes: &[ToolPass], + print_bed: &PrintBed, ) -> String { if tool_passes.len() != 16 { return "Exactly 16 tool passes are required".to_string(); @@ -38,8 +40,8 @@ pub fn generate_hpgl( let var_name = format!( "IN;SC;PU;{}LT;PU{},{};", pen_change(first_pen), - mm_to_hpgl_units(0.0, true), - mm_to_hpgl_units(0.0, false) + print_bed.mm_to_hpgl_units_x(0.0), + print_bed.mm_to_hpgl_units_y(0.0) ); let mut hpgl = var_name; @@ -58,8 +60,8 @@ pub fn generate_hpgl( hpgl.push_str(&format!( "PU{},{};SP0;EC0;EC1;OE;", - mm_to_hpgl_units(0.0, true), - mm_to_hpgl_units(0.0, false) + print_bed.mm_to_hpgl_units_x(0.0), + print_bed.mm_to_hpgl_units_y(0.0) )); hpgl diff --git a/seance/src/lib.rs b/seance/src/lib.rs index 7e18f68..458a265 100644 --- a/seance/src/lib.rs +++ b/seance/src/lib.rs @@ -2,6 +2,7 @@ //! //! A utility for talking to devices that speak HPGL. +pub mod bed; pub mod default_passes; pub mod hpgl; mod laser_passes; @@ -11,6 +12,7 @@ pub mod svg; use std::{fs, path::PathBuf}; +use bed::PrintBed; use hpgl::generate_hpgl; pub use laser_passes::ToolPass; pub use paths::resolve_paths; @@ -19,24 +21,6 @@ use pcl::wrap_hpgl_in_pcl; use serde::{Deserialize, Serialize}; use svg::get_paths_grouped_by_colour; -/// Minimum X position of the X axis in mm. -/// Actually -50.72 but the cutter refuses to move this far... -pub const BED_X_AXIS_MINIMUM_MM: f32 = 0.0; -/// Maximum X position of the X axis in mm. -/// Actual value. -pub const BED_X_AXIS_MAXIMUM_MM: f32 = 901.52; -/// Minimum Y position of the Y axis in mm. -/// Again, actually -4.80 but 🤷. -pub const BED_Y_AXIS_MINIMUM_MM: f32 = 0.0; -/// Maximum Y position of the Y axis in mm. -/// Actual value. -pub const BED_Y_AXIS_MAXIMUM_MM: f32 = 463.20; - -/// The width of the cutting area, in mm. -pub const BED_WIDTH_MM: f32 = BED_X_AXIS_MAXIMUM_MM; -/// The height of the cutting area, in mm. -pub const BED_HEIGHT_MM: f32 = BED_Y_AXIS_MAXIMUM_MM; - /// A loaded design. pub struct DesignFile { /// The name of the design. @@ -107,14 +91,15 @@ pub fn cut_file( design_file: &usvg::Tree, design_name: &str, tool_passes: &Vec, + print_bed: &PrintBed, print_device: &PathBuf, offset: &DesignOffset, ) -> Result<(), SendToDeviceError> { let paths = get_paths_grouped_by_colour(design_file); let mut paths_in_mm = resolve_paths(&paths, offset, 1.0); filter_paths_to_tool_passes(&mut paths_in_mm, tool_passes); - let resolved_paths = convert_points_to_plotter_units(&paths_in_mm); - let hpgl = generate_hpgl(&resolved_paths, tool_passes); + let resolved_paths = convert_points_to_plotter_units(&paths_in_mm, print_bed); + let hpgl = generate_hpgl(&resolved_paths, tool_passes, print_bed); let pcl = wrap_hpgl_in_pcl(hpgl, design_name, tool_passes); fs::write(print_device, pcl.as_bytes()).unwrap(); diff --git a/seance/src/paths.rs b/seance/src/paths.rs index 52e1c17..323a6d3 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -11,11 +11,12 @@ use lyon_algorithms::path::PathSlice; use lyon_algorithms::walk::{walk_along_path, RegularPattern, WalkerEvent}; use usvg::Path; -use crate::{DesignOffset, ToolPass, BED_HEIGHT_MM}; +use crate::bed::PrintBed; +use crate::{DesignOffset, ToolPass}; /// The number of mm that are moved per unit that the plotter is instructed to move. /// This is the HPGL/2 default specified in the HPGL/2 specification. -const MM_PER_PLOTTER_UNIT: f32 = 0.025; +pub const MM_PER_PLOTTER_UNIT: f32 = 0.025; /// This is a point that is along a path that we wish to trace with the tool. /// The units are HPGL/2 units, which are rather nebulous and may vary from @@ -190,13 +191,14 @@ pub fn filter_paths_to_tool_passes( /// The paths expressed in plotter units. pub fn convert_points_to_plotter_units( paths_in_mm: &HashMap>, + print_bed: &PrintBed, ) -> HashMap> { let mut resolved_paths: HashMap> = HashMap::with_capacity(paths_in_mm.capacity()); for (path_colour, paths) in paths_in_mm { for path in paths { let entry = resolved_paths.entry(*path_colour).or_default(); - entry.push(points_in_mm_to_printer_units(path)); + entry.push(print_bed.points_in_mm_to_printer_units(path)); } } resolved_paths @@ -267,77 +269,10 @@ fn offset_point( point.y += offset_y; } -/// Takes a vector of points expressed in mm and turns them into a vector of resolved points. -/// -/// # Arguments -/// * `points`: Points in mm to resolve. -/// -/// # Returns -/// The provided points converted to HPGL/2 machine units. -fn points_in_mm_to_printer_units(points: &[PointInMillimeters]) -> Vec { - let mut resolved_points = Vec::with_capacity(points.len()); - - for point in points { - resolved_points.push(ResolvedPoint { - x: mm_to_hpgl_units(point.x, true), - y: mm_to_hpgl_units(point.y, false), - }); - } - - resolved_points -} - -/// Converts a mm value into the value in HPGL/2 units. -/// -/// # Arguments -/// * `mm`: The value in mm. -/// * `is_x_axis`: The GCC Spirit has x=0 at the bottom. Generally we want 0,0 to be -/// in the top-left, so we mirror the x axis in this case. -#[allow(clippy::cast_possible_truncation)] -pub fn mm_to_hpgl_units(mm: f32, is_x_axis: bool) -> i16 { - let position_mm = if is_x_axis { mm } else { BED_HEIGHT_MM - mm }; - (position_mm / MM_PER_PLOTTER_UNIT).round() as i16 -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn test_mm_to_hpgl_units() { - assert_eq!(mm_to_hpgl_units(10.0, false), 18128, "10mm"); - assert_eq!(mm_to_hpgl_units(0.0, true), 0, "0mm"); - assert_eq!(mm_to_hpgl_units(-0.0, true), 0, "-0mm"); - - // extreme values - assert_eq!(mm_to_hpgl_units(f32::MAX, true), 32767, "f32::MAX mm"); - assert_eq!( - mm_to_hpgl_units(819.175, true), - 32767, - "approx maximum computable value" - ); - assert_eq!(mm_to_hpgl_units(f32::MIN, true), -32768, "f32::MIN mm"); - assert_eq!( - mm_to_hpgl_units(-820.0, true), - -32768, - "approx minimum computable value" - ); - } - - #[test] - fn test_points_in_mm_to_printer_units() { - let points = &[ - PointInMillimeters { x: 10.0, y: 10.0 }, - PointInMillimeters { x: 11.0, y: 10.0 }, - ]; - let expected = &[ - ResolvedPoint { x: 400, y: 18128 }, - ResolvedPoint { x: 440, y: 18128 }, - ]; - - assert_eq!(&points_in_mm_to_printer_units(points), expected); - } - #[test] fn test_filter_paths_to_tool_passes() { let mut passes = crate::default_passes::default_passes(); diff --git a/seance/tests/snapshot_tests.rs b/seance/tests/snapshot_tests.rs index b36b290..ebeebb3 100644 --- a/seance/tests/snapshot_tests.rs +++ b/seance/tests/snapshot_tests.rs @@ -13,6 +13,8 @@ fn hackspace_logo() { tool.set_enabled(true); } + let bed = seance::bed::BED_GCC_SPIRIT; + let paths = seance::svg::get_paths_grouped_by_colour(&design_file); insta::assert_debug_snapshot!("york hackspace logo SVG paths", &paths); @@ -22,10 +24,10 @@ fn hackspace_logo() { seance::paths::filter_paths_to_tool_passes(&mut paths_in_mm, &tool_passes); insta::assert_debug_snapshot!("york hackspace logo filtered paths", &paths_in_mm); - let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm); + let resolved_paths = seance::paths::convert_points_to_plotter_units(&paths_in_mm, &bed); insta::assert_debug_snapshot!("york hackspace logo plotted paths", &resolved_paths); - let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes); + let hpgl = seance::hpgl::generate_hpgl(&resolved_paths, &tool_passes, &bed); insta::assert_snapshot!("york hackspace logo HPGL output", &hpgl); let pcl = seance::pcl::wrap_hpgl_in_pcl(hpgl, design_name, &tool_passes); From 035dd9442f7a4835aa7d70dbbe48881893e3cadb Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 21:24:28 +0100 Subject: [PATCH 09/17] feat(seance): impl From tuples for Point types --- seance/src/paths.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/seance/src/paths.rs b/seance/src/paths.rs index 323a6d3..a558a82 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -28,6 +28,16 @@ pub struct ResolvedPoint { /// Vertical axis position. pub y: i16, } + +impl From<(i16, i16)> for ResolvedPoint { + fn from(value: (i16, i16)) -> Self { + Self { + x: value.0, + y: value.1, + } + } +} + /// A path that the toolhead will move through, comprised of a series of points in-order. pub type ResolvedPath = Vec; /// A toolpath expressed as a series of points in mm. @@ -228,6 +238,15 @@ impl From> for PointInM } } +impl From<(f32, f32)> for PointInMillimeters { + fn from(value: (f32, f32)) -> Self { + Self { + x: value.0, + y: value.1, + } + } +} + /// Works out the points along a path and adds them to a vector of points. /// /// # Arguments From ebbebc254d7d25ebdfca4e7fb44140e2c37144e5 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 21:35:59 +0100 Subject: [PATCH 10/17] refactor(seance)!: rewrite mm to HPGL conversion APIs into single cohesive method --- seance/src/bed.rs | 116 ++++++++++++++++---------------------------- seance/src/hpgl.rs | 17 +++---- seance/src/paths.rs | 6 ++- 3 files changed, 55 insertions(+), 84 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index 3085c3d..baed5a8 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -46,60 +46,24 @@ pub const BED_GCC_SPIRIT: PrintBed = PrintBed { }; impl PrintBed { - /// Converts a mm value into HPGL/2 units. + /// Converts a [`PointInMillimeters`] into the same point in HPGL/2 units. /// /// # Arguments - /// * `value`: The value in mm. - /// * `mirror`: `None` if the value is not to be mirrored, - /// `Some(max)` if the value is to be mirrored where `max` is the maximum value on that axis. - #[inline] - fn mm_to_hpgl_units(&self, mut value: f32, mirror: Option) -> i16 { - if let Some(max) = mirror { - value = max - value; + /// * `point`: The point to convert from mm. + pub fn point_mm_to_hpgl_units(&self, point: PointInMillimeters) -> ResolvedPoint { + #[inline] + fn mm_to_hpgl_units(mut value: f32, mirror: Option) -> i16 { + if let Some(max) = mirror { + value = max - value; + } + + (value / MM_PER_PLOTTER_UNIT).round() as i16 } - (value / MM_PER_PLOTTER_UNIT).round() as i16 - } - - /// Converts a mm value on the X axis into the value in HPGL/2 units. - /// - /// # Arguments - /// * `value`: The value in mm. - #[allow(clippy::cast_possible_truncation)] - pub fn mm_to_hpgl_units_x(&self, value: f32) -> i16 { - self.mm_to_hpgl_units(value, self.mirror_x.then_some(self.width)) - } - - /// Converts a mm value on the Y axis into the value in HPGL/2 units. - /// - /// # Arguments - /// * `value`: The value in mm. - #[allow(clippy::cast_possible_truncation)] - pub fn mm_to_hpgl_units_y(&self, value: f32) -> i16 { - self.mm_to_hpgl_units(value, self.mirror_y.then_some(self.height)) - } - - /// Takes a vector of points expressed in mm and turns them into a vector of resolved points. - /// - /// # Arguments - /// * `points`: Points in mm to resolve. - /// - /// # Returns - /// The provided points converted to HPGL/2 machine units. - pub fn points_in_mm_to_printer_units( - &self, - points: &[PointInMillimeters], - ) -> Vec { - let mut resolved_points = Vec::with_capacity(points.len()); - - for point in points { - resolved_points.push(ResolvedPoint { - x: self.mm_to_hpgl_units_x(point.x), - y: self.mm_to_hpgl_units_y(point.y), - }); + ResolvedPoint { + x: mm_to_hpgl_units(point.x, self.mirror_x.then_some(self.width)), + y: mm_to_hpgl_units(point.y, self.mirror_y.then_some(self.height)), } - - resolved_points } } @@ -111,38 +75,42 @@ mod tests { fn test_mm_to_hpgl_units() { let bed = BED_GCC_SPIRIT; - assert_eq!(bed.mm_to_hpgl_units_y(10.0), 18128, "10mm"); - assert_eq!(bed.mm_to_hpgl_units_x(0.0), 0, "0mm"); - assert_eq!(bed.mm_to_hpgl_units_x(-0.0), 0, "-0mm"); + assert_eq!( + bed.point_mm_to_hpgl_units((10.0, 10.0).into()), + (400, 18128).into(), + "10mm" + ); + assert_eq!( + bed.point_mm_to_hpgl_units((0.0, 0.0).into()), + (0, 18528).into(), + "0mm" + ); + assert_eq!( + bed.point_mm_to_hpgl_units((-0.0, -0.0).into()), + (0, 18528).into(), + "-0mm" + ); // extreme values - assert_eq!(bed.mm_to_hpgl_units_x(f32::MAX), 32767, "f32::MAX mm"); assert_eq!( - bed.mm_to_hpgl_units_x(819.175), - 32767, + bed.point_mm_to_hpgl_units((f32::MAX, f32::MAX).into()), + (32767, -32768).into(), + "f32::MAX mm" + ); + assert_eq!( + bed.point_mm_to_hpgl_units((819.175, 819.175).into()), + (32767, -14239).into(), "approx maximum computable value" ); - assert_eq!(bed.mm_to_hpgl_units_x(f32::MIN), -32768, "f32::MIN mm"); assert_eq!( - bed.mm_to_hpgl_units_x(-820.0), - -32768, + bed.point_mm_to_hpgl_units((f32::MIN, f32::MIN).into()), + (-32768, 32767).into(), + "f32::MIN mm" + ); + assert_eq!( + bed.point_mm_to_hpgl_units((-820.0, -820.0).into()), + (-32768, 32767).into(), "approx minimum computable value" ); } - - #[test] - fn test_points_in_mm_to_printer_units() { - let bed = BED_GCC_SPIRIT; - - let points = &[ - PointInMillimeters { x: 10.0, y: 10.0 }, - PointInMillimeters { x: 11.0, y: 10.0 }, - ]; - let expected = &[ - ResolvedPoint { x: 400, y: 18128 }, - ResolvedPoint { x: 440, y: 18128 }, - ]; - - assert_eq!(&bed.points_in_mm_to_printer_units(points), expected); - } } diff --git a/seance/src/hpgl.rs b/seance/src/hpgl.rs index e2becf1..ecf29b0 100644 --- a/seance/src/hpgl.rs +++ b/seance/src/hpgl.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use crate::{ bed::PrintBed, - paths::{PathColour, ResolvedPath}, + paths::{PathColour, ResolvedPath, ResolvedPoint}, ToolPass, }; @@ -36,12 +36,15 @@ pub fn generate_hpgl( return "No tool passes enabled".to_string(); }; + let ResolvedPoint { + x: x_origin, + y: y_origin, + } = print_bed.point_mm_to_hpgl_units((0.0, 0.0).into()); + // In, Default Coordinate System, Pen Up, Select first pen, reset line type, move to 0,0. let var_name = format!( - "IN;SC;PU;{}LT;PU{},{};", + "IN;SC;PU;{}LT;PU{x_origin},{y_origin};", pen_change(first_pen), - print_bed.mm_to_hpgl_units_x(0.0), - print_bed.mm_to_hpgl_units_y(0.0) ); let mut hpgl = var_name; @@ -58,11 +61,7 @@ pub fn generate_hpgl( } } - hpgl.push_str(&format!( - "PU{},{};SP0;EC0;EC1;OE;", - print_bed.mm_to_hpgl_units_x(0.0), - print_bed.mm_to_hpgl_units_y(0.0) - )); + hpgl.push_str(&format!("PU{x_origin},{y_origin};SP0;EC0;EC1;OE;")); hpgl } diff --git a/seance/src/paths.rs b/seance/src/paths.rs index a558a82..4186b67 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -208,7 +208,11 @@ pub fn convert_points_to_plotter_units( for (path_colour, paths) in paths_in_mm { for path in paths { let entry = resolved_paths.entry(*path_colour).or_default(); - entry.push(print_bed.points_in_mm_to_printer_units(path)); + entry.push( + path.into_iter() + .map(|p| print_bed.point_mm_to_hpgl_units(*p)) + .collect(), + ); } } resolved_paths From efb77d0cd59b744aefa2c360c6b463f7c3ee63a4 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 21:48:31 +0100 Subject: [PATCH 11/17] refactor(seance)!: safety check for value truncations in bed unit conversions --- seance/src/bed.rs | 53 +++++++++++++++++++++++++++------------------ seance/src/hpgl.rs | 4 +++- seance/src/paths.rs | 6 ++++- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index baed5a8..a4a65fb 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -48,22 +48,32 @@ pub const BED_GCC_SPIRIT: PrintBed = PrintBed { impl PrintBed { /// Converts a [`PointInMillimeters`] into the same point in HPGL/2 units. /// + /// Returns `None` if the point is out of the representable range of HPGL's 16-bit integers. + /// /// # Arguments /// * `point`: The point to convert from mm. - pub fn point_mm_to_hpgl_units(&self, point: PointInMillimeters) -> ResolvedPoint { + pub fn point_mm_to_hpgl_units(&self, point: PointInMillimeters) -> Option { #[inline] - fn mm_to_hpgl_units(mut value: f32, mirror: Option) -> i16 { + fn mm_to_hpgl_units(mut value: f32, mirror: Option) -> Option { if let Some(max) = mirror { value = max - value; } - (value / MM_PER_PLOTTER_UNIT).round() as i16 + let adjusted = value / MM_PER_PLOTTER_UNIT; + if adjusted > i16::MAX as f32 || adjusted < i16::MIN as f32 { + // value would be truncated + None + } else { + Some(adjusted.round() as i16) + } } - ResolvedPoint { - x: mm_to_hpgl_units(point.x, self.mirror_x.then_some(self.width)), - y: mm_to_hpgl_units(point.y, self.mirror_y.then_some(self.height)), - } + // TODO: check printer bed sizes won't automatically cause truncation + + Some(ResolvedPoint { + x: mm_to_hpgl_units(point.x, self.mirror_x.then_some(self.width))?, + y: mm_to_hpgl_units(point.y, self.mirror_y.then_some(self.height))?, + }) } } @@ -76,41 +86,42 @@ mod tests { let bed = BED_GCC_SPIRIT; assert_eq!( - bed.point_mm_to_hpgl_units((10.0, 10.0).into()), + bed.point_mm_to_hpgl_units((10.0, 10.0).into()).unwrap(), (400, 18128).into(), "10mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((0.0, 0.0).into()), + bed.point_mm_to_hpgl_units((0.0, 0.0).into()).unwrap(), (0, 18528).into(), "0mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((-0.0, -0.0).into()), + bed.point_mm_to_hpgl_units((-0.0, -0.0).into()).unwrap(), (0, 18528).into(), "-0mm" ); // extreme values - assert_eq!( - bed.point_mm_to_hpgl_units((f32::MAX, f32::MAX).into()), - (32767, -32768).into(), + assert!( + bed.point_mm_to_hpgl_units((f32::MAX, f32::MAX).into()) + .is_none(), "f32::MAX mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((819.175, 819.175).into()), + bed.point_mm_to_hpgl_units((819.175, 819.175).into()) + .unwrap(), (32767, -14239).into(), "approx maximum computable value" ); - assert_eq!( - bed.point_mm_to_hpgl_units((f32::MIN, f32::MIN).into()), - (-32768, 32767).into(), + assert!( + bed.point_mm_to_hpgl_units((f32::MIN, f32::MIN).into()) + .is_none(), "f32::MIN mm" ); - assert_eq!( - bed.point_mm_to_hpgl_units((-820.0, -820.0).into()), - (-32768, 32767).into(), - "approx minimum computable value" + assert!( + bed.point_mm_to_hpgl_units((-818.0, -818.0).into()) + .is_none(), + "negative values" ); } } diff --git a/seance/src/hpgl.rs b/seance/src/hpgl.rs index ecf29b0..482e658 100644 --- a/seance/src/hpgl.rs +++ b/seance/src/hpgl.rs @@ -39,7 +39,9 @@ pub fn generate_hpgl( let ResolvedPoint { x: x_origin, y: y_origin, - } = print_bed.point_mm_to_hpgl_units((0.0, 0.0).into()); + } = print_bed + .point_mm_to_hpgl_units((0.0, 0.0).into()) + .expect("adjusted origin point of print bed is unrepresentable in HPGL"); // In, Default Coordinate System, Pen Up, Select first pen, reset line type, move to 0,0. let var_name = format!( diff --git a/seance/src/paths.rs b/seance/src/paths.rs index 4186b67..439ccb1 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -210,7 +210,11 @@ pub fn convert_points_to_plotter_units( let entry = resolved_paths.entry(*path_colour).or_default(); entry.push( path.into_iter() - .map(|p| print_bed.point_mm_to_hpgl_units(*p)) + .map(|p| { + print_bed + .point_mm_to_hpgl_units(*p) + .expect("point is unrepresentable") + }) .collect(), ); } From 7cc474f3a64d106e333f7312cb6ec91d941e8157 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 22:02:15 +0100 Subject: [PATCH 12/17] refactor(seance)!: add panicking sanity checks --- seance/src/bed.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index a4a65fb..cfbdaf8 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -52,6 +52,9 @@ impl PrintBed { /// /// # Arguments /// * `point`: The point to convert from mm. + /// + /// # Panics + /// Panics when the values of `self` would cause truncation at the origin. pub fn point_mm_to_hpgl_units(&self, point: PointInMillimeters) -> Option { #[inline] fn mm_to_hpgl_units(mut value: f32, mirror: Option) -> Option { @@ -68,7 +71,14 @@ impl PrintBed { } } - // TODO: check printer bed sizes won't automatically cause truncation + // check printer bed sizes won't automatically cause truncation + // TODO: do this in constructor? + if self.mirror_x && mm_to_hpgl_units(self.x_max, None).is_none() { + panic!("x-axis mirroring is enabled but the axis is so large it will be truncated") + } + if self.mirror_y && mm_to_hpgl_units(self.y_max, None).is_none() { + panic!("y-axis mirroring is enabled but the axis is so large it will be truncated") + } Some(ResolvedPoint { x: mm_to_hpgl_units(point.x, self.mirror_x.then_some(self.width))?, From 06238b7da2072c80fd2ae5189e6a523e46d32ae0 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 22:03:26 +0100 Subject: [PATCH 13/17] refactor(seance)!: method renaming --- seance/src/bed.rs | 30 +++++++++++++----------------- seance/src/hpgl.rs | 2 +- seance/src/paths.rs | 6 +----- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index cfbdaf8..5c210d8 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -55,9 +55,9 @@ impl PrintBed { /// /// # Panics /// Panics when the values of `self` would cause truncation at the origin. - pub fn point_mm_to_hpgl_units(&self, point: PointInMillimeters) -> Option { + pub fn place_point(&self, point: PointInMillimeters) -> Option { #[inline] - fn mm_to_hpgl_units(mut value: f32, mirror: Option) -> Option { + fn mm_to_hpgl(mut value: f32, mirror: Option) -> Option { if let Some(max) = mirror { value = max - value; } @@ -73,16 +73,16 @@ impl PrintBed { // check printer bed sizes won't automatically cause truncation // TODO: do this in constructor? - if self.mirror_x && mm_to_hpgl_units(self.x_max, None).is_none() { + if self.mirror_x && mm_to_hpgl(self.x_max, None).is_none() { panic!("x-axis mirroring is enabled but the axis is so large it will be truncated") } - if self.mirror_y && mm_to_hpgl_units(self.y_max, None).is_none() { + if self.mirror_y && mm_to_hpgl(self.y_max, None).is_none() { panic!("y-axis mirroring is enabled but the axis is so large it will be truncated") } Some(ResolvedPoint { - x: mm_to_hpgl_units(point.x, self.mirror_x.then_some(self.width))?, - y: mm_to_hpgl_units(point.y, self.mirror_y.then_some(self.height))?, + x: mm_to_hpgl(point.x, self.mirror_x.then_some(self.width))?, + y: mm_to_hpgl(point.y, self.mirror_y.then_some(self.height))?, }) } } @@ -96,41 +96,37 @@ mod tests { let bed = BED_GCC_SPIRIT; assert_eq!( - bed.point_mm_to_hpgl_units((10.0, 10.0).into()).unwrap(), + bed.place_point((10.0, 10.0).into()).unwrap(), (400, 18128).into(), "10mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((0.0, 0.0).into()).unwrap(), + bed.place_point((0.0, 0.0).into()).unwrap(), (0, 18528).into(), "0mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((-0.0, -0.0).into()).unwrap(), + bed.place_point((-0.0, -0.0).into()).unwrap(), (0, 18528).into(), "-0mm" ); // extreme values assert!( - bed.point_mm_to_hpgl_units((f32::MAX, f32::MAX).into()) - .is_none(), + bed.place_point((f32::MAX, f32::MAX).into()).is_none(), "f32::MAX mm" ); assert_eq!( - bed.point_mm_to_hpgl_units((819.175, 819.175).into()) - .unwrap(), + bed.place_point((819.175, 819.175).into()).unwrap(), (32767, -14239).into(), "approx maximum computable value" ); assert!( - bed.point_mm_to_hpgl_units((f32::MIN, f32::MIN).into()) - .is_none(), + bed.place_point((f32::MIN, f32::MIN).into()).is_none(), "f32::MIN mm" ); assert!( - bed.point_mm_to_hpgl_units((-818.0, -818.0).into()) - .is_none(), + bed.place_point((-818.0, -818.0).into()).is_none(), "negative values" ); } diff --git a/seance/src/hpgl.rs b/seance/src/hpgl.rs index 482e658..3123662 100644 --- a/seance/src/hpgl.rs +++ b/seance/src/hpgl.rs @@ -40,7 +40,7 @@ pub fn generate_hpgl( x: x_origin, y: y_origin, } = print_bed - .point_mm_to_hpgl_units((0.0, 0.0).into()) + .place_point((0.0, 0.0).into()) .expect("adjusted origin point of print bed is unrepresentable in HPGL"); // In, Default Coordinate System, Pen Up, Select first pen, reset line type, move to 0,0. diff --git a/seance/src/paths.rs b/seance/src/paths.rs index 439ccb1..bbca710 100644 --- a/seance/src/paths.rs +++ b/seance/src/paths.rs @@ -210,11 +210,7 @@ pub fn convert_points_to_plotter_units( let entry = resolved_paths.entry(*path_colour).or_default(); entry.push( path.into_iter() - .map(|p| { - print_bed - .point_mm_to_hpgl_units(*p) - .expect("point is unrepresentable") - }) + .map(|p| print_bed.place_point(*p).expect("point is unrepresentable")) .collect(), ); } From 049f0bbb8ee283859300ce1b6c7e75d1178ecc71 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 6 Apr 2025 23:19:47 +0100 Subject: [PATCH 14/17] docs(seance): small clarifications to PrintBed --- seance/src/bed.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index 5c210d8..3d97d80 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -46,9 +46,9 @@ pub const BED_GCC_SPIRIT: PrintBed = PrintBed { }; impl PrintBed { - /// Converts a [`PointInMillimeters`] into the same point in HPGL/2 units. + /// Converts a [`PointInMillimeters`] into the same point in HPGL/2 units **for this printer**. /// - /// Returns `None` if the point is out of the representable range of HPGL's 16-bit integers. + /// Returns `None` if the point is out of the bed of this printer. /// /// # Arguments /// * `point`: The point to convert from mm. @@ -73,12 +73,14 @@ impl PrintBed { // check printer bed sizes won't automatically cause truncation // TODO: do this in constructor? - if self.mirror_x && mm_to_hpgl(self.x_max, None).is_none() { - panic!("x-axis mirroring is enabled but the axis is so large it will be truncated") - } - if self.mirror_y && mm_to_hpgl(self.y_max, None).is_none() { - panic!("y-axis mirroring is enabled but the axis is so large it will be truncated") - } + debug_assert!( + self.mirror_x && mm_to_hpgl(self.x_max, None).is_none(), + "x-axis mirroring is enabled but the axis is so large it would truncate" + ); + debug_assert!( + self.mirror_y && mm_to_hpgl(self.y_max, None).is_none(), + "y-axis mirroring is enabled but the axis is so large it would truncate" + ); Some(ResolvedPoint { x: mm_to_hpgl(point.x, self.mirror_x.then_some(self.width))?, From 4156a73f8f2a74a392e06c6f02606ce65f5f75c9 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Mon, 7 Apr 2025 01:31:13 +0100 Subject: [PATCH 15/17] refactor(seance)!: convert internals of PrintBed --- app/src/app.rs | 6 +- app/src/app/preview.rs | 4 +- seance/src/bed.rs | 161 +++++++++++++++++++++++---------- seance/tests/snapshot_tests.rs | 2 +- 4 files changed, 121 insertions(+), 52 deletions(-) diff --git a/app/src/app.rs b/app/src/app.rs index dfe705b..f0d996b 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -1343,7 +1343,7 @@ fn ui_main( tool_passes_widget(ui, ui_context, tool_passes, tool_pass_widget_states); }); strip.cell(|ui| { - let ratio = BED_GCC_SPIRIT.height / BED_GCC_SPIRIT.width; + let ratio = BED_GCC_SPIRIT.height() / BED_GCC_SPIRIT.width(); let mut width = ui.available_width(); let mut height = width * ratio; let max_height = ui.available_height() * 0.8; @@ -1527,7 +1527,7 @@ fn design_preview_navigation( let mut offset_x = current_offset.x; let offset_x_slider = DragValue::new(&mut offset_x) .max_decimals(2) - .range(0.0..=BED_GCC_SPIRIT.width) + .range(0.0..=BED_GCC_SPIRIT.width()) .clamp_existing_to_range(true); if ui.add(offset_x_slider).changed() { ui_context.send_ui_message(UIMessage::DesignOffsetChanged { @@ -1542,7 +1542,7 @@ fn design_preview_navigation( let mut offset_y = current_offset.y; let offset_y_slider = DragValue::new(&mut offset_y) .max_decimals(2) - .range(0.0..=BED_GCC_SPIRIT.height) + .range(0.0..=BED_GCC_SPIRIT.height()) .clamp_existing_to_range(true); if ui.add(offset_y_slider).changed() { ui_context.send_ui_message(UIMessage::DesignOffsetChanged { diff --git a/app/src/app/preview.rs b/app/src/app/preview.rs index 22f5dab..53856ba 100644 --- a/app/src/app/preview.rs +++ b/app/src/app/preview.rs @@ -313,8 +313,8 @@ fn render_inner( let texture_height = zoomed_bounding_box_height.floor() as u32; // Work out how many pixels correspond to 1mm in each dimension. - let pixels_per_mm_x = zoomed_bounding_box_width / BED_GCC_SPIRIT.width; - let pixels_per_mm_y = zoomed_bounding_box_height / BED_GCC_SPIRIT.height; + let pixels_per_mm_x = zoomed_bounding_box_width / BED_GCC_SPIRIT.width(); + let pixels_per_mm_y = zoomed_bounding_box_height / BED_GCC_SPIRIT.height(); // We want to place a marker every 10mm to give the user a point of reference, so we need to work out how many pixels correspond to 10mm. let pixels_per_10_mm_x = pixels_per_mm_x * 10.0; diff --git a/seance/src/bed.rs b/seance/src/bed.rs index 3d97d80..92ac3f6 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -1,22 +1,15 @@ +use std::{ops::RangeInclusive, sync::LazyLock}; + use crate::paths::{PointInMillimeters, ResolvedPoint, MM_PER_PLOTTER_UNIT}; /// Dimensions and offset information for a given device's print bed. /// /// All measurements are in millimetres. pub struct PrintBed { - /// Minimum X position of the X axis. - pub x_min: f32, - /// Minimum Y position of the Y axis. - pub y_min: f32, - /// Maximum X position of the X axis. - pub x_max: f32, - /// Maximum Y position of the Y axis. - pub y_max: f32, - /// Width of the cutting area. - pub width: f32, - /// Height of the cutting area. - pub height: f32, - // TODO: are these values meaningfully different to x_max and y_max? + /// Value ranges of the X axis. + x_axis: RangeInclusive, + /// Value ranges of the Y axis. + y_axis: RangeInclusive, /// Whether to "mirror" the X axis. /// /// This might be desirable because, for example, the GCC Spirit has x=0 at the bottom. @@ -30,31 +23,77 @@ pub struct PrintBed { } /// Bed configuration for the [GCC Spirit Laser Engraver](https://www.gccworld.com/product/laser-engraver-supremacy/spirit). -pub const BED_GCC_SPIRIT: PrintBed = PrintBed { - // Actually -50.72 but the cutter refuses to move this far... - x_min: 0.0, - x_max: 901.52, - // Again, actually -4.80 but 🤷. - y_min: 0.0, - y_max: 463.20, - - width: 901.52, - height: 463.20, +pub static BED_GCC_SPIRIT: LazyLock = LazyLock::new(|| { + PrintBed::new( + ( + // actually -50.72 but the cutter refuses to move this far... + 0.0, 901.52, + ), + false, + ( + // Again, actually -4.80 but 🤷. + 0.0, 463.20, + ), + true, + ) +}); - mirror_x: false, - mirror_y: true, -}; +const VALID_MM_RANGE: RangeInclusive = + (i16::MIN as f32 * MM_PER_PLOTTER_UNIT)..=(i16::MAX as f32 * MM_PER_PLOTTER_UNIT); impl PrintBed { + /// Creates a new [`PrintBed`] specification. + /// + /// Truncates/clamps `x_axis` and `y_axis` to their HPGL-representable range. + /// + /// # Panics + /// When `x_axis` or `y_axis` aren't in order. + pub fn new( + mut x_axis: (f32, f32), + mirror_x: bool, + mut y_axis: (f32, f32), + mirror_y: bool, + ) -> Self { + #[inline] + fn clamp_mm_to_valid(val: &mut f32) { + if !VALID_MM_RANGE.contains(&val) { + let adjusted = val.clamp(*VALID_MM_RANGE.start(), *VALID_MM_RANGE.end()); + log::warn!( + "axis value {} would produce invalid HPGL values, truncating to {adjusted}", + val + ); + *val = adjusted + } + } + + assert!( + x_axis.0 <= x_axis.1, + "X axis values are the wrong way around" + ); + assert!( + y_axis.0 <= y_axis.1, + "y axis values are the wrong way around" + ); + + clamp_mm_to_valid(&mut x_axis.0); + clamp_mm_to_valid(&mut x_axis.1); + clamp_mm_to_valid(&mut y_axis.0); + clamp_mm_to_valid(&mut y_axis.1); + + Self { + x_axis: x_axis.0..=x_axis.1, + y_axis: y_axis.0..=y_axis.1, + mirror_x, + mirror_y, + } + } + /// Converts a [`PointInMillimeters`] into the same point in HPGL/2 units **for this printer**. /// /// Returns `None` if the point is out of the bed of this printer. /// /// # Arguments /// * `point`: The point to convert from mm. - /// - /// # Panics - /// Panics when the values of `self` would cause truncation at the origin. pub fn place_point(&self, point: PointInMillimeters) -> Option { #[inline] fn mm_to_hpgl(mut value: f32, mirror: Option) -> Option { @@ -63,30 +102,60 @@ impl PrintBed { } let adjusted = value / MM_PER_PLOTTER_UNIT; - if adjusted > i16::MAX as f32 || adjusted < i16::MIN as f32 { + if !((i16::MIN as f32)..=(i16::MAX as f32)).contains(&adjusted) { // value would be truncated + log::warn!( + "HPGL value {adjusted} from {value}mm is out of i16 range: {:?}", + (i16::MIN..=i16::MAX) + ); None } else { Some(adjusted.round() as i16) } } - // check printer bed sizes won't automatically cause truncation - // TODO: do this in constructor? - debug_assert!( - self.mirror_x && mm_to_hpgl(self.x_max, None).is_none(), - "x-axis mirroring is enabled but the axis is so large it would truncate" - ); - debug_assert!( - self.mirror_y && mm_to_hpgl(self.y_max, None).is_none(), - "y-axis mirroring is enabled but the axis is so large it would truncate" - ); + if !(self.x_axis.contains(&point.x)) { + log::warn!( + "x-axis value {}mm is outside of bed size {:?}", + point.x, + self.x_axis, + ); + return None; + } + if !(self.y_axis.contains(&point.y)) { + log::warn!( + "y-axis value {}mm is outside of bed size {:?}", + point.y, + self.y_axis, + ); + return None; + } Some(ResolvedPoint { - x: mm_to_hpgl(point.x, self.mirror_x.then_some(self.width))?, - y: mm_to_hpgl(point.y, self.mirror_y.then_some(self.height))?, + x: mm_to_hpgl(point.x, self.mirror_x.then_some(*self.x_axis.end()))?, + y: mm_to_hpgl(point.y, self.mirror_y.then_some(*self.y_axis.end()))?, }) } + + /// Gets the x axis value range of this print bed in millimetres. + pub fn x_axis(&self) -> &RangeInclusive { + &self.x_axis + } + + /// Gets the y axis value range of this print bed in millimetres. + pub fn y_axis(&self) -> &RangeInclusive { + &self.y_axis + } + + /// Gets the width of this print bed in millimetres. + pub fn width(&self) -> f32 { + self.x_axis.end() - self.x_axis.start() + } + + /// Gets the height of this print bed in millimetres. + pub fn height(&self) -> f32 { + self.y_axis.end() - self.y_axis.start() + } } #[cfg(test)] @@ -95,7 +164,7 @@ mod tests { #[test] fn test_mm_to_hpgl_units() { - let bed = BED_GCC_SPIRIT; + let bed = &BED_GCC_SPIRIT; assert_eq!( bed.place_point((10.0, 10.0).into()).unwrap(), @@ -119,9 +188,9 @@ mod tests { "f32::MAX mm" ); assert_eq!( - bed.place_point((819.175, 819.175).into()).unwrap(), - (32767, -14239).into(), - "approx maximum computable value" + bed.place_point((819.175, 462.0).into()).unwrap(), + (32767, 48).into(), + "bed maximum" ); assert!( bed.place_point((f32::MIN, f32::MIN).into()).is_none(), diff --git a/seance/tests/snapshot_tests.rs b/seance/tests/snapshot_tests.rs index ebeebb3..6f27a0b 100644 --- a/seance/tests/snapshot_tests.rs +++ b/seance/tests/snapshot_tests.rs @@ -13,7 +13,7 @@ fn hackspace_logo() { tool.set_enabled(true); } - let bed = seance::bed::BED_GCC_SPIRIT; + let bed = &seance::bed::BED_GCC_SPIRIT; let paths = seance::svg::get_paths_grouped_by_colour(&design_file); insta::assert_debug_snapshot!("york hackspace logo SVG paths", &paths); From 53cda578e8d3ff99af820140dca1f8caadf3cfad Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Mon, 7 Apr 2025 02:11:26 +0100 Subject: [PATCH 16/17] refactor(seance): more sanity checking for PrinterBed --- seance/src/bed.rs | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index 92ac3f6..d8579b6 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -16,9 +16,6 @@ pub struct PrintBed { /// Generally we want 0,0 to be in the top-left, so we would mirror the x axis in this case. pub mirror_x: bool, /// Whether to "mirror" the Y axis. - /// - /// This might be desirable because, for example, the GCC Spirit has x=0 at the bottom. - /// Generally we want 0,0 to be in the top-left, so we would mirror the x axis in this case. pub mirror_y: bool, } @@ -44,10 +41,12 @@ const VALID_MM_RANGE: RangeInclusive = impl PrintBed { /// Creates a new [`PrintBed`] specification. /// - /// Truncates/clamps `x_axis` and `y_axis` to their HPGL-representable range. + /// `x_axis` and `y_axis` are tuples of lower/upper limits of the bed in millimetres. + /// They will be clamped to their HPGL-representable range. /// /// # Panics - /// When `x_axis` or `y_axis` aren't in order. + /// - When `x_axis` or `y_axis` aren't in order. + /// - When `x_axis` or `y_axis` contain `Nan` or an infinity. pub fn new( mut x_axis: (f32, f32), mirror_x: bool, @@ -55,12 +54,13 @@ impl PrintBed { mirror_y: bool, ) -> Self { #[inline] - fn clamp_mm_to_valid(val: &mut f32) { + fn validate(val: &mut f32) { + assert!(val.is_finite(), "{val} is not a finite number"); + if !VALID_MM_RANGE.contains(&val) { let adjusted = val.clamp(*VALID_MM_RANGE.start(), *VALID_MM_RANGE.end()); log::warn!( - "axis value {} would produce invalid HPGL values, truncating to {adjusted}", - val + "axis value {val} would produce invalid HPGL values, truncating to {adjusted}", ); *val = adjusted } @@ -75,10 +75,10 @@ impl PrintBed { "y axis values are the wrong way around" ); - clamp_mm_to_valid(&mut x_axis.0); - clamp_mm_to_valid(&mut x_axis.1); - clamp_mm_to_valid(&mut y_axis.0); - clamp_mm_to_valid(&mut y_axis.1); + validate(&mut x_axis.0); + validate(&mut x_axis.1); + validate(&mut y_axis.0); + validate(&mut y_axis.1); Self { x_axis: x_axis.0..=x_axis.1, @@ -94,9 +94,13 @@ impl PrintBed { /// /// # Arguments /// * `point`: The point to convert from mm. + /// + /// # Panics + /// When `point` contains a non-finite number. pub fn place_point(&self, point: PointInMillimeters) -> Option { #[inline] fn mm_to_hpgl(mut value: f32, mirror: Option) -> Option { + // TODO: this isn't correct behaviour if self.x_axis.start() < 0 if let Some(max) = mirror { value = max - value; } @@ -114,6 +118,17 @@ impl PrintBed { } } + assert!( + point.x.is_finite(), + "point x value {} is not finite", + point.x + ); + assert!( + point.y.is_finite(), + "point y value {} is not finite", + point.y + ); + if !(self.x_axis.contains(&point.x)) { log::warn!( "x-axis value {}mm is outside of bed size {:?}", From 6781516e7c839934cba72248b37aac6203462847 Mon Sep 17 00:00:00 2001 From: Laura Demkowicz-Duffy Date: Sun, 13 Apr 2025 17:10:08 +0100 Subject: [PATCH 17/17] tweak(seance): log errors instead of warnings for invalid state --- seance/src/bed.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seance/src/bed.rs b/seance/src/bed.rs index d8579b6..dac7d45 100644 --- a/seance/src/bed.rs +++ b/seance/src/bed.rs @@ -108,7 +108,7 @@ impl PrintBed { let adjusted = value / MM_PER_PLOTTER_UNIT; if !((i16::MIN as f32)..=(i16::MAX as f32)).contains(&adjusted) { // value would be truncated - log::warn!( + log::error!( "HPGL value {adjusted} from {value}mm is out of i16 range: {:?}", (i16::MIN..=i16::MAX) ); @@ -130,7 +130,7 @@ impl PrintBed { ); if !(self.x_axis.contains(&point.x)) { - log::warn!( + log::error!( "x-axis value {}mm is outside of bed size {:?}", point.x, self.x_axis, @@ -138,7 +138,7 @@ impl PrintBed { return None; } if !(self.y_axis.contains(&point.y)) { - log::warn!( + log::error!( "y-axis value {}mm is outside of bed size {:?}", point.y, self.y_axis,