From d852d3cd645bfd206def31abfe1a1c8bcdf4402b Mon Sep 17 00:00:00 2001 From: DarkBiscuits Date: Thu, 12 Nov 2026 13:35:02 -0500 Subject: [PATCH 1/3] Add metric Mach4 post and decimal-sensitive dwell support --- README.md | 3 +- crates/cli/tests/post_e2e.rs | 13 +- crates/post/README.md | 28 +- crates/post/posts/mach4.json | 119 +++ crates/post/src/audit.rs | 15 +- crates/post/src/def.rs | 13 + crates/post/src/lib.rs | 3 + crates/post/src/read.rs | 33 +- crates/post/src/tests.rs | 3 +- crates/post/src/write.rs | 5 +- crates/post/tests/audit.rs | 10 + crates/post/tests/golden/curves.mach4.nc | 249 +++++ crates/post/tests/golden/dome.mach4.nc | 1241 ++++++++++++++++++++++ crates/post/tests/golden/sample.mach4.nc | 52 + crates/post/tests/mach4.rs | 267 +++++ 15 files changed, 2041 insertions(+), 13 deletions(-) create mode 100644 crates/post/posts/mach4.json create mode 100644 crates/post/tests/golden/curves.mach4.nc create mode 100644 crates/post/tests/golden/dome.mach4.nc create mode 100644 crates/post/tests/golden/sample.mach4.nc create mode 100644 crates/post/tests/mach4.rs diff --git a/README.md b/README.md index 702c20d..cd80c32 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ that post. The G-code is read back and compared to the toolpath at the job chord | `linuxcnc_router` | LinuxCNC router | - | Millimetre LinuxCNC gantry router. G38.2 tool-length probe, vacuum and dust as M64/M65. No rotary axis. | | `grbl` | Grbl | - | Millimetre dialect for Grbl. Absolute moves, IJK arcs and helices, no line numbers, tool changes as comments. No rotary axis. | | `mach3` | Mach3 | A, degrees per minute | Millimetre dialect for Mach3. Incremental IJK arcs and helices, dwell P in seconds, no line numbers. A axis with degrees-per-minute feed when the angle changes. | +| `mach4` | Mach4 | - | Metric three-axis mill/router. Explicit G91.1, IJK arcs and helices, decimal P dwell seconds, no line numbers or automatic tool-length compensation. | | `fanuc` | Fanuc | A, inverse time | Millimetre Fanuc mill dialect. Program O0001, sequence numbers, R arcs, helices as lines, dwell P in milliseconds. A axis with inverse-time feed (G93, cancelled by G94) when the angle changes. | | `haas` | Haas | A, inverse time | Millimetre Haas mill dialect. Program O00001, sequence numbers, IJK arcs and helices, dwell P in milliseconds. A axis with inverse-time feed (G93, cancelled by G94) when the angle changes. | | `plasma` | LinuxCNC plasma | - | Millimetre plasma. Torch on M3 S1 / off M5, pierce dwell from the path, optional THC codes, helices as lines. `process_cycle` `per_cut` keeps every process, including spindle, off during G0. No rotary axis. | @@ -133,7 +134,7 @@ placeholders, so the same program and post always produce the same bytes. Start - `modal_motion` — when true, a repeated `G0` or `G1` is left off the next line. - `arcs` — `mode` is `ijk` (centre offsets `I` `J` `K`), `r` (signed radius), or `lines` (chords within the chord tolerance). `max_sweep_deg` splits a longer arc (greater than 0, at most 360). `helix` writes a helix as arcs when true, and as chords when false. - `planes` — when true, the post can change plane. When false, an arc has to stay in the header plane. -- `dwell` — `word` (one letter) and `unit` (`s` or `ms`). +- `dwell` — `word` (one letter) and `unit` (`s` or `ms`). Optional `decimal_seconds: true` requires `P` and `s`: always writes decimal seconds and reads integer P as milliseconds (Mach4). See [Mach4 setup and dwell rules](crates/post/README.md#mach4). - `process` — map from a process name (`spindle`, `torch`, `laser`, `extruder`, `knife`, `other`) to `{ "on": [lines], "off": [lines] }`. Placeholders: `{speed}`, `{power}`, `{program_name}`, `{date-free!}`. - `tool_change` — lines for a tool change. Placeholder: `{tool}`. - `coolant` — `flood`, `mist`, and `off`, each a list of lines. diff --git a/crates/cli/tests/post_e2e.rs b/crates/cli/tests/post_e2e.rs index 055b966..7ecaf7c 100644 --- a/crates/cli/tests/post_e2e.rs +++ b/crates/cli/tests/post_e2e.rs @@ -59,7 +59,7 @@ fn first_diff(left: &str, right: &str) -> String { fn sample_job_posts_with_each_builtin() -> Result<(), String> { let root = workspace_root()?; let job = sample_job(&root)?; - for id in ["linuxcnc", "grbl"] { + for id in ["linuxcnc", "grbl", "mach4"] { let out = temp_path(&format!("campanion-post-e2e-{id}.nc")); let out_arg = path_arg(&out)?; run_ok(&["run", &job, "-o", &out_arg, "--post", id])?; @@ -81,6 +81,14 @@ fn sample_job_posts_with_each_builtin() -> Result<(), String> { return Err(format!("{id}: unexpected grbl program\n{posted}")); } } + "mach4" => { + if !posted.starts_with("(sample.job)\nG21\nG90\nG91.1\nG94\nG17\nG40\nG49\nG80\n") + || !posted.ends_with("M30\n") + || posted.lines().any(|line| line.starts_with('N')) + { + return Err(format!("{id}: unexpected Mach4 program\n{posted}")); + } + } _ => return Err(format!("untested builtin {id}")), } } @@ -159,6 +167,7 @@ fn posts_lists_every_builtin() -> Result<(), String> { "linuxcnc_5ax_tcp", "grbl", "mach3", + "mach4", "fanuc", "haas", "haas_umc", @@ -189,7 +198,7 @@ fn post_on_committed_ir_matches_run() -> Result<(), String> { // before it writes IR, so this posts the IR that same run just wrote. let root = workspace_root()?; let job = sample_job(&root)?; - for id in ["linuxcnc", "grbl"] { + for id in ["linuxcnc", "grbl", "mach4"] { let from_run_path = temp_path(&format!("campanion-post-match-run-{id}.nc")); let ir_path = temp_path(&format!("campanion-post-match-{id}.ir.json")); let from_post_path = temp_path(&format!("campanion-post-match-post-{id}.nc")); diff --git a/crates/post/README.md b/crates/post/README.md index 74a32da..72f7ca6 100644 --- a/crates/post/README.md +++ b/crates/post/README.md @@ -48,9 +48,35 @@ A Probe event with a contact pose and a feed is one `G38.2` block: the three axi `audit` treats the probe block as a non-motion and accepts `M64`/`M65` only when `digital_out` is set and `P` is one of the declared pins. `G53` handling is unchanged. This output has not been proven on a machine. +## Mach4 + +`mach4` is a metric three-axis mill/router post, based on the public +[Mach4 Mill Programming Guide v1.1, Build 3775](https://www.machsupport.com/wp-content/uploads/2014/05/Mill%20GCode%20Programming.pdf). +Its header declares `G21 G90 G91.1 G94 G17 G40 G49 G80` on separate blocks: millimetres, +absolute endpoints, incremental IJK centres, feed per minute, XY plane, and cancelled cutter +compensation, tool-length offsets and canned cycles. It writes full circles and helices in +XY/ZX/YZ, standard `T… M6`, `M3`/`M5`, `M7`/`M8`/`M9`, and ends with `M30`. +There are no line numbers, machine parking coordinates, rotary/five-axis or lathe support, +probing macros, automatic tool measurement or accessory mappings. + +The operator must establish the work offset and correct tool-tip Z for each tool with +length compensation cancelled (`G49`). `T… M6` uses the machine's configured tool-change +behavior; this post does not emit `G43 H…` or choose a tool-measurement macro. Review that +setup before use. Software read-back and audit do not establish controller or machine acceptance. + +Mach4 distinguishes literal dwell spelling (manual p. 17): `G4 P2` means 0.002 seconds, +while `G4 P2.0` means 2 seconds. Its dwell must be the only G code in the block. +`dwell: { "word": "P", "unit": "s", "decimal_seconds": true }` opts into these rules for +built-in or custom posts. The writer keeps a decimal point even with `trailing_zeros: false` +or `decimals.dwell: 0`; `read_back` returns the actual seconds for integer and decimal P. +The mill audit validates the literal-P block but does not report elapsed dwell time. +X/U dwell variants and variable expressions are outside this literal-P scope. +The option defaults to false, preserving existing `s` and `ms` posts. It requires `word: P` +and `unit: s`. Dimensional `decimal_point` does not control dwell formatting. + ## Modal audit -`read_back`, `audit` and `audit_five` accept `G91.1` as an explicit declaration of incremental `I`/`J`/`K` arc centres, including in a custom post header or on a motion block. It does not change the `G90` positioning mode. `G90.1` is rejected with a specific diagnostic because absolute arc centres are not supported. Built-in post headers are unchanged. +`read_back`, `audit` and `audit_five` accept `G91.1` as an explicit declaration of incremental `I`/`J`/`K` arc centres, including in a custom post header or on a motion block. It does not change the `G90` positioning mode. `G90.1` is rejected with a specific diagnostic because absolute arc centres are not supported. Existing built-in post headers are unchanged; `mach4` declares `G91.1` explicitly. `audit` accepts `G53` machine-coordinate moves, including retracts in custom tool-change templates. They are excluded from part-frame motions and preserve the part position, motion mode and plane, matching `read_back` and the existing five-axis audit. This replay does not establish a machine-to-work coordinate transform or verify the physical retract path. diff --git a/crates/post/posts/mach4.json b/crates/post/posts/mach4.json new file mode 100644 index 0000000..590e11e --- /dev/null +++ b/crates/post/posts/mach4.json @@ -0,0 +1,119 @@ +{ + "id": "mach4", + "name": "Mach4", + "description": "Metric three-axis Mach4 mill/router. Explicit incremental IJK, arcs and helices in all planes. Decimal P dwell seconds; integer P milliseconds. No rotary axes or automatic tool-length compensation.", + "units": "mm", + "line_numbers": { + "enabled": false, + "start": 10, + "step": 10, + "prefix": "N" + }, + "decimals": { + "linear": 4, + "feed": 3, + "dwell": 3 + }, + "trailing_zeros": false, + "header": [ + "({program_name})", + "G21", + "G90", + "G91.1", + "G94", + "G17", + "G40", + "G49", + "G80" + ], + "footer": [ + "M9" + ], + "modal_motion": true, + "arcs": { + "mode": "ijk", + "max_sweep_deg": 360, + "helix": true + }, + "planes": true, + "dwell": { + "word": "P", + "unit": "s", + "decimal_seconds": true + }, + "process": { + "spindle": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + }, + "torch": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + }, + "laser": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + }, + "extruder": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + }, + "knife": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + }, + "other": { + "on": [ + "M3 S{speed}" + ], + "off": [ + "M5" + ] + } + }, + "tool_change": [ + "T{tool} M6" + ], + "coolant": { + "flood": [ + "M8" + ], + "mist": [ + "M7" + ], + "off": [ + "M9" + ] + }, + "comments": { + "style": "paren" + }, + "program_end": [ + "M30" + ], + "max_line_length": 256, + "axis_letters": "XYZ", + "references": [ + "Newfangled Solutions, Mach4 Mill Programming Guide v1.1, Build 3775, pp. 15-17, 34-35, 40-42, 50-54, 58, 83-84. https://www.machsupport.com/wp-content/uploads/2014/05/Mill%20GCode%20Programming.pdf" + ] +} diff --git a/crates/post/src/audit.rs b/crates/post/src/audit.rs index 863a798..08fae52 100644 --- a/crates/post/src/audit.rs +++ b/crates/post/src/audit.rs @@ -544,6 +544,19 @@ impl<'a> Parser<'a> { self.length = LengthOffset::Active(h); } if saw_dwell { + if self.post.dwell.decimal_seconds { + if words.iter().filter(|word| word.letter == 'G').count() != 1 { + return Err(fail(line_no, "dwell must be the only G code in its block")); + } + // P was checked for finiteness, sign and duplication above. The audit + // validates the block; read_back reports its decimal-sensitive duration. + if p_value.is_none() { + return Err(fail(line_no, "dwell is missing its value")); + } + if i.is_some() || j.is_some() || k.is_some() || r.is_some() { + return Err(fail(line_no, "literal-P dwell does not accept arc or cycle words")); + } + } if block_motion.is_some() || x.is_some() || y.is_some() || z.is_some() || saw_rotary { return Err(fail(line_no, "dwell cannot share a block with motion")); } @@ -1161,7 +1174,7 @@ mod tests { let path = ClPath::try_new(Some("outer".into()), Some(1), CompensationMode::Off, feeds, None, Vec::new(), blocks).expect("path"); let program = Program::try_new(Some("plate".into()), vec![tool], None, vec![path], Vec::new(), Vec::new(), None).expect("program"); let options = PostOptions { arc_tolerance: 0.05 }; - for id in ["linuxcnc", "linuxcnc_router", "grbl", "mach3", "fanuc", "haas", "plasma", "grbl_laser"] { + for id in ["linuxcnc", "linuxcnc_router", "grbl", "mach3", "mach4", "fanuc", "haas", "plasma", "grbl_laser"] { let post = builtin(id).expect(id); let text = post_program(&program, &post, &options).unwrap_or_else(|err| panic!("{id}: {err}")); assert!(!text.contains("-0"), "{id}: {text}"); diff --git a/crates/post/src/def.rs b/crates/post/src/def.rs index 0a7caf2..f4a91d1 100644 --- a/crates/post/src/def.rs +++ b/crates/post/src/def.rs @@ -191,6 +191,16 @@ pub(crate) struct Arcs { pub(crate) struct Dwell { pub(crate) word: String, pub(crate) unit: String, + /// Literal P: a decimal point means seconds, otherwise milliseconds (Mach4). + /// Writing always retains a decimal point. Opt-in; requires word=P and unit=s. + #[serde(default)] + pub(crate) decimal_seconds: bool, +} + +impl Dwell { + pub(crate) fn seconds(&self, value: f64, has_decimal: bool) -> f64 { + if self.unit == "ms" || (self.decimal_seconds && !has_decimal) { value / 1000.0 } else { value } + } } #[derive(Clone, Debug, PartialEq, Deserialize)] @@ -757,6 +767,9 @@ impl PostDef { if !matches!(self.dwell.unit.as_str(), "s" | "ms") { return Err(field("dwell.unit", "must be s or ms")); } + if self.dwell.decimal_seconds && (self.dwell.unit != "s" || self.dwell.word != "P") { + return Err(field("dwell.decimal_seconds", "requires word P and unit s")); + } check_power_scale(self.power_scale)?; if !matches!(self.z_mode.as_str(), "keep" | "drop") { return Err(field("z_mode", "must be keep or drop")); diff --git a/crates/post/src/lib.rs b/crates/post/src/lib.rs index 120ec77..a965f0b 100644 --- a/crates/post/src/lib.rs +++ b/crates/post/src/lib.rs @@ -58,6 +58,7 @@ const LINUXCNC_5AX_JSON: &str = include_str!("../posts/linuxcnc_5ax.json"); const LINUXCNC_5AX_TCP_JSON: &str = include_str!("../posts/linuxcnc_5ax_tcp.json"); const GRBL_JSON: &str = include_str!("../posts/grbl.json"); const MACH3_JSON: &str = include_str!("../posts/mach3.json"); +const MACH4_JSON: &str = include_str!("../posts/mach4.json"); const FANUC_JSON: &str = include_str!("../posts/fanuc.json"); const HAAS_JSON: &str = include_str!("../posts/haas.json"); const HAAS_UMC_JSON: &str = include_str!("../posts/haas_umc.json"); @@ -79,6 +80,7 @@ pub fn builtin_ids() -> &'static [&'static str] { "linuxcnc_5ax_tcp", "grbl", "mach3", + "mach4", "fanuc", "haas", "haas_umc", @@ -102,6 +104,7 @@ pub fn builtin(id: &str) -> Option { "linuxcnc_5ax_tcp" => LINUXCNC_5AX_TCP_JSON, "grbl" => GRBL_JSON, "mach3" => MACH3_JSON, + "mach4" => MACH4_JSON, "fanuc" => FANUC_JSON, "haas" => HAAS_JSON, "haas_umc" => HAAS_UMC_JSON, diff --git a/crates/post/src/read.rs b/crates/post/src/read.rs index ac4b51e..31ceb65 100644 --- a/crates/post/src/read.rs +++ b/crates/post/src/read.rs @@ -174,14 +174,15 @@ impl<'a> Parser<'a> { if stripped.trim() == "%" { return Ok(None); } - let mut words = tokenize(&stripped)?; + let dwell_letter = self.post.dwell.word.chars().next().unwrap_or('P'); + let (mut words, dwell_decimal) = tokenize_with_decimal(&stripped, dwell_letter)?; if self.post.line_numbers.enabled { strip_line_number(&mut words, self.post.line_numbers.prefix.chars().next().unwrap_or('N'))?; } if words.is_empty() { return Ok(None); } - self.interpret(words, stripped.trim()) + self.interpret(words, stripped.trim(), dwell_decimal) } } @@ -214,7 +215,7 @@ impl<'a> Parser<'a> { self.post.probe_number().is_some_and(|code| (value - code).abs() < 1e-6) } - fn interpret(&mut self, mut words: Vec<(char, f64)>, raw: &str) -> Result> { + fn interpret(&mut self, mut words: Vec<(char, f64)>, raw: &str, dwell_decimal: bool) -> Result> { self.note_sub_line(raw); if let Some((letter, value)) = words.first().copied() && letter == 'O' @@ -255,6 +256,15 @@ impl<'a> Parser<'a> { letter => return Err(invalid(format!("unexpected word {letter}"))), } } + if self.post.dwell.decimal_seconds && g_codes.contains(&4) { + // Check before non-motion shortcuts (e.g. G53), including decimal G91.1. + if words.iter().filter(|(letter, _)| *letter == 'G').count() != 1 { + return Err(invalid("dwell must be the only G code in its block")); + } + if words.iter().any(|(letter, _)| matches!(letter, 'I' | 'J' | 'K' | 'R' | 'Q')) { + return Err(invalid("literal-P dwell does not accept arc or cycle words")); + } + } // Resolve feed mode before scaling words, regardless of their order in the block. if self.post.is_inch() { let inverse = if g_codes.contains(&93) { @@ -420,7 +430,7 @@ impl<'a> Parser<'a> { return Err(invalid("dwell cannot share a block with motion")); } let word = unique(&words, dwell_letter)?.ok_or_else(|| invalid("dwell is missing its value"))?; - let seconds = if self.post.dwell.unit == "ms" { word / 1000.0 } else { word }; + let seconds = self.post.dwell.seconds(word, dwell_decimal); if !(seconds.is_finite() && seconds >= 0.0) { return Err(invalid("dwell must be >= 0")); } @@ -906,6 +916,12 @@ pub(crate) fn strip_comments(line: &str) -> String { } pub(crate) fn tokenize(line: &str) -> Result> { + tokenize_with_decimal(line, '\0').map(|(words, _)| words) +} + +// Preserve the requested word's decimal spelling without changing other numeric semantics. +fn tokenize_with_decimal(line: &str, decimal_word: char) -> Result<(Vec<(char, f64)>, bool)> { + let mut has_decimal = false; let bytes = line.as_bytes(); let mut index = 0; let mut words = Vec::new(); @@ -925,10 +941,15 @@ pub(crate) fn tokenize(line: &str) -> Result> { return Err(invalid("unexpected character in g-code")); } index += 1; + let start = index; let value = take_number(line, bytes, &mut index)?; - words.push(((letter as char).to_ascii_uppercase(), value)); + let letter = (letter as char).to_ascii_uppercase(); + if letter == decimal_word { + has_decimal |= line.get(start..index).is_some_and(|raw| raw.contains('.')); + } + words.push((letter, value)); } - Ok(words) + Ok((words, has_decimal)) } fn take_number(line: &str, bytes: &[u8], index: &mut usize) -> Result { diff --git a/crates/post/src/tests.rs b/crates/post/src/tests.rs index 114df89..10d4b3a 100644 --- a/crates/post/src/tests.rs +++ b/crates/post/src/tests.rs @@ -140,6 +140,7 @@ fn builtins_load_and_match_ids() { "linuxcnc_5ax_tcp", "grbl", "mach3", + "mach4", "fanuc", "haas", "haas_umc", @@ -1644,7 +1645,7 @@ fn a_positive_feed_that_rounds_to_zero_is_rejected() { ClBlock::Motion(ClMotion::Linear { to: pose(1.0, 0.0, 1.0), feed: 0.00004, role: MoveRole::Cut }), ]; let program = program(blocks); - for id in ["linuxcnc", "linuxcnc_router", "grbl", "mach3", "fanuc", "haas", "plasma", "grbl_laser"] { + for id in ["linuxcnc", "linuxcnc_router", "grbl", "mach3", "mach4", "fanuc", "haas", "plasma", "grbl_laser"] { let post = builtin(id).expect(id); let err = post_program(&program, &post, &options()).expect_err(id); assert!(err.to_string().contains("feed rounds to zero"), "{id}: {err}"); diff --git a/crates/post/src/write.rs b/crates/post/src/write.rs index 480bac3..b52a4b1 100644 --- a/crates/post/src/write.rs +++ b/crates/post/src/write.rs @@ -3137,7 +3137,10 @@ impl<'a> Writer<'a> { return Err(invalid("dwell must be >= 0")); } let value = if self.post.dwell.unit == "ms" { seconds * 1000.0 } else { seconds }; - let text = format_fixed(value, self.post.decimals.dwell, self.post.trailing_zeros)?; + let mut text = format_fixed(value, self.post.decimals.dwell, self.post.trailing_zeros)?; + if self.post.dwell.decimal_seconds && !text.contains('.') { + text.push_str(".0"); + } self.emit_line(&format!("G4 {}{text}", self.post.dwell.word)) } diff --git a/crates/post/tests/audit.rs b/crates/post/tests/audit.rs index bf9e471..8cb93f4 100644 --- a/crates/post/tests/audit.rs +++ b/crates/post/tests/audit.rs @@ -68,6 +68,16 @@ const MILLS: &[Mill] = &[ decimal_point: false, length: LengthKind::Cancelled, }, + Mill { + id: "mach4", + per_cut: false, + laser: false, + drop_z: false, + rapid_z: true, + numbered: false, + decimal_point: false, + length: LengthKind::Cancelled, + }, Mill { id: "fanuc", per_cut: false, laser: false, drop_z: false, rapid_z: true, numbered: true, decimal_point: true, length: LengthKind::Active }, Mill { id: "haas", per_cut: false, laser: false, drop_z: false, rapid_z: true, numbered: true, decimal_point: true, length: LengthKind::Active }, Mill { diff --git a/crates/post/tests/golden/curves.mach4.nc b/crates/post/tests/golden/curves.mach4.nc new file mode 100644 index 0000000..190a96e --- /dev/null +++ b/crates/post/tests/golden/curves.mach4.nc @@ -0,0 +1,249 @@ +(curves.job) +G21 +G90 +G91.1 +G94 +G17 +G40 +G49 +G80 +(toolpath outer) +T1 M6 +G0 X50.3618 Y68.4519 Z5 +X50.3618 Y68.4519 Z2 +M3 S12000 +G1 X50.3618 Y68.4519 Z-3 F300 +G2 X69.6382 Y68.4519 Z-3 I9.6382 J-84.4757 F1000 +G2 X78.6048 Y66.8747 Z-3 I-8.5316 J-74.7769 +G2 X87.0295 Y64.2745 Z-3 I-15.8472 J-66.2932 +G2 X94.005 Y60.9967 Z-3 I-18.2668 J-47.9348 +G2 X97.2726 Y58.9591 Z-3 I-21.9935 J-38.9093 +G2 X100.0704 Y56.8379 Z-3 I-20.6613 J-30.1578 +G2 X102.6833 Y54.3977 Z-3 I-21.0632 J-25.1732 +G2 X104.7879 Y51.9178 Z-3 I-19.1273 J-18.3654 +G2 X106.6021 Y49.1013 Z-3 I-19.1495 J-14.3274 +G2 X107.8865 Y46.2611 Z-3 I-17.6617 J-9.6978 +G2 X108.7218 Y43.1447 Z-3 I-17.7809 J-6.4359 +G2 X109 Y40 Z-3 I-17.636 J-3.1447 +G2 X108.7546 Y33.6818 Z-3 I-81.4483 J0 +G2 X108.1073 Y28.5948 Z-3 I-52.2829 J4.068 +G2 X106.9698 Y23.8161 Z-3 I-41.5679 J7.37 +G2 X105.5053 Y19.9736 Z-3 I-27.2891 J8.1999 +G2 X103.5608 Y16.5606 Z-3 I-21.8543 J10.191 +G2 X101.2799 Y13.8108 Z-3 I-15.8761 J10.8479 +G2 X98.7066 Y11.6719 Z-3 I-12.8989 J12.9013 +G2 X95.7559 Y10.0164 Z-3 I-10.6121 J15.4575 +G2 X92.8796 Y8.9822 Z-3 I-8.2518 J18.4318 +G2 X89.5703 Y8.2998 Z-3 I-6.8104 J24.6621 +G2 X86.6371 Y8.0415 Z-3 I-4.0766 J29.5102 +G2 X83.1784 Y8.0495 Z-3 I-1.6303 J42.4439 +G2 X80.3567 Y8.2488 Z-3 I2.219 J51.5011 +G2 X76.8131 Y8.6732 Z-3 I8.2151 J83.6087 +G2 X74.7216 Y8.991 Z-3 I14.2308 J100.6724 +G2 X70.6829 Y9.6697 Z-3 I62.5445 J384.5442 +G3 X66.6997 Y10.3366 Z-3 I-58.9725 J-339.9881 +G3 X64.8963 Y10.6046 Z-3 I-11.5698 J-71.6738 +G3 X61.9417 Y10.9227 Z-3 I-7.2245 J-53.2174 +G3 X60 Y11 Z-3 I-1.9417 J-24.345 +G2 X50.3618 Y11.5481 Z-3 I0 J85.0238 +G2 X41.3952 Y13.1253 Z-3 I8.5316 J74.7769 +G2 X32.9705 Y15.7255 Z-3 I15.8472 J66.2932 +G2 X25.995 Y19.0033 Z-3 I18.2668 J47.9348 +G2 X22.7274 Y21.0409 Z-3 I21.9935 J38.9093 +G2 X19.9296 Y23.1621 Z-3 I20.6613 J30.1578 +G2 X17.3167 Y25.6023 Z-3 I21.0632 J25.1732 +G2 X15.2121 Y28.0822 Z-3 I19.1273 J18.3654 +G2 X13.3979 Y30.8987 Z-3 I19.1495 J14.3274 +G2 X12.1135 Y33.7389 Z-3 I17.6617 J9.6978 +G2 X11.2782 Y36.8553 Z-3 I17.7809 J6.4359 +G2 X11.2782 Y43.1447 Z-3 I17.636 J3.1447 +G2 X12.1135 Y46.2611 Z-3 I18.6162 J-3.3195 +G2 X13.3979 Y49.1013 Z-3 I18.9461 J-6.8576 +G2 X15.2121 Y51.9178 Z-3 I20.9637 J-11.5109 +G2 X17.3167 Y54.3977 Z-3 I21.2319 J-15.8855 +G2 X19.9296 Y56.8379 Z-3 I23.6761 J-22.733 +G2 X22.7274 Y58.9591 Z-3 I23.4591 J-28.0366 +G2 X25.995 Y60.9967 Z-3 I25.2611 J-36.8717 +G2 X32.9705 Y64.2745 Z-3 I25.2423 J-44.657 +G2 X41.3952 Y66.8747 Z-3 I24.2719 J-63.693 +G2 X50.3618 Y68.4519 Z-3 I17.4982 J-73.1997 +M5 +G0 X50.3618 Y68.4519 Z5 +(toolpath pocket) +X61.2715 Y44.0367 Z5 +X61.2715 Y44.0367 Z2 +M3 S12000 +G1 X61.2715 Y44.0367 Z-1.5 F300 +G2 X58.7285 Y44.0367 Z-1.5 I-1.2715 J22.0054 F1000 +G2 X56.2211 Y44.2144 Z-1.5 I5.5685 J96.3697 +G3 X53.5444 Y44.3974 Z-1.5 I-7.2461 J-86.309 +G3 X52.4079 Y44.4207 Z-1.5 I-0.928 J-17.5651 +G3 X50.6382 Y44.2709 Z-1.5 I0.1452 J-12.2456 +G3 X49.7591 Y44.0356 Z-1.5 I0.6817 J-4.3065 +G3 X49.3451 Y43.8404 Z-1.5 I1.1018 J-2.8738 +G3 X49.071 Y43.6542 Z-1.5 I0.9981 J-1.7639 +G3 X48.83 Y43.4206 Z-1.5 I1.1602 J-1.4387 +G3 X48.6044 Y43.1064 Z-1.5 I1.6739 J-1.4397 +G3 X48.4002 Y42.6848 Z-1.5 I2.4059 J-1.4255 +G3 X48.1936 Y42.0064 Z-1.5 I4.6305 J-1.781 +G3 X48.0595 Y41.1865 Z-1.5 I6.5385 J-1.4905 +G3 X48 Y40 Z-1.5 I11.807 J-1.1865 +G3 X48.1621 Y37.6806 Z-1.5 I16.674 J0 +G3 X48.4924 Y36.3061 Z-1.5 I7.2832 J1.0231 +G3 X49.1081 Y35.0763 Z-1.5 I4.9346 J1.7016 +G3 X49.9206 Y34.1797 Z-1.5 I3.5905 J2.437 +G3 X50.4866 Y33.7694 Z-1.5 I3.3115 J3.9738 +G3 X51.2548 Y33.3506 Z-1.5 I3.848 J6.1446 +G3 X52.1339 Y32.9942 Z-1.5 I3.7702 J8.0349 +G3 X53.3105 Y32.6475 Z-1.5 I4.4134 J12.8095 +G3 X55.901 Y32.2027 Z-1.5 I4.4132 J17.9378 +G3 X60 Y32 Z-1.5 I4.099 J41.339 +G3 X62.6105 Y32.1669 Z-1.5 I0 J20.4954 +G3 X64.9315 Y32.6293 Z-1.5 I-2.1894 J17.0491 +G3 X67.1313 Y33.4154 Z-1.5 I-3.9474 J14.5163 +G3 X68.81 Y34.3537 Z-1.5 I-4.4338 J9.9033 +G3 X70.2552 Y35.5825 Z-1.5 I-5.0386 J7.39 +G3 X71.1802 Y36.823 Z-1.5 I-4.719 J4.4841 +G3 X71.7807 Y38.2905 Z-1.5 I-5.175 J2.9741 +G3 X72 Y40 Z-1.5 I-6.5532 J1.7095 +G3 X71.9405 Y41.1865 Z-1.5 I-11.8665 J0 +G3 X71.8064 Y42.0064 Z-1.5 I-6.6726 J-0.6706 +G3 X71.5998 Y42.6848 Z-1.5 I-4.8371 J-1.1026 +G3 X71.3956 Y43.1064 Z-1.5 I-2.6101 J-1.0039 +G3 X71.17 Y43.4206 Z-1.5 I-1.8995 J-1.1255 +G3 X70.929 Y43.6542 Z-1.5 I-1.4012 J-1.2051 +G3 X70.6549 Y43.8404 Z-1.5 I-1.2722 J-1.5777 +G3 X70.2409 Y44.0356 Z-1.5 I-1.5158 J-2.6786 +G3 X69.3618 Y44.2709 Z-1.5 I-1.5608 J-4.0712 +G3 X67.5921 Y44.4207 Z-1.5 I-1.9149 J-12.0958 +G3 X66.4556 Y44.3974 Z-1.5 I-0.2085 J-17.5884 +G3 X63.7789 Y44.2144 Z-1.5 I4.5694 J-86.492 +G2 X61.2715 Y44.0367 Z-1.5 I-8.0759 J96.192 +G1 X51.1116 Y41.3085 Z-1.5 +G3 X51.0444 Y40.8865 Z-1.5 I3.6205 J-0.7926 +G3 X51 Y40 Z-1.5 I8.8221 J-0.8865 +G3 X51.1329 Y38.0979 Z-1.5 I13.674 J0 +G3 X51.3285 Y37.2841 Z-1.5 I4.3124 J0.6058 +G3 X51.5903 Y36.7611 Z-1.5 I2.0985 J0.7236 +G3 X51.8411 Y36.4843 Z-1.5 I1.1083 J0.7522 +G3 X52.0789 Y36.312 Z-1.5 I1.391 J1.6692 +G3 X52.5292 Y36.0665 Z-1.5 I2.2557 J3.602 +G3 X53.1111 Y35.8305 Z-1.5 I2.4958 J5.319 +G3 X54.0272 Y35.5606 Z-1.5 I3.4362 J9.9732 +G3 X56.197 Y35.1881 Z-1.5 I3.6965 J15.0247 +G3 X60 Y35 Z-1.5 I3.803 J38.3536 +G3 X62.2284 Y35.1425 Z-1.5 I0 J17.4954 +G3 X64.1443 Y35.5241 Z-1.5 I-1.8073 J14.0735 +G3 X65.9054 Y36.1535 Z-1.5 I-3.1602 J11.6215 +G3 X67.12 Y36.8324 Z-1.5 I-3.2079 J7.1652 +G3 X68.0805 Y37.6491 Z-1.5 I-3.3486 J4.9113 +G3 X68.5792 Y38.3178 Z-1.5 I-2.5443 J2.4175 +G3 X68.8778 Y39.0477 Z-1.5 I-2.574 J1.4793 +G3 X69 Y40 Z-1.5 I-3.6503 J0.9523 +G3 X68.9556 Y40.8865 Z-1.5 I-8.8665 J0 +G3 X68.8884 Y41.3085 Z-1.5 I-3.6877 J-0.3706 +G3 X67.5566 Y41.4209 Z-1.5 I-1.4415 J-9.1334 +G3 X66.6139 Y41.4016 Z-1.5 I-0.173 J-14.5886 +G3 X64.0299 Y41.2249 Z-1.5 I4.4111 J-83.4962 +G2 X61.4446 Y41.0417 Z-1.5 I-8.3269 J99.1815 +G2 X58.5554 Y41.0417 Z-1.5 I-1.4446 J25.0004 +G2 X55.9701 Y41.2249 Z-1.5 I5.7416 J99.3647 +G3 X53.3861 Y41.4016 Z-1.5 I-6.9951 J-83.3195 +G3 X52.4434 Y41.4209 Z-1.5 I-0.7697 J-14.5693 +G3 X51.1116 Y41.3085 Z-1.5 I0.1097 J-9.2458 +G1 X56.4957 Y38.1732 Z-1.5 +G3 X60 Y38 Z-1.5 I3.5043 J35.3685 +G2 X58.3823 Y38.0467 Z-1.5 I0 J28.0421 +G2 X56.4957 Y38.1732 Z-1.5 I5.9147 J102.3597 +M5 +G0 X56.4957 Y38.1732 Z5 +(toolpath pocket) +X61.2715 Y44.0367 Z5 +X61.2715 Y44.0367 Z2 +M3 S12000 +G1 X61.2715 Y44.0367 Z-3 F300 +G2 X58.7285 Y44.0367 Z-3 I-1.2715 J22.0054 F1000 +G2 X56.2211 Y44.2144 Z-3 I5.5685 J96.3697 +G3 X53.5444 Y44.3974 Z-3 I-7.2461 J-86.309 +G3 X52.4079 Y44.4207 Z-3 I-0.928 J-17.5651 +G3 X50.6382 Y44.2709 Z-3 I0.1452 J-12.2456 +G3 X49.7591 Y44.0356 Z-3 I0.6817 J-4.3065 +G3 X49.3451 Y43.8404 Z-3 I1.1018 J-2.8738 +G3 X49.071 Y43.6542 Z-3 I0.9981 J-1.7639 +G3 X48.83 Y43.4206 Z-3 I1.1602 J-1.4387 +G3 X48.6044 Y43.1064 Z-3 I1.6739 J-1.4397 +G3 X48.4002 Y42.6848 Z-3 I2.4059 J-1.4255 +G3 X48.1936 Y42.0064 Z-3 I4.6305 J-1.781 +G3 X48.0595 Y41.1865 Z-3 I6.5385 J-1.4905 +G3 X48 Y40 Z-3 I11.807 J-1.1865 +G3 X48.1621 Y37.6806 Z-3 I16.674 J0 +G3 X48.4924 Y36.3061 Z-3 I7.2832 J1.0231 +G3 X49.1081 Y35.0763 Z-3 I4.9346 J1.7016 +G3 X49.9206 Y34.1797 Z-3 I3.5905 J2.437 +G3 X50.4866 Y33.7694 Z-3 I3.3115 J3.9738 +G3 X51.2548 Y33.3506 Z-3 I3.848 J6.1446 +G3 X52.1339 Y32.9942 Z-3 I3.7702 J8.0349 +G3 X53.3105 Y32.6475 Z-3 I4.4134 J12.8095 +G3 X55.901 Y32.2027 Z-3 I4.4132 J17.9378 +G3 X60 Y32 Z-3 I4.099 J41.339 +G3 X62.6105 Y32.1669 Z-3 I0 J20.4954 +G3 X64.9315 Y32.6293 Z-3 I-2.1894 J17.0491 +G3 X67.1313 Y33.4154 Z-3 I-3.9474 J14.5163 +G3 X68.81 Y34.3537 Z-3 I-4.4338 J9.9033 +G3 X70.2552 Y35.5825 Z-3 I-5.0386 J7.39 +G3 X71.1802 Y36.823 Z-3 I-4.719 J4.4841 +G3 X71.7807 Y38.2905 Z-3 I-5.175 J2.9741 +G3 X72 Y40 Z-3 I-6.5532 J1.7095 +G3 X71.9405 Y41.1865 Z-3 I-11.8665 J0 +G3 X71.8064 Y42.0064 Z-3 I-6.6726 J-0.6706 +G3 X71.5998 Y42.6848 Z-3 I-4.8371 J-1.1026 +G3 X71.3956 Y43.1064 Z-3 I-2.6101 J-1.0039 +G3 X71.17 Y43.4206 Z-3 I-1.8995 J-1.1255 +G3 X70.929 Y43.6542 Z-3 I-1.4012 J-1.2051 +G3 X70.6549 Y43.8404 Z-3 I-1.2722 J-1.5777 +G3 X70.2409 Y44.0356 Z-3 I-1.5158 J-2.6786 +G3 X69.3618 Y44.2709 Z-3 I-1.5608 J-4.0712 +G3 X67.5921 Y44.4207 Z-3 I-1.9149 J-12.0958 +G3 X66.4556 Y44.3974 Z-3 I-0.2085 J-17.5884 +G3 X63.7789 Y44.2144 Z-3 I4.5694 J-86.492 +G2 X61.2715 Y44.0367 Z-3 I-8.0759 J96.192 +G1 X51.1116 Y41.3085 Z-3 +G3 X51.0444 Y40.8865 Z-3 I3.6205 J-0.7926 +G3 X51 Y40 Z-3 I8.8221 J-0.8865 +G3 X51.1329 Y38.0979 Z-3 I13.674 J0 +G3 X51.3285 Y37.2841 Z-3 I4.3124 J0.6058 +G3 X51.5903 Y36.7611 Z-3 I2.0985 J0.7236 +G3 X51.8411 Y36.4843 Z-3 I1.1083 J0.7522 +G3 X52.0789 Y36.312 Z-3 I1.391 J1.6692 +G3 X52.5292 Y36.0665 Z-3 I2.2557 J3.602 +G3 X53.1111 Y35.8305 Z-3 I2.4958 J5.319 +G3 X54.0272 Y35.5606 Z-3 I3.4362 J9.9732 +G3 X56.197 Y35.1881 Z-3 I3.6965 J15.0247 +G3 X60 Y35 Z-3 I3.803 J38.3536 +G3 X62.2284 Y35.1425 Z-3 I0 J17.4954 +G3 X64.1443 Y35.5241 Z-3 I-1.8073 J14.0735 +G3 X65.9054 Y36.1535 Z-3 I-3.1602 J11.6215 +G3 X67.12 Y36.8324 Z-3 I-3.2079 J7.1652 +G3 X68.0805 Y37.6491 Z-3 I-3.3486 J4.9113 +G3 X68.5792 Y38.3178 Z-3 I-2.5443 J2.4175 +G3 X68.8778 Y39.0477 Z-3 I-2.574 J1.4793 +G3 X69 Y40 Z-3 I-3.6503 J0.9523 +G3 X68.9556 Y40.8865 Z-3 I-8.8665 J0 +G3 X68.8884 Y41.3085 Z-3 I-3.6877 J-0.3706 +G3 X67.5566 Y41.4209 Z-3 I-1.4415 J-9.1334 +G3 X66.6139 Y41.4016 Z-3 I-0.173 J-14.5886 +G3 X64.0299 Y41.2249 Z-3 I4.4111 J-83.4962 +G2 X61.4446 Y41.0417 Z-3 I-8.3269 J99.1815 +G2 X58.5554 Y41.0417 Z-3 I-1.4446 J25.0004 +G2 X55.9701 Y41.2249 Z-3 I5.7416 J99.3647 +G3 X53.3861 Y41.4016 Z-3 I-6.9951 J-83.3195 +G3 X52.4434 Y41.4209 Z-3 I-0.7697 J-14.5693 +G3 X51.1116 Y41.3085 Z-3 I0.1097 J-9.2458 +G1 X56.4957 Y38.1732 Z-3 +G3 X60 Y38 Z-3 I3.5043 J35.3685 +G2 X58.3823 Y38.0467 Z-3 I0 J28.0421 +G2 X56.4957 Y38.1732 Z-3 I5.9147 J102.3597 +M5 +G0 X56.4957 Y38.1732 Z5 +M9 +M30 diff --git a/crates/post/tests/golden/dome.mach4.nc b/crates/post/tests/golden/dome.mach4.nc new file mode 100644 index 0000000..c231da8 --- /dev/null +++ b/crates/post/tests/golden/dome.mach4.nc @@ -0,0 +1,1241 @@ +(dome.job) +G21 +G90 +G91.1 +G94 +G17 +G40 +G49 +G80 +(toolpath dome) +T1 M6 +G0 X-20 Y-20 Z10 +M3 S12000 +G1 X-20 Y-20 Z0 F300 +X20 Y-20 Z0 F1000 +X20 Y-19.5 Z0 +X20 Y-19 Z0 +X-20 Y-19 Z0 +X-20 Y-18.5 Z0 +X-20 Y-18 Z0 +X20 Y-18 Z0 +X20 Y-17.5 Z0 +X20 Y-17 Z0 +X-20 Y-17 Z0 +X-20 Y-16.5 Z0 +X-20 Y-16 Z0 +X-1.125 Y-16 Z0 +X-0.25 Y-16 Z0.1885 +X0 Y-16 Z0.2143 +X0.25 Y-16 Z0.1885 +X1.125 Y-16 Z0 +X20 Y-16 Z0 +X20 Y-15.5 Z0 +X20 Y-15 Z0 +X5.8125 Y-15 Z0 +X5.75 Y-15 Z0.0166 +X5.5 Y-15 Z0.1813 +X5 Y-15 Z0.4949 +X4.5 Y-15 Z0.7437 +X3.5 Y-15 Z1.1981 +X3 Y-15 Z1.4086 +X2.75 Y-15 Z1.4685 +X0.25 Y-15 Z1.8522 +X0 Y-15 Z1.8715 +X-0.25 Y-15 Z1.8522 +X-2.75 Y-15 Z1.4685 +X-3 Y-15 Z1.4086 +X-3.5 Y-15 Z1.1981 +X-4.5 Y-15 Z0.7437 +X-5 Y-15 Z0.4949 +X-5.5 Y-15 Z0.1813 +X-5.75 Y-15 Z0.0166 +X-5.8125 Y-15 Z0 +X-20 Y-15 Z0 +X-20 Y-14.5 Z0 +X-20 Y-14 Z0 +X-7.8125 Y-14 Z0.002 +X-7.5 Y-14 Z0.3362 +X-7.25 Y-14 Z0.5709 +X-6 Y-14 Z1.5019 +X-5.75 Y-14 Z1.6609 +X-5.5 Y-14 Z1.7873 +X-3.5 Y-14 Z2.6957 +X-3 Y-14 Z2.8978 +X-2.75 Y-14 Z2.9696 +X-0.5 Y-14 Z3.2307 +X0 Y-14 Z3.2741 +X0.5 Y-14 Z3.2307 +X2.75 Y-14 Z2.9696 +X3 Y-14 Z2.8978 +X3.5 Y-14 Z2.6957 +X5.5 Y-14 Z1.7873 +X5.75 Y-14 Z1.6609 +X6 Y-14 Z1.5019 +X7.25 Y-14 Z0.5709 +X7.5 Y-14 Z0.3362 +X7.8125 Y-14 Z0.002 +X20 Y-14 Z0 +X20 Y-13.5 Z0 +X20 Y-13 Z0 +X9.4375 Y-13 Z0 +X9.375 Y-13 Z0.0772 +X9.25 Y-13 Z0.2572 +X9.125 Y-13 Z0.4307 +X9 Y-13 Z0.5813 +X8.75 Y-13 Z0.8263 +X8.5 Y-13 Z1.0361 +X6.5 Y-13 Z2.5147 +X6 Y-13 Z2.8651 +X5.5 Y-13 Z3.1411 +X5.25 Y-13 Z3.2481 +X5 Y-13 Z3.3328 +X2.75 Y-13 Z4.0734 +X2.5 Y-13 Z4.1333 +X0.5 Y-13 Z4.3605 +X0 Y-13 Z4.4039 +X-0.5 Y-13 Z4.3606 +X-2.5 Y-13 Z4.1333 +X-2.75 Y-13 Z4.0734 +X-5 Y-13 Z3.3328 +X-5.25 Y-13 Z3.2481 +X-5.5 Y-13 Z3.1411 +X-6 Y-13 Z2.8651 +X-6.5 Y-13 Z2.5147 +X-8.5 Y-13 Z1.0361 +X-8.75 Y-13 Z0.8263 +X-9 Y-13 Z0.5813 +X-9.125 Y-13 Z0.4307 +X-9.25 Y-13 Z0.2572 +X-9.375 Y-13 Z0.0772 +X-9.4375 Y-13 Z0 +X-20 Y-13 Z0 +X-20 Y-12.5 Z0 +X-20 Y-12 Z0 +X-10.6562 Y-12 Z0 +X-10.625 Y-12 Z0.0318 +X-10.375 Y-12 Z0.3893 +X-10.25 Y-12 Z0.5453 +X-10 Y-12 Z0.8068 +X-8.25 Y-12 Z2.5438 +X-8 Y-12 Z2.7673 +X-7.75 Y-12 Z2.9376 +X-7.5 Y-12 Z3.0752 +X-5 Y-12 Z4.4037 +X-4.5 Y-12 Z4.584 +X-3.5 Y-12 Z4.9036 +X-2.5 Y-12 Z5.1591 +X-2 Y-12 Z5.2194 +X0 Y-12 Z5.375 +X2 Y-12 Z5.2194 +X2.5 Y-12 Z5.1591 +X3.5 Y-12 Z4.9036 +X4.5 Y-12 Z4.584 +X5 Y-12 Z4.4037 +X7.5 Y-12 Z3.0752 +X7.75 Y-12 Z2.9376 +X8 Y-12 Z2.7673 +X8.25 Y-12 Z2.5438 +X10 Y-12 Z0.8068 +X10.25 Y-12 Z0.5453 +X10.375 Y-12 Z0.3893 +X10.625 Y-12 Z0.0318 +X10.6562 Y-12 Z0 +X20 Y-12 Z0 +X20 Y-11.5 Z0 +X20 Y-11 Z0 +X11.7188 Y-11 Z0 +X11.6875 Y-11 Z0.0401 +X11.5 Y-11 Z0.3679 +X11.375 Y-11 Z0.5586 +X11.25 Y-11 Z0.7197 +X11 Y-11 Z1.0047 +X10.5 Y-11 Z1.5204 +X9.25 Y-11 Z2.7578 +X9 Y-11 Z2.9665 +X7.5 Y-11 Z4.0464 +X7.25 Y-11 Z4.2055 +X6.5 Y-11 Z4.6117 +X6 Y-11 Z4.8731 +X5.5 Y-11 Z5.0897 +X4.5 Y-11 Z5.47 +X4 Y-11 Z5.595 +X2.25 Y-11 Z6.0149 +X2 Y-11 Z6.0488 +X0 Y-11 Z6.2044 +X-2 Y-11 Z6.0488 +X-2.25 Y-11 Z6.0149 +X-4 Y-11 Z5.595 +X-4.5 Y-11 Z5.47 +X-5.5 Y-11 Z5.0897 +X-6 Y-11 Z4.8731 +X-6.5 Y-11 Z4.6117 +X-7.25 Y-11 Z4.2055 +X-7.5 Y-11 Z4.0464 +X-9 Y-11 Z2.9665 +X-9.25 Y-11 Z2.7578 +X-10.5 Y-11 Z1.5204 +X-11 Y-11 Z1.0047 +X-11.25 Y-11 Z0.7197 +X-11.375 Y-11 Z0.5586 +X-11.5 Y-11 Z0.3679 +X-11.6875 Y-11 Z0.0401 +X-11.7188 Y-11 Z0 +X-20 Y-11 Z0 +X-20 Y-10.5 Z0 +X-20 Y-10 Z0 +X-12.5312 Y-10 Z0 +X-12.25 Y-10 Z0.4798 +X-12.125 Y-10 Z0.6534 +X-10.5 Y-10 Z2.622 +X-10.25 Y-10 Z2.898 +X-10 Y-10 Z3.1135 +X-9.5 Y-10 Z3.4859 +X-8 Y-10 Z4.5662 +X-7.5 Y-10 Z4.9147 +X-7 Y-10 Z5.1922 +X-6.5 Y-10 Z5.4315 +X-4.5 Y-10 Z6.2176 +X-4 Y-10 Z6.3919 +X-3 Y-10 Z6.631 +X-2 Y-10 Z6.8233 +X0 Y-10 Z6.9417 +X2 Y-10 Z6.8233 +X3 Y-10 Z6.631 +X4 Y-10 Z6.3919 +X4.5 Y-10 Z6.2176 +X6.5 Y-10 Z5.4315 +X7 Y-10 Z5.1922 +X7.5 Y-10 Z4.9147 +X8 Y-10 Z4.5662 +X9.5 Y-10 Z3.4859 +X10 Y-10 Z3.1135 +X10.25 Y-10 Z2.898 +X10.5 Y-10 Z2.622 +X12.125 Y-10 Z0.6534 +X12.25 Y-10 Z0.4798 +X12.5312 Y-10 Z0 +X20 Y-10 Z0 +X20 Y-9.5 Z0 +X20 Y-9 Z0 +X13.3438 Y-9 Z0 +X13.125 Y-9 Z0.3927 +X13 Y-9 Z0.5813 +X11.25 Y-9 Z2.7067 +X11 Y-9 Z2.9665 +X10 Y-9 Z3.846 +X9 Y-9 Z4.7113 +X8.75 Y-9 Z4.8946 +X8.5 Y-9 Z5.0426 +X6 Y-9 Z6.3565 +X5.5 Y-9 Z6.5597 +X5 Y-9 Z6.7358 +X4 Y-9 Z7.0235 +X3.5 Y-9 Z7.1403 +X2 Y-9 Z7.4017 +X1.5 Y-9 Z7.4553 +X0 Y-9 Z7.5391 +X-1.5 Y-9 Z7.4553 +X-2 Y-9 Z7.4017 +X-3.5 Y-9 Z7.1403 +X-4 Y-9 Z7.0235 +X-5 Y-9 Z6.7358 +X-5.5 Y-9 Z6.5597 +X-6 Y-9 Z6.3565 +X-8.5 Y-9 Z5.0426 +X-8.75 Y-9 Z4.8946 +X-9 Y-9 Z4.7113 +X-10 Y-9 Z3.846 +X-11 Y-9 Z2.9665 +X-11.25 Y-9 Z2.7067 +X-13 Y-9 Z0.5813 +X-13.125 Y-9 Z0.3927 +X-13.3438 Y-9 Z0 +X-20 Y-9 Z0 +X-20 Y-8.5 Z0 +X-20 Y-8 Z0 +X-13.9062 Y-8 Z0 +X-13.75 Y-8 Z0.3018 +X-13.625 Y-8 Z0.5292 +X-13.5 Y-8 Z0.7175 +X-12.5 Y-8 Z2.0981 +X-12 Y-8 Z2.7673 +X-11.75 Y-8 Z3.028 +X-9.75 Y-8 Z4.7853 +X-9.5 Y-8 Z4.9807 +X-8 Y-8 Z5.9443 +X-7 Y-8 Z6.4807 +X-6.5 Y-8 Z6.7268 +X-6 Y-8 Z6.9229 +X-5.5 Y-8 Z7.1133 +X-5 Y-8 Z7.27 +X-3.5 Y-8 Z7.6945 +X-3 Y-8 Z7.8019 +X-2.5 Y-8 Z7.889 +X-1.5 Y-8 Z8.0504 +X0 Y-8 Z8.1245 +X1.5 Y-8 Z8.0504 +X2 Y-8 Z7.9762 +X3 Y-8 Z7.8019 +X3.5 Y-8 Z7.6945 +X5 Y-8 Z7.27 +X5.5 Y-8 Z7.1133 +X6 Y-8 Z6.9229 +X6.5 Y-8 Z6.7268 +X7 Y-8 Z6.4807 +X8 Y-8 Z5.9443 +X9.5 Y-8 Z4.9807 +X9.75 Y-8 Z4.7853 +X11.75 Y-8 Z3.028 +X12 Y-8 Z2.7673 +X12.5 Y-8 Z2.0981 +X13.5 Y-8 Z0.7175 +X13.625 Y-8 Z0.5292 +X13.75 Y-8 Z0.3018 +X13.9062 Y-8 Z0 +X20 Y-8 Z0 +X20 Y-7.5 Z0 +X20 Y-7 Z0 +X14.4375 Y-7 Z0 +X14.25 Y-7 Z0.3696 +X14.125 Y-7 Z0.5844 +X13 Y-7 Z2.1457 +X12.5 Y-7 Z2.8243 +X12.25 Y-7 Z3.0925 +X11 Y-7 Z4.3441 +X10.5 Y-7 Z4.8317 +X10.25 Y-7 Z5.0288 +X7.75 Y-7 Z6.6376 +X7.5 Y-7 Z6.7726 +X7 Y-7 Z7.0017 +X6.5 Y-7 Z7.1966 +X5 Y-7 Z7.7679 +X4.5 Y-7 Z7.941 +X4 Y-7 Z8.0779 +X3 Y-7 Z8.2868 +X1.5 Y-7 Z8.4713 +X0 Y-7 Z8.5374 +X-1.5 Y-7 Z8.4713 +X-3 Y-7 Z8.2868 +X-4 Y-7 Z8.0779 +X-4.5 Y-7 Z7.941 +X-5 Y-7 Z7.7679 +X-6.5 Y-7 Z7.1966 +X-7 Y-7 Z7.0017 +X-7.5 Y-7 Z6.7726 +X-7.75 Y-7 Z6.6376 +X-10.25 Y-7 Z5.0288 +X-10.5 Y-7 Z4.8317 +X-11 Y-7 Z4.3441 +X-12.25 Y-7 Z3.0925 +X-12.5 Y-7 Z2.8243 +X-13 Y-7 Z2.1457 +X-14.125 Y-7 Z0.5844 +X-14.25 Y-7 Z0.3696 +X-14.4375 Y-7 Z0 +X-20 Y-7 Z0 +X-20 Y-6.5 Z0 +X-20 Y-6 Z0 +X-14.9375 Y-6 Z0 +X-14.9062 Y-6 Z0.0507 +X-14.75 Y-6 Z0.3815 +X-14.625 Y-6 Z0.6057 +X-14.5 Y-6 Z0.7928 +X-13.5 Y-6 Z2.1934 +X-13.25 Y-6 Z2.5386 +X-13 Y-6 Z2.8651 +X-12.5 Y-6 Z3.3774 +X-11 Y-6 Z4.8731 +X-10.75 Y-6 Z5.077 +X-9.5 Y-6 Z5.9958 +X-9 Y-6 Z6.3565 +X-8.5 Y-6 Z6.6781 +X-8 Y-6 Z6.9229 +X-6 Y-6 Z7.8466 +X-5.5 Y-6 Z8.0404 +X-5 Y-6 Z8.1961 +X-4 Y-6 Z8.4519 +X-2.5 Y-6 Z8.7398 +X-1.5 Y-6 Z8.8606 +X-1 Y-6 Z8.9045 +X0 Y-6 Z8.9422 +X1 Y-6 Z8.9045 +X1.5 Y-6 Z8.8606 +X2.5 Y-6 Z8.7398 +X4 Y-6 Z8.4519 +X5 Y-6 Z8.1961 +X5.5 Y-6 Z8.0404 +X6 Y-6 Z7.8466 +X8 Y-6 Z6.9229 +X8.5 Y-6 Z6.6781 +X9 Y-6 Z6.3565 +X9.5 Y-6 Z5.9958 +X10.75 Y-6 Z5.077 +X11 Y-6 Z4.8731 +X12.5 Y-6 Z3.3774 +X13 Y-6 Z2.8651 +X13.25 Y-6 Z2.5386 +X13.5 Y-6 Z2.1934 +X14.5 Y-6 Z0.7928 +X14.625 Y-6 Z0.6057 +X14.75 Y-6 Z0.3815 +X14.9062 Y-6 Z0.0507 +X14.9375 Y-6 Z0 +X20 Y-6 Z0 +X20 Y-5.5 Z0 +X20 Y-5 Z0 +X15.2344 Y-5 Z0.0017 +X15.125 Y-5 Z0.2393 +X15 Y-5 Z0.4949 +X14.875 Y-5 Z0.7035 +X13.5 Y-5 Z2.7595 +X13.25 Y-5 Z3.0612 +X12.5 Y-5 Z3.8752 +X12 Y-5 Z4.4037 +X11.5 Y-5 Z4.9038 +X11 Y-5 Z5.2861 +X9.25 Y-5 Z6.5722 +X9 Y-5 Z6.7358 +X7.5 Y-5 Z7.5308 +X6.5 Y-5 Z7.9999 +X6 Y-5 Z8.1961 +X5 Y-5 Z8.5086 +X3.5 Y-5 Z8.8979 +X2.5 Y-5 Z9.0994 +X2 Y-5 Z9.1742 +X1.5 Y-5 Z9.2129 +X1 Y-5 Z9.2465 +X0 Y-5 Z9.2698 +X-1 Y-5 Z9.2465 +X-1.5 Y-5 Z9.2129 +X-2 Y-5 Z9.1742 +X-2.5 Y-5 Z9.0994 +X-3.5 Y-5 Z8.8979 +X-5 Y-5 Z8.5086 +X-6 Y-5 Z8.1961 +X-6.5 Y-5 Z7.9999 +X-7.5 Y-5 Z7.5308 +X-9 Y-5 Z6.7358 +X-9.25 Y-5 Z6.5722 +X-11 Y-5 Z5.2861 +X-11.5 Y-5 Z4.9038 +X-12 Y-5 Z4.4037 +X-12.5 Y-5 Z3.8752 +X-13.25 Y-5 Z3.0612 +X-13.5 Y-5 Z2.7595 +X-14.875 Y-5 Z0.7035 +X-15 Y-5 Z0.4949 +X-15.125 Y-5 Z0.2393 +X-15.2344 Y-5 Z0.0017 +X-20 Y-5 Z0 +X-20 Y-4.5 Z0 +X-20 Y-4 Z0 +X-15.5625 Y-4 Z0 +X-15.5312 Y-4 Z0.0158 +X-15.375 Y-4 Z0.3545 +X-15.25 Y-4 Z0.5887 +X-15 Y-4 Z0.9709 +X-14 Y-4 Z2.4689 +X-13.75 Y-4 Z2.8308 +X-13.5 Y-4 Z3.1192 +X-12 Y-4 Z4.7488 +X-11.75 Y-4 Z4.9931 +X-10 Y-4 Z6.3919 +X-9.75 Y-4 Z6.5864 +X-9.5 Y-4 Z6.754 +X-7 Y-4 Z8.0779 +X-6.5 Y-4 Z8.2744 +X-6 Y-4 Z8.4519 +X-4 Y-4 Z9.0811 +X-3.5 Y-4 Z9.1892 +X-2.5 Y-4 Z9.3343 +X-2 Y-4 Z9.3899 +X-1 Y-4 Z9.4731 +X0 Y-4 Z9.5048 +X1 Y-4 Z9.4731 +X2 Y-4 Z9.3899 +X2.5 Y-4 Z9.3343 +X3.5 Y-4 Z9.1892 +X4 Y-4 Z9.0811 +X6 Y-4 Z8.4519 +X6.5 Y-4 Z8.2744 +X7 Y-4 Z8.0779 +X9.5 Y-4 Z6.754 +X9.75 Y-4 Z6.5864 +X10 Y-4 Z6.3919 +X11.75 Y-4 Z4.9931 +X12 Y-4 Z4.7488 +X13.5 Y-4 Z3.1192 +X13.75 Y-4 Z2.8308 +X14 Y-4 Z2.4689 +X15 Y-4 Z0.9709 +X15.25 Y-4 Z0.5887 +X15.375 Y-4 Z0.3545 +X15.5312 Y-4 Z0.0158 +X15.5625 Y-4 Z0 +X20 Y-4 Z0 +X20 Y-3.5 Z0 +X20 Y-3 Z0 +X15.8125 Y-3 Z0 +X15.625 Y-3 Z0.4091 +X15.5 Y-3 Z0.6393 +X14.25 Y-3 Z2.5466 +X14.125 Y-3 Z2.733 +X14 Y-3 Z2.8978 +X12.25 Y-3 Z4.8057 +X12 Y-3 Z5.0385 +X10 Y-3 Z6.631 +X9.75 Y-3 Z6.7961 +X7.5 Y-3 Z8.0835 +X7 Y-3 Z8.2868 +X5 Y-3 Z9.0043 +X4.5 Y-3 Z9.168 +X3 Y-3 Z9.4477 +X2 Y-3 Z9.5975 +X1 Y-3 Z9.6991 +X0.5 Y-3 Z9.729 +X0 Y-3 Z9.7398 +X-0.5 Y-3 Z9.729 +X-1 Y-3 Z9.6991 +X-2 Y-3 Z9.5975 +X-3 Y-3 Z9.4477 +X-4.5 Y-3 Z9.168 +X-5 Y-3 Z9.0043 +X-7 Y-3 Z8.2868 +X-7.5 Y-3 Z8.0835 +X-9.75 Y-3 Z6.7961 +X-10 Y-3 Z6.631 +X-12 Y-3 Z5.0385 +X-12.25 Y-3 Z4.8057 +X-14 Y-3 Z2.8978 +X-14.125 Y-3 Z2.733 +X-14.25 Y-3 Z2.5466 +X-15.5 Y-3 Z0.6393 +X-15.625 Y-3 Z0.4091 +X-15.8125 Y-3 Z0 +X-20 Y-3 Z0 +X-20 Y-2.5 Z0 +X-20 Y-2 Z0 +X-15.9375 Y-2 Z0 +X-15.9062 Y-2 Z0.0111 +X-15.75 Y-2 Z0.363 +X-15.625 Y-2 Z0.6039 +X-14.25 Y-2 Z2.7489 +X-14.125 Y-2 Z2.9169 +X-14 Y-2 Z3.0638 +X-12.5 Y-2 Z4.7586 +X-12.25 Y-2 Z5.0095 +X-10.25 Y-2 Z6.6598 +X-10 Y-2 Z6.8233 +X-9.5 Y-2 Z7.1141 +X-7.75 Y-2 Z8.1101 +X-5.5 Y-2 Z8.9962 +X-5 Y-2 Z9.1742 +X-3 Y-2 Z9.5975 +X-2 Y-2 Z9.7757 +X-1.5 Y-2 Z9.8155 +X-1 Y-2 Z9.8358 +X0 Y-2 Z9.8547 +X1 Y-2 Z9.8358 +X1.5 Y-2 Z9.8155 +X2 Y-2 Z9.7757 +X3 Y-2 Z9.5975 +X5 Y-2 Z9.1742 +X5.5 Y-2 Z8.9962 +X7.75 Y-2 Z8.1101 +X9.5 Y-2 Z7.1141 +X10 Y-2 Z6.8233 +X10.25 Y-2 Z6.6598 +X12.25 Y-2 Z5.0095 +X12.5 Y-2 Z4.7586 +X14 Y-2 Z3.0638 +X14.125 Y-2 Z2.9169 +X14.25 Y-2 Z2.7489 +X15.625 Y-2 Z0.6039 +X15.75 Y-2 Z0.363 +X15.9062 Y-2 Z0.0111 +X15.9375 Y-2 Z0 +X20 Y-2 Z0 +X20 Y-1.5 Z0 +X20 Y-1 Z0 +X16.0156 Y-1 Z0 +X16 Y-1 Z0.0218 +X15.875 Y-1 Z0.3041 +X15.75 Y-1 Z0.5571 +X15.5 Y-1 Z0.9581 +X15 Y-1 Z1.7371 +X14.5 Y-1 Z2.5161 +X14.375 Y-1 Z2.7099 +X14.25 Y-1 Z2.8833 +X12.5 Y-1 Z4.8646 +X12.25 Y-1 Z5.0937 +X10.5 Y-1 Z6.5452 +X10.25 Y-1 Z6.7329 +X8 Y-1 Z8.0776 +X7.5 Y-1 Z8.2973 +X5.5 Y-1 Z9.1056 +X5.25 Y-1 Z9.1862 +X5 Y-1 Z9.2465 +X3 Y-1 Z9.6991 +X2.5 Y-1 Z9.7967 +X1 Y-1 Z9.8999 +X0 Y-1 Z9.9318 +X-1 Y-1 Z9.8999 +X-2.5 Y-1 Z9.7967 +X-3 Y-1 Z9.6991 +X-5 Y-1 Z9.2465 +X-5.25 Y-1 Z9.1862 +X-5.5 Y-1 Z9.1056 +X-7.5 Y-1 Z8.2973 +X-8 Y-1 Z8.0776 +X-10.25 Y-1 Z6.7329 +X-10.5 Y-1 Z6.5452 +X-12.25 Y-1 Z5.0937 +X-12.5 Y-1 Z4.8646 +X-14.25 Y-1 Z2.8833 +X-14.375 Y-1 Z2.7099 +X-14.5 Y-1 Z2.5161 +X-15 Y-1 Z1.7371 +X-15.5 Y-1 Z0.9581 +X-15.75 Y-1 Z0.5571 +X-15.875 Y-1 Z0.3041 +X-16 Y-1 Z0.0218 +X-16.0156 Y-1 Z0 +X-20 Y-1 Z0 +X-20 Y-0.5 Z0 +X-20 Y0 Z0 +X-16.0938 Y0 Z0.0025 +X-15.875 Y0 Z0.4835 +X-15.75 Y0 Z0.7028 +X-14.5 Y0 Z2.6505 +X-14.375 Y0 Z2.833 +X-14.25 Y0 Z2.9906 +X-14 Y0 Z3.2741 +X-12.75 Y0 Z4.6864 +X-12.5 Y0 Z4.9517 +X-10.5 Y0 Z6.6171 +X-10.25 Y0 Z6.7916 +X-9 Y0 Z7.5391 +X-8 Y0 Z8.1245 +X-5.5 Y0 Z9.1388 +X-5 Y0 Z9.2698 +X-3 Y0 Z9.7398 +X-2.75 Y0 Z9.7887 +X-2.5 Y0 Z9.8162 +X0 Y0 Z10 +X2.5 Y0 Z9.8162 +X2.75 Y0 Z9.7887 +X3 Y0 Z9.7398 +X5 Y0 Z9.2698 +X5.5 Y0 Z9.1388 +X8 Y0 Z8.1245 +X9 Y0 Z7.5391 +X10.25 Y0 Z6.7916 +X10.5 Y0 Z6.6171 +X12.5 Y0 Z4.9517 +X12.75 Y0 Z4.6864 +X14 Y0 Z3.2741 +X14.25 Y0 Z2.9906 +X14.375 Y0 Z2.833 +X14.5 Y0 Z2.6505 +X15.75 Y0 Z0.7028 +X15.875 Y0 Z0.4835 +X16.0938 Y0 Z0.0025 +X20 Y0 Z0 +X20 Y0.5 Z0 +X20 Y1 Z0 +X16.0156 Y1 Z0 +X16 Y1 Z0.0218 +X15.875 Y1 Z0.3041 +X15.75 Y1 Z0.5571 +X15.5 Y1 Z0.9581 +X15 Y1 Z1.7371 +X14.5 Y1 Z2.5161 +X14.375 Y1 Z2.7099 +X14.25 Y1 Z2.8833 +X12.5 Y1 Z4.8646 +X12.25 Y1 Z5.0937 +X10.5 Y1 Z6.5452 +X10.25 Y1 Z6.7329 +X8 Y1 Z8.0776 +X7.5 Y1 Z8.2973 +X5.5 Y1 Z9.1056 +X5.25 Y1 Z9.1862 +X5 Y1 Z9.2465 +X3 Y1 Z9.6991 +X2.5 Y1 Z9.7967 +X1 Y1 Z9.8999 +X0 Y1 Z9.9318 +X-1 Y1 Z9.8999 +X-2.5 Y1 Z9.7967 +X-3 Y1 Z9.6991 +X-5 Y1 Z9.2465 +X-5.25 Y1 Z9.1862 +X-5.5 Y1 Z9.1056 +X-7.5 Y1 Z8.2973 +X-8 Y1 Z8.0776 +X-10.25 Y1 Z6.7329 +X-10.5 Y1 Z6.5452 +X-12.25 Y1 Z5.0937 +X-12.5 Y1 Z4.8646 +X-14.25 Y1 Z2.8833 +X-14.375 Y1 Z2.7099 +X-14.5 Y1 Z2.5161 +X-15 Y1 Z1.7371 +X-15.5 Y1 Z0.9581 +X-15.75 Y1 Z0.5571 +X-15.875 Y1 Z0.3041 +X-16 Y1 Z0.0218 +X-16.0156 Y1 Z0 +X-20 Y1 Z0 +X-20 Y1.5 Z0 +X-20 Y2 Z0 +X-15.9375 Y2 Z0 +X-15.9062 Y2 Z0.0111 +X-15.75 Y2 Z0.363 +X-15.625 Y2 Z0.6039 +X-14.25 Y2 Z2.7489 +X-14.125 Y2 Z2.9169 +X-14 Y2 Z3.0638 +X-12.5 Y2 Z4.7586 +X-12.25 Y2 Z5.0095 +X-10.25 Y2 Z6.6598 +X-10 Y2 Z6.8233 +X-9.5 Y2 Z7.1141 +X-7.75 Y2 Z8.1101 +X-5.5 Y2 Z8.9962 +X-5 Y2 Z9.1742 +X-3 Y2 Z9.5975 +X-2 Y2 Z9.7757 +X-1.5 Y2 Z9.8155 +X-1 Y2 Z9.8358 +X0 Y2 Z9.8547 +X1 Y2 Z9.8358 +X1.5 Y2 Z9.8155 +X2 Y2 Z9.7757 +X3 Y2 Z9.5975 +X5 Y2 Z9.1742 +X5.5 Y2 Z8.9962 +X7.75 Y2 Z8.1101 +X9.5 Y2 Z7.1141 +X10 Y2 Z6.8233 +X10.25 Y2 Z6.6598 +X12.25 Y2 Z5.0095 +X12.5 Y2 Z4.7586 +X14 Y2 Z3.0638 +X14.125 Y2 Z2.9169 +X14.25 Y2 Z2.7489 +X15.625 Y2 Z0.6039 +X15.75 Y2 Z0.363 +X15.9062 Y2 Z0.0111 +X15.9375 Y2 Z0 +X20 Y2 Z0 +X20 Y2.5 Z0 +X20 Y3 Z0 +X15.8125 Y3 Z0 +X15.625 Y3 Z0.4091 +X15.5 Y3 Z0.6393 +X14.25 Y3 Z2.5466 +X14.125 Y3 Z2.733 +X14 Y3 Z2.8978 +X12.25 Y3 Z4.8057 +X12 Y3 Z5.0385 +X10 Y3 Z6.631 +X9.75 Y3 Z6.7961 +X7.5 Y3 Z8.0835 +X7 Y3 Z8.2868 +X5 Y3 Z9.0043 +X4.5 Y3 Z9.168 +X3 Y3 Z9.4477 +X2 Y3 Z9.5975 +X1 Y3 Z9.6991 +X0.5 Y3 Z9.729 +X0 Y3 Z9.7398 +X-0.5 Y3 Z9.729 +X-1 Y3 Z9.6991 +X-2 Y3 Z9.5975 +X-3 Y3 Z9.4477 +X-4.5 Y3 Z9.168 +X-5 Y3 Z9.0043 +X-7 Y3 Z8.2868 +X-7.5 Y3 Z8.0835 +X-9.75 Y3 Z6.7961 +X-10 Y3 Z6.631 +X-12 Y3 Z5.0385 +X-12.25 Y3 Z4.8057 +X-14 Y3 Z2.8978 +X-14.125 Y3 Z2.733 +X-14.25 Y3 Z2.5466 +X-15.5 Y3 Z0.6393 +X-15.625 Y3 Z0.4091 +X-15.8125 Y3 Z0 +X-20 Y3 Z0 +X-20 Y3.5 Z0 +X-20 Y4 Z0 +X-15.5625 Y4 Z0 +X-15.5312 Y4 Z0.0158 +X-15.375 Y4 Z0.3545 +X-15.25 Y4 Z0.5887 +X-15 Y4 Z0.9709 +X-14 Y4 Z2.4689 +X-13.75 Y4 Z2.8308 +X-13.5 Y4 Z3.1192 +X-12 Y4 Z4.7488 +X-11.75 Y4 Z4.9931 +X-10 Y4 Z6.3919 +X-9.75 Y4 Z6.5864 +X-9.5 Y4 Z6.754 +X-7 Y4 Z8.0779 +X-6.5 Y4 Z8.2744 +X-6 Y4 Z8.4519 +X-4 Y4 Z9.0811 +X-3.5 Y4 Z9.1892 +X-2.5 Y4 Z9.3343 +X-2 Y4 Z9.3899 +X-1 Y4 Z9.4731 +X0 Y4 Z9.5048 +X1 Y4 Z9.4731 +X2 Y4 Z9.3899 +X2.5 Y4 Z9.3343 +X3.5 Y4 Z9.1892 +X4 Y4 Z9.0811 +X6 Y4 Z8.4519 +X6.5 Y4 Z8.2744 +X7 Y4 Z8.0779 +X9.5 Y4 Z6.754 +X9.75 Y4 Z6.5864 +X10 Y4 Z6.3919 +X11.75 Y4 Z4.9931 +X12 Y4 Z4.7488 +X13.5 Y4 Z3.1192 +X13.75 Y4 Z2.8308 +X14 Y4 Z2.4689 +X15 Y4 Z0.9709 +X15.25 Y4 Z0.5887 +X15.375 Y4 Z0.3545 +X15.5312 Y4 Z0.0158 +X15.5625 Y4 Z0 +X20 Y4 Z0 +X20 Y4.5 Z0 +X20 Y5 Z0 +X15.2344 Y5 Z0.0017 +X15.125 Y5 Z0.2393 +X15 Y5 Z0.4949 +X14.875 Y5 Z0.7035 +X13.5 Y5 Z2.7595 +X13.25 Y5 Z3.0612 +X12.5 Y5 Z3.8752 +X12 Y5 Z4.4037 +X11.5 Y5 Z4.9038 +X11 Y5 Z5.2861 +X9.25 Y5 Z6.5722 +X9 Y5 Z6.7358 +X7.5 Y5 Z7.5308 +X6.5 Y5 Z7.9999 +X6 Y5 Z8.1961 +X5 Y5 Z8.5086 +X3.5 Y5 Z8.8979 +X2.5 Y5 Z9.0994 +X2 Y5 Z9.1742 +X1.5 Y5 Z9.2129 +X1 Y5 Z9.2465 +X0 Y5 Z9.2698 +X-1 Y5 Z9.2465 +X-1.5 Y5 Z9.2129 +X-2 Y5 Z9.1742 +X-2.5 Y5 Z9.0994 +X-3.5 Y5 Z8.8979 +X-5 Y5 Z8.5086 +X-6 Y5 Z8.1961 +X-6.5 Y5 Z7.9999 +X-7.5 Y5 Z7.5308 +X-9 Y5 Z6.7358 +X-9.25 Y5 Z6.5722 +X-11 Y5 Z5.2861 +X-11.5 Y5 Z4.9038 +X-12 Y5 Z4.4037 +X-12.5 Y5 Z3.8752 +X-13.25 Y5 Z3.0612 +X-13.5 Y5 Z2.7595 +X-14.875 Y5 Z0.7035 +X-15 Y5 Z0.4949 +X-15.125 Y5 Z0.2393 +X-15.2344 Y5 Z0.0017 +X-20 Y5 Z0 +X-20 Y5.5 Z0 +X-20 Y6 Z0 +X-14.9375 Y6 Z0 +X-14.9062 Y6 Z0.0507 +X-14.75 Y6 Z0.3815 +X-14.625 Y6 Z0.6057 +X-14.5 Y6 Z0.7928 +X-13.5 Y6 Z2.1934 +X-13.25 Y6 Z2.5386 +X-13 Y6 Z2.8651 +X-12.5 Y6 Z3.3774 +X-11 Y6 Z4.8731 +X-10.75 Y6 Z5.077 +X-9.5 Y6 Z5.9958 +X-9 Y6 Z6.3565 +X-8.5 Y6 Z6.6781 +X-8 Y6 Z6.9229 +X-6 Y6 Z7.8466 +X-5.5 Y6 Z8.0404 +X-5 Y6 Z8.1961 +X-4 Y6 Z8.4519 +X-2.5 Y6 Z8.7398 +X-1.5 Y6 Z8.8606 +X-1 Y6 Z8.9045 +X0 Y6 Z8.9422 +X1 Y6 Z8.9045 +X1.5 Y6 Z8.8606 +X2.5 Y6 Z8.7398 +X4 Y6 Z8.4519 +X5 Y6 Z8.1961 +X5.5 Y6 Z8.0404 +X6 Y6 Z7.8466 +X8 Y6 Z6.9229 +X8.5 Y6 Z6.6781 +X9 Y6 Z6.3565 +X9.5 Y6 Z5.9958 +X10.75 Y6 Z5.077 +X11 Y6 Z4.8731 +X12.5 Y6 Z3.3774 +X13 Y6 Z2.8651 +X13.25 Y6 Z2.5386 +X13.5 Y6 Z2.1934 +X14.5 Y6 Z0.7928 +X14.625 Y6 Z0.6057 +X14.75 Y6 Z0.3815 +X14.9062 Y6 Z0.0507 +X14.9375 Y6 Z0 +X20 Y6 Z0 +X20 Y6.5 Z0 +X20 Y7 Z0 +X14.4375 Y7 Z0 +X14.25 Y7 Z0.3696 +X14.125 Y7 Z0.5844 +X13 Y7 Z2.1457 +X12.5 Y7 Z2.8243 +X12.25 Y7 Z3.0925 +X11 Y7 Z4.3441 +X10.5 Y7 Z4.8317 +X10.25 Y7 Z5.0288 +X7.75 Y7 Z6.6376 +X7.5 Y7 Z6.7726 +X7 Y7 Z7.0017 +X6.5 Y7 Z7.1966 +X5 Y7 Z7.7679 +X4.5 Y7 Z7.941 +X4 Y7 Z8.0779 +X3 Y7 Z8.2868 +X1.5 Y7 Z8.4713 +X0 Y7 Z8.5374 +X-1.5 Y7 Z8.4713 +X-3 Y7 Z8.2868 +X-4 Y7 Z8.0779 +X-4.5 Y7 Z7.941 +X-5 Y7 Z7.7679 +X-6.5 Y7 Z7.1966 +X-7 Y7 Z7.0017 +X-7.5 Y7 Z6.7726 +X-7.75 Y7 Z6.6376 +X-10.25 Y7 Z5.0288 +X-10.5 Y7 Z4.8317 +X-11 Y7 Z4.3441 +X-12.25 Y7 Z3.0925 +X-12.5 Y7 Z2.8243 +X-13 Y7 Z2.1457 +X-14.125 Y7 Z0.5844 +X-14.25 Y7 Z0.3696 +X-14.4375 Y7 Z0 +X-20 Y7 Z0 +X-20 Y7.5 Z0 +X-20 Y8 Z0 +X-13.9062 Y8 Z0 +X-13.75 Y8 Z0.3018 +X-13.625 Y8 Z0.5292 +X-13.5 Y8 Z0.7175 +X-12.5 Y8 Z2.0981 +X-12 Y8 Z2.7673 +X-11.75 Y8 Z3.028 +X-9.75 Y8 Z4.7853 +X-9.5 Y8 Z4.9807 +X-8 Y8 Z5.9443 +X-7 Y8 Z6.4807 +X-6.5 Y8 Z6.7268 +X-6 Y8 Z6.9229 +X-5.5 Y8 Z7.1133 +X-5 Y8 Z7.27 +X-3.5 Y8 Z7.6945 +X-3 Y8 Z7.8019 +X-2.5 Y8 Z7.889 +X-1.5 Y8 Z8.0504 +X0 Y8 Z8.1245 +X1.5 Y8 Z8.0504 +X2 Y8 Z7.9762 +X3 Y8 Z7.8019 +X3.5 Y8 Z7.6945 +X5 Y8 Z7.27 +X5.5 Y8 Z7.1133 +X6 Y8 Z6.9229 +X6.5 Y8 Z6.7268 +X7 Y8 Z6.4807 +X8 Y8 Z5.9443 +X9.5 Y8 Z4.9807 +X9.75 Y8 Z4.7853 +X11.75 Y8 Z3.028 +X12 Y8 Z2.7673 +X12.5 Y8 Z2.0981 +X13.5 Y8 Z0.7175 +X13.625 Y8 Z0.5292 +X13.75 Y8 Z0.3018 +X13.9062 Y8 Z0 +X20 Y8 Z0 +X20 Y8.5 Z0 +X20 Y9 Z0 +X13.3438 Y9 Z0 +X13.125 Y9 Z0.3927 +X13 Y9 Z0.5813 +X11.25 Y9 Z2.7067 +X11 Y9 Z2.9665 +X10 Y9 Z3.846 +X9 Y9 Z4.7113 +X8.75 Y9 Z4.8946 +X8.5 Y9 Z5.0426 +X6 Y9 Z6.3565 +X5.5 Y9 Z6.5597 +X5 Y9 Z6.7358 +X4 Y9 Z7.0235 +X3.5 Y9 Z7.1403 +X2 Y9 Z7.4017 +X1.5 Y9 Z7.4553 +X0 Y9 Z7.5391 +X-1.5 Y9 Z7.4553 +X-2 Y9 Z7.4017 +X-3.5 Y9 Z7.1403 +X-4 Y9 Z7.0235 +X-5 Y9 Z6.7358 +X-5.5 Y9 Z6.5597 +X-6 Y9 Z6.3565 +X-8.5 Y9 Z5.0426 +X-8.75 Y9 Z4.8946 +X-9 Y9 Z4.7113 +X-10 Y9 Z3.846 +X-11 Y9 Z2.9665 +X-11.25 Y9 Z2.7067 +X-13 Y9 Z0.5813 +X-13.125 Y9 Z0.3927 +X-13.3438 Y9 Z0 +X-20 Y9 Z0 +X-20 Y9.5 Z0 +X-20 Y10 Z0 +X-12.5312 Y10 Z0 +X-12.25 Y10 Z0.4798 +X-12.125 Y10 Z0.6534 +X-10.5 Y10 Z2.622 +X-10.25 Y10 Z2.898 +X-10 Y10 Z3.1135 +X-9.5 Y10 Z3.4859 +X-8 Y10 Z4.5662 +X-7.5 Y10 Z4.9147 +X-7 Y10 Z5.1922 +X-6.5 Y10 Z5.4315 +X-4.5 Y10 Z6.2176 +X-4 Y10 Z6.3919 +X-3 Y10 Z6.631 +X-2 Y10 Z6.8233 +X0 Y10 Z6.9417 +X2 Y10 Z6.8233 +X3 Y10 Z6.631 +X4 Y10 Z6.3919 +X4.5 Y10 Z6.2176 +X6.5 Y10 Z5.4315 +X7 Y10 Z5.1922 +X7.5 Y10 Z4.9147 +X8 Y10 Z4.5662 +X9.5 Y10 Z3.4859 +X10 Y10 Z3.1135 +X10.25 Y10 Z2.898 +X10.5 Y10 Z2.622 +X12.125 Y10 Z0.6534 +X12.25 Y10 Z0.4798 +X12.5312 Y10 Z0 +X20 Y10 Z0 +X20 Y10.5 Z0 +X20 Y11 Z0 +X11.7188 Y11 Z0 +X11.6875 Y11 Z0.0401 +X11.5 Y11 Z0.3679 +X11.375 Y11 Z0.5586 +X11.25 Y11 Z0.7197 +X11 Y11 Z1.0047 +X10.5 Y11 Z1.5204 +X9.25 Y11 Z2.7578 +X9 Y11 Z2.9665 +X7.5 Y11 Z4.0464 +X7.25 Y11 Z4.2055 +X6.5 Y11 Z4.6117 +X6 Y11 Z4.8731 +X5.5 Y11 Z5.0897 +X4.5 Y11 Z5.47 +X4 Y11 Z5.595 +X2.25 Y11 Z6.0149 +X2 Y11 Z6.0488 +X0 Y11 Z6.2044 +X-2 Y11 Z6.0488 +X-2.25 Y11 Z6.0149 +X-4 Y11 Z5.595 +X-4.5 Y11 Z5.47 +X-5.5 Y11 Z5.0897 +X-6 Y11 Z4.8731 +X-6.5 Y11 Z4.6117 +X-7.25 Y11 Z4.2055 +X-7.5 Y11 Z4.0464 +X-9 Y11 Z2.9665 +X-9.25 Y11 Z2.7578 +X-10.5 Y11 Z1.5204 +X-11 Y11 Z1.0047 +X-11.25 Y11 Z0.7197 +X-11.375 Y11 Z0.5586 +X-11.5 Y11 Z0.3679 +X-11.6875 Y11 Z0.0401 +X-11.7188 Y11 Z0 +X-20 Y11 Z0 +X-20 Y11.5 Z0 +X-20 Y12 Z0 +X-10.6562 Y12 Z0 +X-10.625 Y12 Z0.0318 +X-10.375 Y12 Z0.3893 +X-10.25 Y12 Z0.5453 +X-10 Y12 Z0.8068 +X-8.25 Y12 Z2.5438 +X-8 Y12 Z2.7673 +X-7.75 Y12 Z2.9376 +X-7.5 Y12 Z3.0752 +X-5 Y12 Z4.4037 +X-4.5 Y12 Z4.584 +X-3.5 Y12 Z4.9036 +X-2.5 Y12 Z5.1591 +X-2 Y12 Z5.2194 +X0 Y12 Z5.375 +X2 Y12 Z5.2194 +X2.5 Y12 Z5.1591 +X3.5 Y12 Z4.9036 +X4.5 Y12 Z4.584 +X5 Y12 Z4.4037 +X7.5 Y12 Z3.0752 +X7.75 Y12 Z2.9376 +X8 Y12 Z2.7673 +X8.25 Y12 Z2.5438 +X10 Y12 Z0.8068 +X10.25 Y12 Z0.5453 +X10.375 Y12 Z0.3893 +X10.625 Y12 Z0.0318 +X10.6562 Y12 Z0 +X20 Y12 Z0 +X20 Y12.5 Z0 +X20 Y13 Z0 +X9.4375 Y13 Z0 +X9.375 Y13 Z0.0772 +X9.25 Y13 Z0.2572 +X9.125 Y13 Z0.4307 +X9 Y13 Z0.5813 +X8.75 Y13 Z0.8263 +X8.5 Y13 Z1.0361 +X6.5 Y13 Z2.5147 +X6 Y13 Z2.8651 +X5.5 Y13 Z3.1411 +X5.25 Y13 Z3.2481 +X5 Y13 Z3.3328 +X2.75 Y13 Z4.0734 +X2.5 Y13 Z4.1333 +X0.5 Y13 Z4.3606 +X0 Y13 Z4.4039 +X-0.5 Y13 Z4.3605 +X-2.5 Y13 Z4.1333 +X-2.75 Y13 Z4.0734 +X-5 Y13 Z3.3328 +X-5.25 Y13 Z3.2481 +X-5.5 Y13 Z3.1411 +X-6 Y13 Z2.8651 +X-6.5 Y13 Z2.5147 +X-8.5 Y13 Z1.0361 +X-8.75 Y13 Z0.8263 +X-9 Y13 Z0.5813 +X-9.125 Y13 Z0.4307 +X-9.25 Y13 Z0.2572 +X-9.375 Y13 Z0.0772 +X-9.4375 Y13 Z0 +X-20 Y13 Z0 +X-20 Y13.5 Z0 +X-20 Y14 Z0 +X-7.8125 Y14 Z0.002 +X-7.5 Y14 Z0.3362 +X-7.25 Y14 Z0.5709 +X-6 Y14 Z1.5019 +X-5.75 Y14 Z1.6609 +X-5.5 Y14 Z1.7873 +X-3.5 Y14 Z2.6957 +X-3 Y14 Z2.8978 +X-2.75 Y14 Z2.9696 +X-0.5 Y14 Z3.2307 +X0 Y14 Z3.2741 +X0.5 Y14 Z3.2307 +X2.75 Y14 Z2.9696 +X3 Y14 Z2.8978 +X3.5 Y14 Z2.6957 +X5.5 Y14 Z1.7873 +X5.75 Y14 Z1.6609 +X6 Y14 Z1.5019 +X7.25 Y14 Z0.5709 +X7.5 Y14 Z0.3362 +X7.8125 Y14 Z0.002 +X20 Y14 Z0 +X20 Y14.5 Z0 +X20 Y15 Z0 +X5.8125 Y15 Z0 +X5.75 Y15 Z0.0166 +X5.5 Y15 Z0.1813 +X5 Y15 Z0.4949 +X4.5 Y15 Z0.7437 +X3.5 Y15 Z1.1981 +X3 Y15 Z1.4086 +X2.75 Y15 Z1.4685 +X0.25 Y15 Z1.8522 +X0 Y15 Z1.8715 +X-0.25 Y15 Z1.8522 +X-2.75 Y15 Z1.4685 +X-3 Y15 Z1.4086 +X-3.5 Y15 Z1.1981 +X-4.5 Y15 Z0.7437 +X-5 Y15 Z0.4949 +X-5.5 Y15 Z0.1813 +X-5.75 Y15 Z0.0166 +X-5.8125 Y15 Z0 +X-20 Y15 Z0 +X-20 Y15.5 Z0 +X-20 Y16 Z0 +X-1.125 Y16 Z0 +X-0.25 Y16 Z0.1885 +X0 Y16 Z0.2143 +X0.25 Y16 Z0.1885 +X1.125 Y16 Z0 +X20 Y16 Z0 +X20 Y16.5 Z0 +X20 Y17 Z0 +X-20 Y17 Z0 +X-20 Y17.5 Z0 +X-20 Y18 Z0 +X20 Y18 Z0 +X20 Y18.5 Z0 +X20 Y19 Z0 +X-20 Y19 Z0 +X-20 Y19.5 Z0 +X-20 Y20 Z0 +X20 Y20 Z0 +X20 Y20 Z2 +M5 +G0 X20 Y20 Z10 +M9 +M30 diff --git a/crates/post/tests/golden/sample.mach4.nc b/crates/post/tests/golden/sample.mach4.nc new file mode 100644 index 0000000..cc03826 --- /dev/null +++ b/crates/post/tests/golden/sample.mach4.nc @@ -0,0 +1,52 @@ +(sample.job) +G21 +G90 +G91.1 +G94 +G17 +G40 +G49 +G80 +(toolpath inner) +T1 M6 +G0 X17 Y30 Z5 +X17 Y30 Z2 +M3 S12000 +G1 X17 Y30 Z-3 F300 +G3 X17 Y30 Z-3 I-2 J0 F1000 +M5 +G0 X17 Y30 Z5 +X40 Y28 Z5 +X40 Y28 Z2 +M3 S12000 +G1 X40 Y28 Z-3 F300 +X60 Y28 Z-3 F1000 +G3 X60 Y32 Z-3 I0 J2 +G1 X40 Y32 Z-3 +G3 X40 Y28 Z-3 I0 J-2 +M5 +G0 X40 Y28 Z5 +X87 Y30 Z5 +X87 Y30 Z2 +M3 S12000 +G1 X87 Y30 Z-3 F300 +G3 X87 Y30 Z-3 I-2 J0 F1000 +M5 +G0 X87 Y30 Z5 +(toolpath outer) +X0 Y-3 Z5 +X0 Y-3 Z2 +M3 S12000 +G1 X0 Y-3 Z-3 F300 +G2 X-3 Y0 Z-3 I0 J3 F1000 +G1 X-3 Y60 Z-3 +G2 X0 Y63 Z-3 I3 J0 +G1 X100 Y63 Z-3 +G2 X103 Y60 Z-3 I0 J-3 +G1 X103 Y0 Z-3 +G2 X100 Y-3 Z-3 I-3 J0 +G1 X0 Y-3 Z-3 +M5 +G0 X0 Y-3 Z5 +M9 +M30 diff --git a/crates/post/tests/mach4.rs b/crates/post/tests/mach4.rs new file mode 100644 index 0000000..b17f32e --- /dev/null +++ b/crates/post/tests/mach4.rs @@ -0,0 +1,267 @@ +//! Mach4 v1.1 / Build 3775: public-manual semantics and synthetic geometry only. +#![allow(clippy::expect_used)] + +use campanion_ir::{ + ArcSense, ClBlock, ClMotion, ClPath, CompensationMode, CoolantMode, Document, Event, Feeds, MoveRole, Plane, Pose, ProcessKind, ProcessParams, + Program, Tool, Vec3, +}; +use campanion_post::{PostDef, PostOptions, ReadMove, audit, builtin, compare, load_post, post_program, read_back}; +use proptest::prelude::*; +use serde_json::{Value, json}; + +fn post() -> PostDef { + builtin("mach4").expect("Mach4 built-in") +} +fn raw_post() -> Value { + serde_json::from_str(include_str!("../posts/mach4.json")).expect("post JSON") +} +fn pose(p: [f64; 3]) -> Pose { + Pose::new(Vec3::new(p[0], p[1], p[2]).expect("tip"), Vec3::Z).expect("pose") +} +fn rapid(p: [f64; 3]) -> ClBlock { + ClBlock::Motion(ClMotion::Rapid { to: pose(p), feed: None }) +} +fn program(blocks: Vec) -> Program { + let feeds = Feeds::try_new(100.0, None, None, None, None).expect("feeds"); + let path = ClPath::try_new(None, None, CompensationMode::Off, feeds, None, vec![], blocks).expect("path"); + Program::try_new(Some("mach4-test".into()), vec![Tool::try_new(7, None, None, None).expect("tool")], None, vec![path], vec![], vec![], None) + .expect("program") +} +fn write(p: &Program, post: &PostDef) -> String { + post_program(p, post, &PostOptions { arc_tolerance: 0.01 }).expect("post program") +} +fn roundtrip(p: &Program) -> String { + let text = write(p, &post()); + compare(p, &read_back(&text, &post()).expect("read"), 0.001, &post()).expect("compare"); + audit(&text, &post()).expect("audit"); + text +} +fn dwell_seconds(text: &str, post: &PostDef) -> Vec { + read_back(text, post) + .expect("read") + .into_iter() + .filter_map(|m| match m { + ReadMove::Dwell { seconds } => Some(seconds), + _ => None, + }) + .collect() +} + +#[test] +fn startup_tool_change_process_and_shutdown() { + let p = program(vec![ + rapid([0.0, 0.0, 5.0]), + ClBlock::Event(Event::ToolChange { tool_id: 7 }), + ClBlock::Event(Event::ProcessOn { process: ProcessKind::Spindle, params: ProcessParams::new().speed(Some(12000.0)).expect("speed") }), + ClBlock::Event(Event::Coolant { mode: CoolantMode::Flood }), + ClBlock::Event(Event::Coolant { mode: CoolantMode::Mist }), + ClBlock::Motion(ClMotion::Linear { to: pose([10.0, 0.0, 5.0]), feed: 100.0, role: MoveRole::Cut }), + // Intentionally leave the spindle and coolant on: program shutdown must stop both. + ]); + let text = roundtrip(&p); + assert!(text.starts_with("(mach4-test)\nG21\nG90\nG91.1\nG94\nG17\nG40\nG49\nG80\n"), "{text}"); + assert!(text.contains("T7 M6\nM3 S12000\nM8\nM7\n"), "{text}"); + assert!(text.ends_with("M5\nM9\nM30\n"), "{text}"); + assert!(!text.contains("G43") && !text.contains("G53"), "{text}"); + assert!(!text.lines().any(|l| l.starts_with('N') || l.starts_with('O')), "{text}"); +} + +#[test] +fn writer_keeps_decimal_seconds_even_when_zeroes_are_trimmed() { + let values = [0.0, 2.0, 0.5, 1.25, 2.0001]; + let p = program(values.into_iter().map(|seconds| ClBlock::Event(Event::Dwell { seconds })).collect()); + let text = write(&p, &post()); + let lines: Vec<_> = text.lines().filter(|l| l.starts_with("G4 ")).collect(); + assert_eq!(lines, ["G4 P0.0", "G4 P2.0", "G4 P0.5", "G4 P1.25", "G4 P2.0"]); + assert_eq!(dwell_seconds(&text, &post()), [0.0, 2.0, 0.5, 1.25, 2.0]); + audit(&text, &post()).expect("dwell audit"); + for trailing in [false, true] { + let mut raw = raw_post(); + raw["decimals"]["dwell"] = json!(0); + raw["trailing_zeros"] = json!(trailing); + let post = load_post(&raw.to_string()).expect("zero decimal precision"); + let text = write(&program(vec![ClBlock::Event(Event::Dwell { seconds: 2.0 })]), &post); + assert!(text.contains("G4 P2.0\n"), "{text}"); + assert_eq!(dwell_seconds(&text, &post), [2.0]); + } +} + +#[test] +fn literal_spelling_controls_duration_including_glued_words_and_comments() { + for (word, seconds) in [("2", 0.002), ("2000", 2.0), ("2.0", 2.0), ("2.", 2.0), (".5", 0.5), ("0", 0.0), ("0.0", 0.0), ("+0002", 0.002)] { + let text = format!("g04p{word}(P9.9) ; P8.8\n"); + assert_eq!(dwell_seconds(&text, &post()), [seconds], "{text}"); + audit(&text, &post()).expect("literal dwell audit"); + } + // Dwell neither moves nor replaces modal G1/feed. + let text = "G21 G90 G17\nG0 X0 Y0 Z0\nG1 X1 F100\nG4 P2\nX2\n"; + assert_eq!(dwell_seconds(text, &post()), [0.002]); + let report = audit(text, &post()).expect("modal audit"); + assert_eq!(report.motions.last().expect("last move").end, [2.0, 0.0, 0.0]); + assert_eq!(report.motions.last().expect("last move").feed, Some(100.0)); +} + +#[test] +fn custom_posts_inherit_the_setting_and_existing_units_keep_their_meaning() { + let mut raw = raw_post(); + raw["id"] = json!("custom-mill"); + let custom = load_post(&raw.to_string()).expect("custom"); + assert_eq!(dwell_seconds("G4 P2\nG4 P2.0", &custom), [0.002, 2.0]); + for (unit, expected, token) in [("s", 2.0, "P2"), ("ms", 0.002, "P2000")] { + raw["dwell"] = json!({"word": "P", "unit": unit}); + let legacy = load_post(&raw.to_string()).expect("legacy dwell"); + assert_eq!(dwell_seconds("G4 P2\nG4 P2.0", &legacy), [expected, expected]); + let text = write(&program(vec![ClBlock::Event(Event::Dwell { seconds: 2.0 })]), &legacy); + assert!(text.contains(&format!("G4 {token}\n")), "{text}"); + } + for dwell in [json!({"word":"P","unit":"ms","decimal_seconds":true}), json!({"word":"X","unit":"s","decimal_seconds":true})] { + raw["dwell"] = dwell; + let err = load_post(&raw.to_string()).expect_err("incompatible dwell"); + assert!(err.to_string().contains("dwell.decimal_seconds"), "{err}"); + } +} + +#[test] +fn malformed_dwell_blocks_fail_in_reader_and_audit() { + for text in [ + "G4", + "G4 P-1", + "G4 P1 P2.0", + "G4 G4 P2", + "G4 G90 P2", + "G91.1 G4 P2", + "G4 G53 P2", + "G53 G4 P2", + "G4 G0 P2", + "G4 G1 X1 P2 F100", + "G4 P2 X1", + "G4 P2 I1", + "G4 P2 J1", + "G4 P2 K1", + "G4 P2 R1", + "G4 P2 Q1", + "G4 X2", + "G4 U2", + "G4 P#2", + "G4 PNaN", + "G4 Pinf", + "G4 P1e3", + "G4 P2..0", + ] { + assert!(read_back(text, &post()).is_err(), "reader accepted {text}"); + assert!(audit(text, &post()).is_err(), "audit accepted {text}"); + } + let overflow = format!("G4 P{}", "9".repeat(400)); + assert!(read_back(&overflow, &post()).is_err()); + assert!(audit(&overflow, &post()).is_err()); + // IR validation or writer validation must reject hostile event values. + for seconds in [-1.0, f64::NAN, f64::INFINITY] { + let feeds = Feeds::try_new(100.0, None, None, None, None).expect("feeds"); + if let Ok(path) = ClPath::try_new(None, None, CompensationMode::Off, feeds, None, vec![], vec![ClBlock::Event(Event::Dwell { seconds })]) + && let Ok(p) = Program::try_new(None, vec![], None, vec![path], vec![], vec![], None) + { + assert!(post_program(&p, &post(), &PostOptions { arc_tolerance: 0.01 }).is_err()); + } + } +} + +#[test] +fn arcs_circles_and_helices_use_incremental_ijk_in_every_plane() { + for plane in [Plane::Xy, Plane::Zx, Plane::Yz] { + let map = |u, v, w| match plane { + Plane::Xy => [u, v, w], + Plane::Zx => [v, w, u], + Plane::Yz => [w, u, v], + }; + // Nonzero centre distinguishes incremental offsets from absolute coordinates. + let center = map(10.0, 20.0, 3.0); + for sense in [ArcSense::Cw, ArcSense::Ccw] { + for full in [false, true] { + for rise in [0.0, 2.0] { + let end = + if full { map(12.0, 20.0, 3.0 + rise) } else { map(10.0, if sense == ArcSense::Ccw { 22.0 } else { 18.0 }, 3.0 + rise) }; + let motion = if rise == 0.0 { + ClMotion::Arc { + to: pose(end), + plane, + center: Vec3::new(center[0], center[1], center[2]).expect("centre"), + sense, + feed: 100.0, + role: MoveRole::Cut, + } + } else { + ClMotion::Helix { + to: pose(end), + plane, + center: Vec3::new(center[0], center[1], center[2]).expect("centre"), + sense, + sweep: if full { std::f64::consts::TAU } else { std::f64::consts::FRAC_PI_2 }, + feed: 100.0, + role: MoveRole::Cut, + } + }; + let p = program(vec![rapid(map(12.0, 20.0, 3.0)), ClBlock::Motion(motion)]); + let text = roundtrip(&p); + let arc = text + .lines() + .find(|l| l.split_whitespace().any(|w| w == "G2" || w == "G3")) + .unwrap_or_else(|| panic!("missing arc {plane:?} {sense:?} full={full} rise={rise}\n{text}")); + let offsets = match plane { + Plane::Xy => "I-2 J0", + Plane::Zx => "I0 K-2", + Plane::Yz => "J-2 K0", + }; + assert!(arc.contains(offsets), "{text}"); + assert!(!arc.split_whitespace().any(|w| w.starts_with('R')), "{text}"); + let read = read_back(&text, &post()).expect("read"); + let ReadMove::Arc { center: got_center, end: got_end, .. } = + read.iter().find(|m| matches!(m, ReadMove::Arc { .. })).expect("arc") + else { + panic!("expected arc") + }; + assert_eq!(*got_center, center); + assert_eq!(*got_end, end); + } + } + } + } +} + +#[test] +fn rotary_five_axis_and_turning_programs_are_rejected() { + for source in [include_str!("data/indexed.ir.json"), include_str!("data/wrap.ir.json"), include_str!("data/turn.ir.json")] { + let doc = Document::from_json(source).expect("fixture"); + let err = post_program(doc.program(), &post(), &PostOptions { arc_tolerance: 0.01 }).expect_err("unsupported family"); + assert!(err.to_string().contains("not supported"), "{err}"); + } + let tilted = Pose::new(Vec3::ZERO, Vec3::new(0.0, -1.0, 0.0).expect("axis")).expect("tilt"); + let p = program(vec![ClBlock::Motion(ClMotion::Rapid { to: tilted, feed: None })]); + let err = post_program(&p, &post(), &PostOptions { arc_tolerance: 0.01 }).expect_err("tilted tool"); + assert!(err.to_string().contains("tilted-plane"), "{err}"); + for axis in ["A90", "B90", "C90"] { + let text = format!("G21 G90 G17\nG0 X0 Y0 Z0 {axis}"); + assert!(read_back(&text, &post()).is_err()); + assert!(audit(&text, &post()).is_err()); + } +} + +proptest! { + #[test] + fn integer_and_decimal_p_differ_by_one_thousand(ms in 0u32..1_000_000) { + let text = format!("G4 P{ms}\nG4 P{ms}.0"); + prop_assert_eq!(dwell_seconds(&text, &post()), vec![f64::from(ms) / 1000.0, f64::from(ms)]); + prop_assert!(audit(&text, &post()).is_ok()); + } + + #[test] + fn written_dwell_rounds_to_milliseconds_without_changing_units(ticks in 0u32..1_000_000, trailing in any::()) { + let mut raw = raw_post(); + raw["trailing_zeros"] = json!(trailing); + let post = load_post(&raw.to_string()).expect("custom post"); + let seconds = f64::from(ticks) / 1000.0; + let text = write(&program(vec![ClBlock::Event(Event::Dwell { seconds })]), &post); + let parsed = dwell_seconds(&text, &post); + prop_assert!((parsed[0] - seconds).abs() < 1e-9); + prop_assert!(text.lines().filter(|l| l.starts_with("G4 ")).all(|l| l.contains('.'))); + } +} From dc461294651c586d1d9fb2568da0bd2a420f92ec Mon Sep 17 00:00:00 2001 From: Lead Bot Date: Thu, 12 Nov 2026 17:57:20 -0700 Subject: [PATCH 2/3] Changelog: Mach4 post and decimal-sensitive dwell Record the built-in mach4 post and dwell.decimal_seconds under Unreleased. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9a7c4..d15cc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- A built-in `mach4` three-axis mill/router post writes metric output with incremental IJK (`G91.1`) and `M9`/`M30` at the end. The opt-in `dwell.decimal_seconds` setting makes read-back treat an integer `G4 P` as milliseconds and a decimal `G4 P` as seconds, matching Mach4, and the writer always emits a decimal P so Mach4 reads seconds. Existing posts keep their dwell behavior and golden output. This output has not been proven on a machine. + ## 0.12.0 — 2026-11-12 Plasma improvements: optional G41/G42 from IR controller compensation, job-file lead-ins and lead-outs with the small-hole rule, automatic contour side and climb or conventional direction, and a narrower-than-kerf refusal plus a warning when a rapid crosses a freed slug (#163–#166). This output has not been proven on a machine. From ee1aca8a724b344ff87032cce297ecf22225a0ac Mon Sep 17 00:00:00 2001 From: Lead Bot Date: Thu, 12 Nov 2026 17:58:29 -0700 Subject: [PATCH 3/3] Add missing IR power field to Mach4 post tests ClMotion gained a per-move power field on main; set it to None so the Mach4 tests still compile and keep their existing dwell and incremental IJK coverage. --- crates/post/tests/mach4.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/post/tests/mach4.rs b/crates/post/tests/mach4.rs index b17f32e..1df0ca5 100644 --- a/crates/post/tests/mach4.rs +++ b/crates/post/tests/mach4.rs @@ -55,7 +55,7 @@ fn startup_tool_change_process_and_shutdown() { ClBlock::Event(Event::ProcessOn { process: ProcessKind::Spindle, params: ProcessParams::new().speed(Some(12000.0)).expect("speed") }), ClBlock::Event(Event::Coolant { mode: CoolantMode::Flood }), ClBlock::Event(Event::Coolant { mode: CoolantMode::Mist }), - ClBlock::Motion(ClMotion::Linear { to: pose([10.0, 0.0, 5.0]), feed: 100.0, role: MoveRole::Cut }), + ClBlock::Motion(ClMotion::Linear { to: pose([10.0, 0.0, 5.0]), feed: 100.0, role: MoveRole::Cut, power: None }), // Intentionally leave the spindle and coolant on: program shutdown must stop both. ]); let text = roundtrip(&p); @@ -188,6 +188,7 @@ fn arcs_circles_and_helices_use_incremental_ijk_in_every_plane() { sense, feed: 100.0, role: MoveRole::Cut, + power: None, } } else { ClMotion::Helix { @@ -198,6 +199,7 @@ fn arcs_circles_and_helices_use_incremental_ijk_in_every_plane() { sweep: if full { std::f64::consts::TAU } else { std::f64::consts::FRAC_PI_2 }, feed: 100.0, role: MoveRole::Cut, + power: None, } }; let p = program(vec![rapid(map(12.0, 20.0, 3.0)), ClBlock::Motion(motion)]);