@chestnutlabs/gcode-parser 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,9 @@ export interface SegmentRecord {
28
28
  tool: number;
29
29
  layer: number;
30
30
  srcByte: number;
31
+ /** Current value of each requested opt-in modal channel (DD-012 D3), keyed by channel id.
32
+ * Missing/undefined entries store NaN — an honest "no value here", never a fabricated 0. */
33
+ modal?: Record<string, number>;
31
34
  }
32
35
  export interface FinalChannels {
33
36
  count: number;
@@ -45,6 +48,9 @@ export interface FinalChannels {
45
48
  feature: Uint8Array;
46
49
  object: Uint32Array;
47
50
  srcByte: Uint32Array;
51
+ /** Opt-in modal channels (DD-012 D3), present only when requested at construction. Each is a
52
+ * Float32 column of length `count`; an unset value is NaN, never a fabricated 0. */
53
+ modal?: Record<string, Float32Array>;
48
54
  }
49
55
  export declare class SegmentWriter {
50
56
  private readonly maxBufferBytes;
@@ -68,7 +74,13 @@ export declare class SegmentWriter {
68
74
  private accountedBytes;
69
75
  /** Extra live bytes the caller wants included in the budget (line buffer, warnings). */
70
76
  private externalBytes;
71
- constructor(maxBufferBytes: number);
77
+ /** Opt-in modal channel ids (DD-012 D3), stable order; empty for a default parse. */
78
+ private readonly modalNames;
79
+ /** Growable Float32 columns, parallel to `modalNames`. */
80
+ private modalArrays;
81
+ /** Bytes per segment across the core set plus any requested modal columns. */
82
+ private readonly bytesPerSegment;
83
+ constructor(maxBufferBytes: number, modalChannels?: readonly string[]);
72
84
  get count(): number;
73
85
  get accounted(): number;
74
86
  /** Report additional live working bytes (e.g., the line-drain buffer) into the budget. */
@@ -82,6 +94,9 @@ export declare class SegmentWriter {
82
94
  setLayerRange(start: number, end: number, layerIndex: number): void;
83
95
  /** Zero the whole layer channel (non-planar clearing semantics). */
84
96
  clearLayers(): void;
97
+ /** Build the optional `modal` output record (DD-012 D3), or `{}` when no channels were requested
98
+ * so a default parse yields no `modal` key at all. `view` slices/subarrays each column to count. */
99
+ private modalOutput;
85
100
  /**
86
101
  * Copy a segment range into fresh right-sized arrays (progressive-preview
87
102
  * snapshots, DD-004 §5.4 / issue #60). Budget-checked: the copies coexist with
@@ -1 +1 @@
1
- {"version":3,"file":"growable.d.ts","sourceRoot":"","sources":["../src/growable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,cAAc,EAAE,MAAM;aACtB,cAAc,EAAE,MAAM;aACtB,cAAc,EAAE,MAAM;gBAFtB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM;CAOzC;AAuBD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,CAAC,EAAE,YAAY,CAAC;IAChB,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,CAAC;CACtB;AAID,qBAAa,aAAa;IAwBZ,OAAO,CAAC,QAAQ,CAAC,cAAc;IAvB3C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAK;IAEnB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,CAAC,CAAgB;IACzB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,OAAO,CAAe;IAE9B,wEAAwE;IACxE,OAAO,CAAC,cAAc,CAAK;IAC3B,wFAAwF;IACxF,OAAO,CAAC,aAAa,CAAK;gBAEG,cAAc,EAAE,MAAM;IAInD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,0FAA0F;IAC1F,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,QAAQ;IAwDhB,yFAAyF;IACzF,IAAI,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAsB5B,4FAA4F;IAC5F,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAInE,oEAAoE;IACpE,WAAW,IAAI,IAAI;IAInB;;;;;;OAMG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa;IAsBxD,qFAAqF;IACrF,QAAQ,IAAI,aAAa;CAsB1B"}
1
+ {"version":3,"file":"growable.d.ts","sourceRoot":"","sources":["../src/growable.ts"],"names":[],"mappings":"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"}
package/dist/growable.js CHANGED
@@ -37,7 +37,8 @@ const CHANNELS = [
37
37
  { name: 'object', bytesPerElement: 4 },
38
38
  { name: 'srcByte', bytesPerElement: 4 }
39
39
  ];
40
- const BYTES_PER_SEGMENT = CHANNELS.reduce((a, c) => a + c.bytesPerElement, 0); // 40 B core set
40
+ const CORE_BYTES_PER_SEGMENT = CHANNELS.reduce((a, c) => a + c.bytesPerElement, 0); // 40 B core set
41
+ const MODAL_BYTES_PER_ELEMENT = 4; // each opt-in modal channel is one Float32 column
41
42
  const INITIAL_CAPACITY = 4096;
42
43
  export class SegmentWriter {
43
44
  maxBufferBytes;
@@ -61,8 +62,16 @@ export class SegmentWriter {
61
62
  accountedBytes = 0;
62
63
  /** Extra live bytes the caller wants included in the budget (line buffer, warnings). */
63
64
  externalBytes = 0;
64
- constructor(maxBufferBytes) {
65
+ /** Opt-in modal channel ids (DD-012 D3), stable order; empty for a default parse. */
66
+ modalNames;
67
+ /** Growable Float32 columns, parallel to `modalNames`. */
68
+ modalArrays = [];
69
+ /** Bytes per segment across the core set plus any requested modal columns. */
70
+ bytesPerSegment;
71
+ constructor(maxBufferBytes, modalChannels = []) {
65
72
  this.maxBufferBytes = maxBufferBytes;
73
+ this.modalNames = modalChannels;
74
+ this.bytesPerSegment = CORE_BYTES_PER_SEGMENT + MODAL_BYTES_PER_ELEMENT * modalChannels.length;
66
75
  this.allocate(INITIAL_CAPACITY);
67
76
  }
68
77
  get count() {
@@ -83,7 +92,7 @@ export class SegmentWriter {
83
92
  }
84
93
  allocate(newCapacity) {
85
94
  // During a grow/compact, old and new capacity coexist until copies complete.
86
- const newBytes = newCapacity * BYTES_PER_SEGMENT;
95
+ const newBytes = newCapacity * this.bytesPerSegment;
87
96
  this.ensureBudget(newBytes);
88
97
  const nx0 = new Float32Array(newCapacity);
89
98
  const ny0 = new Float32Array(newCapacity);
@@ -99,6 +108,13 @@ export class SegmentWriter {
99
108
  const nfeature = new Uint8Array(newCapacity);
100
109
  const nobject = new Uint32Array(newCapacity);
101
110
  const nsrc = new Uint32Array(newCapacity);
111
+ // Opt-in modal columns grow in lockstep with the core set.
112
+ const nmodal = this.modalNames.map((_, k) => {
113
+ const a = new Float32Array(newCapacity);
114
+ if (this.capacity > 0)
115
+ a.set(this.modalArrays[k].subarray(0, this._count));
116
+ return a;
117
+ });
102
118
  if (this.capacity > 0) {
103
119
  nx0.set(this.x0.subarray(0, this._count));
104
120
  ny0.set(this.y0.subarray(0, this._count));
@@ -115,7 +131,8 @@ export class SegmentWriter {
115
131
  nobject.set(this.object.subarray(0, this._count));
116
132
  nsrc.set(this.srcByte.subarray(0, this._count));
117
133
  }
118
- const oldBytes = this.capacity * BYTES_PER_SEGMENT;
134
+ const oldBytes = this.capacity * this.bytesPerSegment;
135
+ this.modalArrays = nmodal;
119
136
  this.x0 = nx0;
120
137
  this.y0 = ny0;
121
138
  this.z0 = nz0;
@@ -153,6 +170,10 @@ export class SegmentWriter {
153
170
  this.feature[i] = 0;
154
171
  this.object[i] = 0;
155
172
  this.srcByte[i] = s.srcByte;
173
+ for (let k = 0; k < this.modalNames.length; k++) {
174
+ const v = s.modal?.[this.modalNames[k]];
175
+ this.modalArrays[k][i] = v === undefined ? NaN : v;
176
+ }
156
177
  this._count++;
157
178
  }
158
179
  /** Overwrite the layer channel for a segment range (used for post-hoc layer resolution). */
@@ -163,6 +184,16 @@ export class SegmentWriter {
163
184
  clearLayers() {
164
185
  this.layer.fill(0, 0, this._count);
165
186
  }
187
+ /** Build the optional `modal` output record (DD-012 D3), or `{}` when no channels were requested
188
+ * so a default parse yields no `modal` key at all. `view` slices/subarrays each column to count. */
189
+ modalOutput(view) {
190
+ if (this.modalNames.length === 0)
191
+ return {};
192
+ const modal = {};
193
+ for (let k = 0; k < this.modalNames.length; k++)
194
+ modal[this.modalNames[k]] = view(this.modalArrays[k]);
195
+ return { modal };
196
+ }
166
197
  /**
167
198
  * Copy a segment range into fresh right-sized arrays (progressive-preview
168
199
  * snapshots, DD-004 §5.4 / issue #60). Budget-checked: the copies coexist with
@@ -172,9 +203,10 @@ export class SegmentWriter {
172
203
  */
173
204
  snapshotRange(start, end) {
174
205
  const count = Math.max(0, end - start);
175
- this.ensureBudget(count * BYTES_PER_SEGMENT);
206
+ this.ensureBudget(count * this.bytesPerSegment);
176
207
  return {
177
208
  count,
209
+ ...this.modalOutput((a) => a.slice(start, end)),
178
210
  x0: this.x0.slice(start, end),
179
211
  y0: this.y0.slice(start, end),
180
212
  z0: this.z0.slice(start, end),
@@ -198,6 +230,7 @@ export class SegmentWriter {
198
230
  }
199
231
  return {
200
232
  count: this._count,
233
+ ...this.modalOutput((a) => a.subarray(0, this._count)),
201
234
  x0: this.x0.subarray(0, this._count),
202
235
  y0: this.y0.subarray(0, this._count),
203
236
  z0: this.z0.subarray(0, this._count),
package/dist/parse.d.ts CHANGED
@@ -56,6 +56,21 @@ export interface ParseOptions {
56
56
  limits?: Partial<ParseLimits>;
57
57
  /** Layer-detection tolerance (inherited default 0.05). */
58
58
  minLayerThreshold?: number;
59
+ /**
60
+ * Firmware hint (DD-010 D2): does `G90`/`G91` also set the *extruder* mode? Marlin/Klipper —
61
+ * `true`; RepRapFirmware treats E independently — `false`. Default `false` (RRF/unknown-conservative).
62
+ * Only consulted while no `M82`/`M83` has latched the E-mode; supplied by the dialect layer / caller
63
+ * that has confidently classified the firmware. The byte-exact engine never sniffs firmware itself.
64
+ */
65
+ extruderFollowsPositioning?: boolean;
66
+ /**
67
+ * Opt-in modal channels to capture per segment (DD-012 D3, #189). Each requested id adds one
68
+ * Float32 column to the IR (`segments.modal[id]`); a default parse requests none and pays nothing,
69
+ * so FDM output is unchanged. Supported id: `'toolPower'` — the modal spindle/laser `S` value while
70
+ * a tool is engaged (`M3`/`M4`), NaN when the tool is off. Presentation (Watts vs RPM) is a dialect
71
+ * label, not a separate channel (DD-012 D4). Fan/temp/accel etc. (#180) reuse the same mechanism.
72
+ */
73
+ modalChannels?: readonly string[];
59
74
  /**
60
75
  * DD-005 §4.3 read-only hooks: observe comments/commands during the parse.
61
76
  * Inert when unset (one branch per line); they cannot alter lexing, dispatch,
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
package/dist/parse.js CHANGED
@@ -38,29 +38,61 @@ export const DEFAULT_LIMITS = {
38
38
  };
39
39
  const LAYER_TOLERANCE = 0.05;
40
40
  const UNRESOLVED_LAYER = 0xffffffff;
41
+ /** Opt-in modal channels this engine can capture (DD-012 D3, #189). Requested via
42
+ * `ParseOptions.modalChannels`; unknown ids are ignored with a warning. Extend as later phases
43
+ * (spindle/laser already covered by `toolPower`; #180's fan/temp/accel land here next). */
44
+ const SUPPORTED_MODAL_CHANNELS = new Set(['toolPower']);
41
45
  const SPLIT_LETTERS = /([a-zA-Z])/g;
42
- /** Port of the inherited lexer: trim, comment split on ';', letter/value pairs. */
46
+ /**
47
+ * Lex a line into its G/M/T command words plus the axis/param words. Real CNC G-code routinely puts
48
+ * several commands on one line and prefixes lines with `N` line numbers; both are handled here.
49
+ * `N`-words (line numbers) are dropped, and non-command letters become params. FDM lines have a single
50
+ * command word, so `codes = [thatCommand]` and behavior is byte-identical to the inherited lexer.
51
+ */
43
52
  function lexLine(line) {
44
- const input = line.trim();
45
- const cmd = input.split(';')[0];
46
- const parts = cmd
53
+ const body = line.trim().split(';')[0];
54
+ const parts = body
47
55
  .split(SPLIT_LETTERS)
48
56
  .slice(1)
49
57
  .map((s) => s.trim());
50
- const gcode = !parts.length ? '' : `${parts[0]?.toLowerCase()}${Number(parts[1])}`;
51
- // Faithful replication of the inherited parseParams reduce (odd indices are values):
58
+ const codes = [];
52
59
  const params = {};
53
- const rest = parts.slice(2);
54
- for (let idx = 0; idx < rest.length; idx++) {
55
- if (idx % 2 === 0)
56
- continue;
57
- const key = rest[idx - 1].toLowerCase();
58
- const code = key.charCodeAt(0);
59
- if ((code >= 97 && code <= 122) || (code >= 65 && code <= 90)) {
60
- params[key] = parseFloat(rest[idx]);
60
+ let sawCommand = false;
61
+ for (let i = 0; i + 1 < parts.length; i += 2) {
62
+ const key = parts[i].toLowerCase();
63
+ const cc = key.charCodeAt(0);
64
+ if (cc < 97 || cc > 122)
65
+ continue; // not a letter
66
+ const value = parts[i + 1];
67
+ // A real G-code word is a letter followed by a NUMBER. This guard rejects letters embedded in
68
+ // extended-command words (Klipper `EXCLUDE_OBJECT … POLYGON=…`, `CENTER=`, `NAME=`) where a letter
69
+ // is followed by another letter or `=…` — the inherited lexer skipped these by only reading the
70
+ // first word; scanning every letter must not resurrect them as commands/params.
71
+ const num = value.trim() === '' ? NaN : Number(value);
72
+ if (key === 'g' || key === 'm') {
73
+ if (Number.isFinite(num)) {
74
+ codes.push(`${key}${num}`); // e.g. 'g1', 'm3' — leading zeros normalized
75
+ sawCommand = true;
76
+ }
77
+ }
78
+ else if (key === 't' && !sawCommand) {
79
+ // `T` is a tool-select command only as the LEADING command word (`T1 M6`). After an M-code it is
80
+ // that code's parameter, NOT a tool change — e.g. Marlin `M486 T<count>`, `M104 T<tool> S<temp>`.
81
+ if (Number.isFinite(num)) {
82
+ codes.push(`t${num}`);
83
+ sawCommand = true;
84
+ }
85
+ }
86
+ else if (key === 'n') {
87
+ // Line number — not a command, not a motion word. Ignored (fixes Fanuc/Mach/TinyG files).
88
+ }
89
+ else {
90
+ const p = parseFloat(value);
91
+ if (Number.isFinite(p))
92
+ params[key] = p;
61
93
  }
62
94
  }
63
- return { gcode, params };
95
+ return { codes, params };
64
96
  }
65
97
  /** Internal: build an engine over decoded text (streaming passes '' and feeds lines itself). */
66
98
  export function createEngine(input, opts) {
@@ -92,7 +124,18 @@ export function createEngine(input, opts) {
92
124
  warnings.set(code, { code, message, severity: 'warn', srcByte, count: 1 });
93
125
  }
94
126
  };
95
- const writer = new SegmentWriter(limits.maxBufferBytes);
127
+ // Opt-in modal channels (DD-012 D3): dedupe, keep supported, warn (once) on any unknown id.
128
+ const requestedModal = [];
129
+ for (const id of opts.modalChannels ?? []) {
130
+ if (!SUPPORTED_MODAL_CHANNELS.has(id)) {
131
+ warn('modal-channel-unsupported', `Ignoring unsupported modal channel '${id}'.`);
132
+ }
133
+ else if (!requestedModal.includes(id)) {
134
+ requestedModal.push(id);
135
+ }
136
+ }
137
+ const wantToolPower = requestedModal.includes('toolPower');
138
+ const writer = new SegmentWriter(limits.maxBufferBytes, requestedModal);
96
139
  // Machine state (port of State.initial + Interpreter fields).
97
140
  let sx = 0;
98
141
  let sy = 0;
@@ -101,6 +144,79 @@ export function createEngine(input, opts) {
101
144
  let units = 'mm';
102
145
  let unitsSeen = false;
103
146
  let modalFeed = NaN;
147
+ // Motion-model modal state (DD-010 E10 phase 1). `xyzAbsolute` (G90/G91) and the extruder mode
148
+ // resolve the effective per-move E delta and the extrude/travel classification. Defaults are the
149
+ // firmware power-on convention: absolute XYZ, absolute E. `eModeExplicit` is set only by M82/M83;
150
+ // `lastE` is the running absolute-E datum (also reset by `G92 E`).
151
+ let xyzAbsolute = true;
152
+ let positioningSeen = false; // any G90/G91 observed → positioningMode 'known'
153
+ let eModeExplicit = null; // M82/M83; null = infer
154
+ let lastE = 0;
155
+ const extruderFollowsPositioning = opts.extruderFollowsPositioning === true;
156
+ // Arc-plane modal state (DD-010 D3, #157, E10 phase 2). G17=XY (default, power-on),
157
+ // G18=XZ, G19=YZ. Arc flattening runs in the active plane; the through axis ramps linearly.
158
+ let arcPlane = 'xy';
159
+ let arcPlaneSeen = false; // any G17/G18/G19 observed → arcPlanes 'known'
160
+ // Coordinate systems (DD-010 D4, #158, E10 phase 3). The IR stays in the logical/work frame
161
+ // (Option A): the emitted position is `commanded + activeWcsOffset + g92off`. FDM slicer files use
162
+ // the identity WCS with no G92, so every offset is 0 → the corpus is byte-identical. G54–G59 select
163
+ // a per-system offset (settable via G10 L2/L20); G92 X/Y/Z shifts the datum without motion; G53 is a
164
+ // one-shot machine-coordinate bypass for the following move.
165
+ let activeWcs = 0; // 0..5 → G54..G59; G54 (identity) is the power-on default
166
+ const wcsOffsets = Array.from({ length: 6 }, () => ({ x: 0, y: 0, z: 0 }));
167
+ let g92x = 0;
168
+ let g92y = 0;
169
+ let g92z = 0;
170
+ let coordSystemSeen = false; // any G53/G54–G59/G92-XYZ/G10 → coordinateSystem 'known'
171
+ let g53OneShot = false; // next move ignores the work offset (machine coordinates)
172
+ // Tool-state modal (DD-012 D2/D4, #189). Spindle/laser on (M3/M4) engages the tool; M5 disengages.
173
+ // A FEED move (G1/G2/G3) with no extrusion E while the tool is engaged is a Cut (productive); a
174
+ // rapid (G0) stays Travel even with the tool engaged (refined #189). FDM slices never issue M3/M4,
175
+ // so toolEngaged stays false and their classification is byte-identical.
176
+ let toolEngaged = false;
177
+ let toolStateSeen = false; // any M3/M4/M5 observed → cutMoves capability 'known'
178
+ let modalS = NaN; // current modal spindle/laser S value (power / RPM); NaN until first seen (#189)
179
+ let modalMotion = null; // last G0–G3 mode for modal-motion lines (#189)
180
+ // Canned drilling cycle state (DD-012 phase 2, #189). Z/R/Q/initial-plane are captured when the
181
+ // cycle is defined and retained across modal (bare X/Y) repeats until G80 or a G0–G3 cancels it.
182
+ let cannedCycle = null;
183
+ let cannedZ = 0; // final drill depth (absolute)
184
+ let cannedR = 0; // retract / reference plane (absolute Z)
185
+ let cannedQ = 0; // peck increment (G83)
186
+ let cannedInitialZ = 0; // Z when the cycle activated — the G98 retract plane
187
+ let cannedRetractInitial = true; // G98 (retract to initial Z) vs G99 (retract to R); modal, default G98
188
+ let cannedCyclesSeen = false; // any G81/G82/G83 expanded → cannedCycles capability 'known'
189
+ // Per-axis position certainty (DD-010 D4 amendment, #158). A G31 probe endpoint is reached at
190
+ // RUNTIME (workpiece contact), not the commanded value, so it marks the probed axes uncertain. A
191
+ // following G92 then RESYNCS the logical frame at the datum (rather than datum-shifting a stale
192
+ // position); an absolute move re-establishes certainty. Independent of the work offsets above.
193
+ let certainX = true;
194
+ let certainY = true;
195
+ let certainZ = true;
196
+ const offX = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].x + g92x);
197
+ const offY = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].y + g92y);
198
+ const offZ = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].z + g92z);
199
+ /**
200
+ * Effective per-move E delta from the modal extruder mode (DD-010 D1/D2 precedence:
201
+ * explicit M82/M83 → firmware-conditioned G90/G91 → absolute default). Updates `lastE`.
202
+ * For relative E the delta is the raw word (byte-identical to the pre-E10 engine for the
203
+ * M83 corpus); for absolute E it is `eParam − lastE`.
204
+ */
205
+ const resolveEDelta = (eParam) => {
206
+ if (eParam === undefined)
207
+ return 0;
208
+ const absolute = eModeExplicit !== null ? eModeExplicit === 'absolute' : extruderFollowsPositioning ? xyzAbsolute : true; // firmware-neutral default: absolute, disclosed 'inferred'
209
+ const delta = absolute ? eParam - lastE : eParam;
210
+ lastE = absolute ? eParam : lastE + eParam;
211
+ return delta;
212
+ };
213
+ /**
214
+ * Logical (work-frame) target for one axis given the positioning mode (G90/G91) and the active
215
+ * work offset (DD-010 D4). Absolute: `word + off`; relative: `cur + word` (the offset is already
216
+ * baked into `cur`); an absent word holds the current logical position. With `off === 0` (the
217
+ * identity WCS + no G92) this reduces to the pre-#158 `word ?? cur`, so the corpus is byte-identical.
218
+ */
219
+ const nextAxis = (cur, word, off = 0) => xyzAbsolute ? (word !== undefined ? word + off : cur) : cur + (word ?? 0);
104
220
  // Floating origin (DD-001 §4.6): fixed at the first emitted segment's start.
105
221
  let originSet = false;
106
222
  let ox = 0;
@@ -158,7 +274,10 @@ export function createEngine(input, opts) {
158
274
  kind,
159
275
  tool: path?.tool ?? tool,
160
276
  layer: UNRESOLVED_LAYER,
161
- srcByte: currentSrcByte
277
+ srcByte: currentSrcByte,
278
+ // toolPower is the modal S while a tool is engaged; NaN when off (M5 / never engaged) — an
279
+ // honest "no cutting power here", not a fabricated 0. Only built when requested (DD-012 D3).
280
+ modal: wantToolPower ? { toolPower: toolEngaged ? modalS : NaN } : undefined
162
281
  });
163
282
  }
164
283
  catch (err) {
@@ -252,21 +371,27 @@ export function createEngine(input, opts) {
252
371
  prevY = sy;
253
372
  prevZ = sz;
254
373
  };
255
- const g0 = (p) => {
374
+ // Map a resolved path type to its MoveKind bit (DD-012 D2). `cut` only ever arises on a feed move
375
+ // when the tool is engaged and E is absent, so FDM (never `cut`) is byte-identical.
376
+ const kindForPath = (t) => t === 'extrusion' ? MoveKind.Extrude : t === 'cut' ? MoveKind.Cut : MoveKind.Travel;
377
+ const g0 = (p, rapid) => {
256
378
  const { x, y, z, e, f } = p;
379
+ // Classify on the true per-move delta, not the raw E word (DD-010 D1). For the M83/relative
380
+ // corpus the delta equals the word, so output is byte-identical to the pre-E10 engine.
381
+ const eDelta = resolveEDelta(e);
257
382
  if (x === undefined && y === undefined && z === undefined) {
258
- if (e > 0)
383
+ if (eDelta > 0)
259
384
  stats.retractions++;
260
- else if (e < 0)
385
+ else if (eDelta < 0)
261
386
  stats.deretractions++;
262
387
  else if (f !== undefined)
263
388
  stats.feedrateChanges++;
264
- if (e !== undefined && e !== 0) {
389
+ if (e !== undefined && eDelta !== 0) {
265
390
  retractionEvents.push({
266
391
  x: sx,
267
392
  y: sy,
268
393
  z: sz,
269
- kind: e < 0 ? 'retract' : 'unretract',
394
+ kind: eDelta < 0 ? 'retract' : 'unretract',
270
395
  srcByte: currentSrcByte,
271
396
  segIndex: writer.count
272
397
  });
@@ -280,47 +405,78 @@ export function createEngine(input, opts) {
280
405
  stats.points++;
281
406
  if (f !== undefined)
282
407
  modalFeed = f;
283
- const pathType = e > 0 ? 'extrusion' : 'travel';
408
+ // A rapid (G0) is a non-cutting positioning traverse — Travel even while the tool is engaged
409
+ // (a router keeps its spindle on during rapids; a GRBL-laser gates the beam off during G0). Only
410
+ // a FEED move (G1/G2/G3) with the tool engaged and no extrusion is a Cut (DD-012 D2, refined #189).
411
+ const pathType = eDelta > 0 ? 'extrusion' : toolEngaged && !rapid ? 'cut' : 'travel';
284
412
  if (path === null || path.type !== pathType) {
285
413
  breakPath(pathType);
286
414
  }
287
- if (e > 0)
288
- stats.extrusionDistance += e;
289
- sx = x ?? sx;
290
- sy = y ?? sy;
291
- sz = z ?? sz;
292
- return emitSegment(sx, sy, sz, e ?? 0, pathType === 'extrusion' ? MoveKind.Extrude : MoveKind.Travel);
415
+ if (eDelta > 0)
416
+ stats.extrusionDistance += eDelta;
417
+ sx = nextAxis(sx, x, offX());
418
+ sy = nextAxis(sy, y, offY());
419
+ sz = nextAxis(sz, z, offZ());
420
+ // An absolute commanded move re-establishes certainty for the axes it names (#158).
421
+ if (xyzAbsolute) {
422
+ if (x !== undefined)
423
+ certainX = true;
424
+ if (y !== undefined)
425
+ certainY = true;
426
+ if (z !== undefined)
427
+ certainZ = true;
428
+ }
429
+ g53OneShot = false; // one-shot machine-coordinate bypass consumed by this move
430
+ return emitSegment(sx, sy, sz, eDelta, kindForPath(pathType));
293
431
  };
294
432
  const g2 = (p, cw) => {
295
- const { x, y, z, e, f } = p;
296
- let { i, j, r } = p;
433
+ const { e, f } = p;
434
+ let { r } = p;
297
435
  if (f !== undefined)
298
436
  modalFeed = f;
299
- const pathType = e ? 'extrusion' : 'travel';
437
+ // E is delta-based (DD-010 D1) so M82 arcs classify correctly and `lastE` stays consistent with g0.
438
+ const eDelta = resolveEDelta(e);
439
+ const pathType = eDelta ? 'extrusion' : toolEngaged ? 'cut' : 'travel';
300
440
  if (path === null || path.type !== pathType) {
301
441
  breakPath(pathType);
302
442
  }
303
- if (e > 0)
304
- stats.extrusionDistance += e;
443
+ if (eDelta > 0)
444
+ stats.extrusionDistance += eDelta;
445
+ // Absolute targets, honoring G90/G91 (DD-010 D2 — the deferred G91-arc geometry lands here, #157)
446
+ // and the active work offset (DD-010 D4, #158). I/J/K are ALWAYS current-relative center offsets.
447
+ const tx = nextAxis(sx, p.x, offX());
448
+ const ty = nextAxis(sy, p.y, offY());
449
+ const tz = nextAxis(sz, p.z, offZ());
450
+ g53OneShot = false; // one-shot machine-coordinate bypass consumed by this move
451
+ // Select the active arc plane (DD-010 D3): (sa,sb)→(ta,tb) is the in-plane arc with center offsets
452
+ // (oa,ob); sc→tc is the through axis (linear ramp). XY reproduces the pre-#157 math exactly, so the
453
+ // XY-arc corpus stays byte-identical; only G18/G19 arcs newly flatten in the correct plane.
454
+ const [sa, sb, sc, ta, tb, tc, oa, ob] = arcPlane === 'xz'
455
+ ? [sx, sz, sy, tx, tz, ty, p.i, p.k]
456
+ : arcPlane === 'yz'
457
+ ? [sy, sz, sx, ty, tz, tx, p.j, p.k]
458
+ : [sx, sy, sz, tx, ty, tz, p.i, p.j];
459
+ let ia = oa;
460
+ let ib = ob;
305
461
  if (r) {
306
- const deltaX = x - sx; // assume abs mode (inherited)
307
- const deltaY = y - sy;
308
- const minR = Math.sqrt(Math.pow(deltaX / 2, 2) + Math.pow(deltaY / 2, 2));
462
+ const deltaA = ta - sa;
463
+ const deltaB = tb - sb;
464
+ const minR = Math.sqrt(Math.pow(deltaA / 2, 2) + Math.pow(deltaB / 2, 2));
309
465
  r = Math.max(r, minR);
310
- const dSquared = Math.pow(deltaX, 2) + Math.pow(deltaY, 2);
466
+ const dSquared = Math.pow(deltaA, 2) + Math.pow(deltaB, 2);
311
467
  const hSquared = Math.pow(r, 2) - dSquared / 4;
312
468
  let hDivD = Math.sqrt(hSquared / dSquared);
313
469
  if ((cw && r < 0.0) || (!cw && r > 0.0))
314
470
  hDivD = -hDivD;
315
- i = deltaX / 2 + deltaY * hDivD;
316
- j = deltaY / 2 - deltaX * hDivD;
317
- }
318
- const wholeCircle = sx == x && sy == y;
319
- const centerX = sx + i;
320
- const centerY = sy + j;
321
- const arcRadius = Math.sqrt(i * i + j * j);
322
- const arcCurrentAngle = Math.atan2(-j, -i);
323
- const finalTheta = Math.atan2(y - centerY, x - centerX);
471
+ ia = deltaA / 2 + deltaB * hDivD;
472
+ ib = deltaB / 2 - deltaA * hDivD;
473
+ }
474
+ const wholeCircle = sa == ta && sb == tb;
475
+ const centerA = sa + ia;
476
+ const centerB = sb + ib;
477
+ const arcRadius = Math.sqrt(ia * ia + ib * ib);
478
+ const arcCurrentAngle = Math.atan2(-ib, -ia);
479
+ const finalTheta = Math.atan2(tb - centerB, ta - centerA);
324
480
  let totalArc;
325
481
  if (wholeCircle) {
326
482
  totalArc = 2 * Math.PI;
@@ -337,26 +493,70 @@ export function createEngine(input, opts) {
337
493
  totalSegments = 1;
338
494
  let arcAngleIncrement = totalArc / totalSegments;
339
495
  arcAngleIncrement *= cw ? -1 : 1;
340
- const zDist = sz - (z || sz);
341
- const zStep = zDist / totalSegments;
342
- const kind = (pathType === 'extrusion' ? MoveKind.Extrude : MoveKind.Travel) | MoveKind.ArcSegment;
343
- const eachE = e !== undefined ? e / Math.max(1, Math.ceil(totalSegments)) : 0;
344
- let px;
345
- let py;
346
- let pz = sz;
496
+ // Through-axis linear ramp — preserves the inherited step (byte-identical for planar XY arcs,
497
+ // where the through axis is unchanged so the ramp is flat).
498
+ const cStep = (sc - tc) / totalSegments;
499
+ const kind = kindForPath(pathType) | MoveKind.ArcSegment;
500
+ const eachE = e !== undefined ? eDelta / Math.max(1, Math.ceil(totalSegments)) : 0;
501
+ // Map an in-plane point (pa,pb) + through-axis pc back to (x,y,z) for the active plane.
502
+ const emitArc = (pa, pb, pcv) => {
503
+ const [px, py, pz] = arcPlane === 'xz' ? [pa, pcv, pb] : arcPlane === 'yz' ? [pcv, pa, pb] : [pa, pb, pcv];
504
+ return emitSegment(px, py, pz, eachE, kind);
505
+ };
506
+ let pc = sc;
347
507
  let currentAngle = arcCurrentAngle;
348
508
  for (let moveIdx = 0; moveIdx < totalSegments - 1; moveIdx++) {
349
509
  currentAngle += arcAngleIncrement;
350
- px = centerX + arcRadius * Math.cos(currentAngle);
351
- py = centerY + arcRadius * Math.sin(currentAngle);
352
- pz += zStep;
353
- if (!emitSegment(px, py, pz, eachE, kind))
510
+ const pa = centerA + arcRadius * Math.cos(currentAngle);
511
+ const pb = centerB + arcRadius * Math.sin(currentAngle);
512
+ pc += cStep;
513
+ if (!emitArc(pa, pb, pc))
354
514
  return false;
355
515
  }
356
- sx = x || sx;
357
- sy = y || sy;
358
- sz = z || sz;
359
- return emitSegment(sx, sy, sz, eachE, kind);
516
+ sx = tx;
517
+ sy = ty;
518
+ sz = tz;
519
+ return emitArc(ta, tb, tc);
520
+ };
521
+ // Canned drilling cycle (DD-012 phase 2, #189): expand G81/G82/G83 into explicit sub-moves so the
522
+ // holes are real geometry (today they emit nothing). Rapids are Travel; the plunge(s) are Cut. Z/R/Q
523
+ // and the initial plane are captured when the cycle is defined and retained across modal repeats.
524
+ const runCannedCycle = (p) => {
525
+ // 1. Rapid to the hole XY at the current Z.
526
+ sx = nextAxis(sx, p.x, offX());
527
+ sy = nextAxis(sy, p.y, offY());
528
+ g53OneShot = false;
529
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Travel))
530
+ return false;
531
+ // 2. Rapid Z down to the R (reference) plane.
532
+ sz = cannedR;
533
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Travel))
534
+ return false;
535
+ // 3. Feed to depth — one plunge (G81/G82) or a peck loop (G83). Productive → Cut.
536
+ if (cannedCycle === 'g83' && cannedQ > 0 && cannedR > cannedZ) {
537
+ let depth = cannedR;
538
+ while (depth > cannedZ) {
539
+ depth = Math.max(cannedZ, depth - cannedQ);
540
+ sz = depth;
541
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Cut))
542
+ return false; // feed down one peck
543
+ if (depth > cannedZ) {
544
+ sz = cannedR;
545
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Travel))
546
+ return false; // rapid retract to R between pecks
547
+ }
548
+ }
549
+ }
550
+ else {
551
+ sz = cannedZ;
552
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Cut))
553
+ return false; // single plunge
554
+ }
555
+ // 4. Retract (rapid) to the initial plane (G98) or the R plane (G99).
556
+ sz = cannedRetractInitial ? cannedInitialZ : cannedR;
557
+ if (!emitSegment(sx, sy, sz, 0, MoveKind.Travel))
558
+ return false;
559
+ return true;
360
560
  };
361
561
  const onComment = opts.onComment;
362
562
  const onCommand = opts.onCommand;
@@ -374,56 +574,266 @@ export function createEngine(input, opts) {
374
574
  onComment(rawLine.slice(ci + 1), offset);
375
575
  }
376
576
  const cmd = lexLine(rawLine);
377
- if (cmd.gcode === '')
378
- return;
379
- stats.commands++;
380
- if (onCommand !== undefined) {
381
- onCommand({ gcode: cmd.gcode, params: cmd.params, rawLine, srcByte: offset, segIndex: writer.count });
577
+ // Skip only genuinely empty lines. A line with no lexable words but real content (a dialect
578
+ // extended command like `EXCLUDE_OBJECT_START NAME=cube`, a macro) must NOT be dropped — the
579
+ // observer below still forwards its raw line. The body split runs only when there are no words.
580
+ if (cmd.codes.length === 0 && Object.keys(cmd.params).length === 0 && rawLine.split(';')[0].trim() === '') {
581
+ return; // blank or comment-only line
382
582
  }
583
+ stats.commands++;
584
+ // Modal S/F latch from params on ANY line — including bare `S1000` / `F600` lines that carry no
585
+ // command word (common in GRBL-laser output). S is captured for toolPower only when requested.
586
+ if (wantToolPower && cmd.params.s !== undefined)
587
+ modalS = cmd.params.s;
588
+ if (cmd.params.f !== undefined)
589
+ modalFeed = cmd.params.f;
383
590
  let ok = true;
384
- switch (cmd.gcode) {
385
- case 'g0':
386
- case 'g1':
387
- ok = g0(cmd.params);
388
- break;
389
- case 'g2':
390
- ok = g2(cmd.params, true);
391
- break;
392
- case 'g3':
393
- ok = g2(cmd.params, false);
394
- break;
395
- case 'g20':
396
- units = 'in';
397
- unitsSeen = true;
398
- break;
399
- case 'g21':
400
- units = 'mm';
401
- unitsSeen = true;
402
- break;
403
- case 'g28':
404
- sx = 0;
405
- sy = 0;
406
- sz = 0;
407
- break;
408
- case 't0':
409
- case 't1':
410
- case 't2':
411
- case 't3':
412
- case 't4':
413
- case 't5':
414
- case 't6':
415
- case 't7':
416
- tool = Number(cmd.gcode.slice(1));
417
- break;
418
- case 'm600':
419
- // Manual filament swap = color boundary (#147). A marker, not motion — do
420
- // NOT break/finish the current path; record position (current head), the
421
- // next segment index (slot boundary), and the active tool for provenance.
422
- colorChangeEvents.push({ x: sx, y: sy, z: sz, segIndex: writer.count, srcByte: offset, tool });
423
- break;
424
- default:
425
- warn('unsupported-command', `unsupported command '${cmd.gcode}' preserved as metadata`, offset);
591
+ if (cmd.codes.length === 0) {
592
+ // No G/M/T command word. This is either a dialect extended command (EXCLUDE_OBJECT_*, macros —
593
+ // matched on the raw line, DD-005) or a bare motion/param line. Forward the raw line to the
594
+ // observer so dialects still see it, then continue the active modal motion group if the line
595
+ // carries motion axes (the CNC/LinuxCNC modal form; FDM always emits the G word, so this never
596
+ // runs for FDM — byte-identical).
597
+ if (onCommand !== undefined) {
598
+ onCommand({ gcode: '', params: cmd.params, rawLine, srcByte: offset, segIndex: writer.count });
599
+ }
600
+ if (cmd.params.x !== undefined || cmd.params.y !== undefined || cmd.params.z !== undefined) {
601
+ if (cannedCycle !== null)
602
+ ok = runCannedCycle(cmd.params);
603
+ else if (modalMotion === 'g2' || modalMotion === 'g3')
604
+ ok = g2(cmd.params, modalMotion === 'g2');
605
+ else if (modalMotion === 'g0' || modalMotion === 'g1')
606
+ ok = g0(cmd.params, modalMotion === 'g0');
607
+ }
426
608
  }
609
+ else
610
+ for (const gcode of cmd.codes) {
611
+ if (onCommand !== undefined) {
612
+ onCommand({ gcode, params: cmd.params, rawLine, srcByte: offset, segIndex: writer.count });
613
+ }
614
+ switch (gcode) {
615
+ case 'g0':
616
+ case 'g1':
617
+ modalMotion = gcode;
618
+ cannedCycle = null; // a G0–G3 motion command cancels the canned-cycle modal group
619
+ ok = g0(cmd.params, gcode === 'g0');
620
+ break;
621
+ case 'g2':
622
+ modalMotion = 'g2';
623
+ cannedCycle = null;
624
+ ok = g2(cmd.params, true);
625
+ break;
626
+ case 'g3':
627
+ modalMotion = 'g3';
628
+ cannedCycle = null;
629
+ ok = g2(cmd.params, false);
630
+ break;
631
+ // Canned drilling cycles (DD-012 phase 2, #189). A fresh G81/G82/G83 captures Z/R/Q + the
632
+ // initial plane and drills the first hole; bare X/Y lines (cannedRepeat) drill more, retaining
633
+ // that state; G80 cancels; G98/G99 set the retract plane.
634
+ case 'g81':
635
+ case 'g82':
636
+ case 'g83':
637
+ cannedCyclesSeen = true;
638
+ cannedCycle = gcode;
639
+ cannedInitialZ = sz;
640
+ if (cmd.params.z !== undefined)
641
+ cannedZ = nextAxis(sz, cmd.params.z, offZ());
642
+ if (cmd.params.r !== undefined)
643
+ cannedR = nextAxis(sz, cmd.params.r, offZ());
644
+ if (cmd.params.q !== undefined)
645
+ cannedQ = Math.abs(cmd.params.q);
646
+ modalMotion = null;
647
+ ok = runCannedCycle(cmd.params);
648
+ break;
649
+ case 'g80':
650
+ cannedCycle = null;
651
+ break;
652
+ case 'g98':
653
+ cannedRetractInitial = true;
654
+ break;
655
+ case 'g99':
656
+ cannedRetractInitial = false;
657
+ break;
658
+ case 'g20':
659
+ units = 'in';
660
+ unitsSeen = true;
661
+ break;
662
+ case 'g21':
663
+ units = 'mm';
664
+ unitsSeen = true;
665
+ break;
666
+ case 'g28':
667
+ sx = 0;
668
+ sy = 0;
669
+ sz = 0;
670
+ break;
671
+ case 't0':
672
+ case 't1':
673
+ case 't2':
674
+ case 't3':
675
+ case 't4':
676
+ case 't5':
677
+ case 't6':
678
+ case 't7':
679
+ tool = Number(gcode.slice(1));
680
+ break;
681
+ case 'm600':
682
+ // Manual filament swap = color boundary (#147). A marker, not motion — do
683
+ // NOT break/finish the current path; record position (current head), the
684
+ // next segment index (slot boundary), and the active tool for provenance.
685
+ colorChangeEvents.push({ x: sx, y: sy, z: sz, segIndex: writer.count, srcByte: offset, tool });
686
+ break;
687
+ // Motion-model modal commands (DD-010 E10 phase 1).
688
+ case 'g90':
689
+ xyzAbsolute = true;
690
+ positioningSeen = true;
691
+ break;
692
+ case 'g91':
693
+ xyzAbsolute = false;
694
+ positioningSeen = true;
695
+ break;
696
+ // Arc-plane selection (DD-010 D3, #157, E10 phase 2).
697
+ case 'g17':
698
+ arcPlane = 'xy';
699
+ arcPlaneSeen = true;
700
+ break;
701
+ case 'g18':
702
+ arcPlane = 'xz';
703
+ arcPlaneSeen = true;
704
+ break;
705
+ case 'g19':
706
+ arcPlane = 'yz';
707
+ arcPlaneSeen = true;
708
+ break;
709
+ case 'm82':
710
+ eModeExplicit = 'absolute';
711
+ break;
712
+ case 'm83':
713
+ eModeExplicit = 'relative';
714
+ break;
715
+ // Tool-state modal (DD-012 D2/D4, #189). M3 (CW) / M4 (CCW) engage the spindle/laser;
716
+ // M5 disengages. Drives Cut-vs-Travel classification for non-extrusion moves. The `S` word
717
+ // (power/RPM) is a modal channel handled in a later phase; here we track only engagement.
718
+ case 'm3':
719
+ case 'm4':
720
+ toolEngaged = true;
721
+ toolStateSeen = true;
722
+ break;
723
+ case 'm5':
724
+ toolEngaged = false;
725
+ toolStateSeen = true;
726
+ break;
727
+ // Coordinate systems (DD-010 D4, #158, E10 phase 3).
728
+ case 'g53':
729
+ // One-shot: the following move is in machine coordinates (ignores the work offset).
730
+ g53OneShot = true;
731
+ coordSystemSeen = true;
732
+ break;
733
+ case 'g54':
734
+ case 'g55':
735
+ case 'g56':
736
+ case 'g57':
737
+ case 'g58':
738
+ case 'g59':
739
+ activeWcs = Number(gcode.slice(1)) - 54; // g54→0 … g59→5
740
+ coordSystemSeen = true;
741
+ break;
742
+ case 'g10': {
743
+ // Set a work-coordinate offset. L2 P<n>: set the offset directly. L20 P<n>: set it so the
744
+ // current position reads the given value in WCS n. P1→G54 … P6→G59 (default: active system).
745
+ const l = cmd.params.l;
746
+ const idx = cmd.params.p !== undefined ? Math.round(cmd.params.p) - 1 : activeWcs;
747
+ if (idx >= 0 && idx < 6 && (l === 2 || l === 20)) {
748
+ const w = wcsOffsets[idx];
749
+ const setAxis = (word, cur, axis) => {
750
+ if (word === undefined)
751
+ return;
752
+ w[axis] = l === 2 ? word : cur - word; // L20: offset = current logical − desired reading
753
+ };
754
+ setAxis(cmd.params.x, sx, 'x');
755
+ setAxis(cmd.params.y, sy, 'y');
756
+ setAxis(cmd.params.z, sz, 'z');
757
+ coordSystemSeen = true;
758
+ }
759
+ break;
760
+ }
761
+ case 'g31': {
762
+ // Probe move (DD-010 D4 amendment, #158): the endpoint is reached at RUNTIME (workpiece
763
+ // contact), NOT the commanded value. Do not advance the position or draw a fabricated probe
764
+ // move; mark the probed axes runtime-dependent so a following G92 resyncs the logical frame.
765
+ let probed = false;
766
+ if (cmd.params.x !== undefined) {
767
+ certainX = false;
768
+ probed = true;
769
+ }
770
+ if (cmd.params.y !== undefined) {
771
+ certainY = false;
772
+ probed = true;
773
+ }
774
+ if (cmd.params.z !== undefined) {
775
+ certainZ = false;
776
+ probed = true;
777
+ }
778
+ if (probed) {
779
+ warn('probe-position-runtime-dependent', 'G31 probe endpoint is determined at runtime; the probed axis is runtime-dependent until re-established (G92 or an absolute move).', offset);
780
+ }
781
+ break;
782
+ }
783
+ case 'g92': {
784
+ // Datum WITHOUT motion (DD-010 D4 + probe amendment, #158). `G92 E<v>` rebases the absolute-E
785
+ // origin (also used in phase 1). For X/Y/Z, when the position is KNOWN it is a datum SHIFT (the
786
+ // work offset is set so the current logical position reads <v>, preserving continuity). When the
787
+ // axis is runtime-dependent (post-probe) it is a logical RESYNC: the current logical position is
788
+ // declared to be <v>, the offset is reset, certainty is restored, and the current path is
789
+ // finalized so the next move starts a NEW frame at the datum — no fabricated move is drawn
790
+ // across the unknown probe result.
791
+ if (cmd.params.e !== undefined)
792
+ lastE = cmd.params.e;
793
+ let resync = false;
794
+ if (cmd.params.x !== undefined) {
795
+ if (certainX)
796
+ g92x = sx - cmd.params.x - wcsOffsets[activeWcs].x;
797
+ else {
798
+ sx = cmd.params.x + wcsOffsets[activeWcs].x;
799
+ g92x = 0;
800
+ certainX = true;
801
+ resync = true;
802
+ }
803
+ coordSystemSeen = true;
804
+ }
805
+ if (cmd.params.y !== undefined) {
806
+ if (certainY)
807
+ g92y = sy - cmd.params.y - wcsOffsets[activeWcs].y;
808
+ else {
809
+ sy = cmd.params.y + wcsOffsets[activeWcs].y;
810
+ g92y = 0;
811
+ certainY = true;
812
+ resync = true;
813
+ }
814
+ coordSystemSeen = true;
815
+ }
816
+ if (cmd.params.z !== undefined) {
817
+ if (certainZ)
818
+ g92z = sz - cmd.params.z - wcsOffsets[activeWcs].z;
819
+ else {
820
+ sz = cmd.params.z + wcsOffsets[activeWcs].z;
821
+ g92z = 0;
822
+ certainZ = true;
823
+ resync = true;
824
+ }
825
+ coordSystemSeen = true;
826
+ }
827
+ if (resync)
828
+ finishPath(); // start a new frame at the datum; no move connects across the probe
829
+ break;
830
+ }
831
+ default:
832
+ warn('unsupported-command', `unsupported command '${gcode}' preserved as metadata`, offset);
833
+ }
834
+ if (!ok)
835
+ break;
836
+ }
427
837
  if (!ok) {
428
838
  truncatedAtByte = offset;
429
839
  }
@@ -463,8 +873,33 @@ export function createEngine(input, opts) {
463
873
  featureRoles: 'unavailable',
464
874
  objects: 'unavailable',
465
875
  retractions: retractionEvents.length > 0 ? 'known' : 'unavailable',
466
- colorChanges: colorChangeEvents.length > 0 ? 'known' : 'unavailable'
876
+ colorChanges: colorChangeEvents.length > 0 ? 'known' : 'unavailable',
877
+ // Annotation-derived move kinds (DD-016, #182). The parser never sets these — they come from a
878
+ // slicer adapter's WIPE_START/END brackets (upgraded to 'known' via the sink). Seam has no
879
+ // per-move G-code signal and stays 'unavailable' (a future geometry-heuristic DD may change it).
880
+ wipeMoves: 'unavailable',
881
+ seamMoves: 'unavailable',
882
+ // Non-extrusion productive moves (DD-012 D2, #189). 'known' once a tool-state modal (M3/M4/M5)
883
+ // was seen — i.e. this is a CNC/laser/plotter file whose Cut moves are meaningful; 'unavailable'
884
+ // for FDM (no tool-state), where every move is Extrude/Travel and Cut is never set.
885
+ cutMoves: toolStateSeen ? 'known' : 'unavailable',
886
+ // Canned drilling cycles expanded to geometry (DD-012 phase 2, #189). 'known' once a G81/G82/G83
887
+ // was seen (holes are real segments), else 'unavailable' (no cycles / FDM).
888
+ cannedCycles: cannedCyclesSeen ? 'known' : 'unavailable',
889
+ // Motion-model modes (DD-010 E10 phase 1). 'known' when the governing command was seen
890
+ // (M82/M83, or a firmware-known G90/G91 for E); 'inferred' when defaulted (absolute).
891
+ extrusionMode: eModeExplicit !== null || (extruderFollowsPositioning && positioningSeen) ? 'known' : 'inferred',
892
+ positioningMode: positioningSeen ? 'known' : 'inferred',
893
+ // Arc plane (DD-010 D3, #157): 'known' once a G17/G18/G19 was seen, else 'inferred' (XY assumed).
894
+ arcPlanes: arcPlaneSeen ? 'known' : 'inferred',
895
+ // Coordinate system (DD-010 D4, #158): 'known' once a G53/G54–G59/G92-XYZ/G10 was seen, else
896
+ // 'inferred' (identity WCS / no offset assumed — the FDM-slicer default).
897
+ coordinateSystem: coordSystemSeen ? 'known' : 'inferred'
467
898
  };
899
+ // Opt-in modal channels (DD-012 D3): only surfaced when requested. 'known' once its controlling
900
+ // signal was seen (toolPower ← a tool-state modal), else 'unavailable' — never a fabricated value.
901
+ if (wantToolPower)
902
+ capabilities.toolPower = toolStateSeen ? 'known' : 'unavailable';
468
903
  if (layersCapability === 'unavailable') {
469
904
  warn('layers-unavailable', 'No planar layer index; all segments assigned to layer 0.');
470
905
  }
@@ -564,7 +999,15 @@ export function createEngine(input, opts) {
564
999
  featureRoles: 'unavailable',
565
1000
  objects: 'unavailable',
566
1001
  retractions: 'unavailable', // markers resolve on the final IR, not on preview slices
567
- colorChanges: 'unavailable' // color-change boundaries resolve on the final IR
1002
+ colorChanges: 'unavailable', // color-change boundaries resolve on the final IR
1003
+ wipeMoves: 'unavailable', // annotation move kinds resolve on the final IR (DD-016)
1004
+ seamMoves: 'unavailable',
1005
+ cutMoves: 'unavailable', // non-extrusion classification resolves on the final IR (DD-012)
1006
+ cannedCycles: 'unavailable', // canned-cycle expansion resolves on the final IR (DD-012)
1007
+ extrusionMode: 'inferred', // motion modes resolve fully on the final IR (E10)
1008
+ positioningMode: 'inferred',
1009
+ arcPlanes: 'inferred',
1010
+ coordinateSystem: 'inferred'
568
1011
  }
569
1012
  },
570
1013
  segments: channels,
package/dist/worker.js CHANGED
@@ -10,8 +10,9 @@
10
10
  * `worker-slim.ts`; custom adapters use a consumer-supplied worker entry
11
11
  * calling `createWorkerHandler(post, { dialects: createDialectRunner([...]) })`.
12
12
  */
13
- import { createDialectRunner, cura, klipper, marlin, orcaBambu, prusaSlicer, repRap } from '@chestnutlabs/gcode-dialects';
13
+ import { createDialectRunner, cura, grblLaser, grblMill, klipper, linuxCnc, marlin, orcaBambu, prusaSlicer, repRap } from '@chestnutlabs/gcode-dialects';
14
14
  import { openGcode3mf, sniffGcode3mf } from '@chestnutlabs/gcode-containers';
15
+ import { openBgcodeContainer, sniffBgcode } from '@chestnutlabs/gcode-bgcode';
15
16
  import { createWorkerHandler } from './worker-core.js';
16
17
  /** Built-in dialect adapter set (DD-005 phases 3–4; phase 5 adds command semantics). */
17
18
  const BUILTIN_ADAPTERS = [
@@ -20,11 +21,16 @@ const BUILTIN_ADAPTERS = [
20
21
  cura(),
21
22
  klipper(),
22
23
  marlin(),
23
- repRap()
24
+ repRap(),
25
+ // Non-extrusion controllers (DD-012 phase 3, #189) — all EXPERIMENTAL tier until hardware-validated.
26
+ grblLaser(),
27
+ grblMill(),
28
+ linuxCnc()
24
29
  ];
25
- /** Built-in container adapters (DD-005 §4.4): .gcode.3mf. */
30
+ /** Built-in container adapters (DD-005 §4.4): .gcode.3mf and Prusa binary G-code (.bgcode, #188). */
26
31
  const BUILTIN_CONTAINERS = [
27
- { id: 'gcode-3mf', sniff: sniffGcode3mf, open: (bytes) => openGcode3mf(bytes) }
32
+ { id: 'gcode-3mf', sniff: sniffGcode3mf, open: (bytes) => openGcode3mf(bytes) },
33
+ { id: 'bgcode', sniff: (prefix) => sniffBgcode(prefix), open: (bytes) => openBgcodeContainer(bytes) }
28
34
  ];
29
35
  const post = (msg, transfer) => self.postMessage(msg, transfer);
30
36
  const handle = createWorkerHandler(post, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnutlabs/gcode-parser",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Worker-safe G-code parse core producing ToolpathIR (DD-003).",
5
5
  "keywords": [
6
6
  "gcode",
@@ -49,8 +49,9 @@
49
49
  "test": "vitest run"
50
50
  },
51
51
  "dependencies": {
52
- "@chestnutlabs/toolpath-core": "0.2.0",
53
- "@chestnutlabs/gcode-dialects": "0.2.0",
54
- "@chestnutlabs/gcode-containers": "0.2.0"
52
+ "@chestnutlabs/toolpath-core": "0.4.0",
53
+ "@chestnutlabs/gcode-dialects": "0.4.0",
54
+ "@chestnutlabs/gcode-containers": "0.4.0",
55
+ "@chestnutlabs/gcode-bgcode": "0.4.0"
55
56
  }
56
57
  }