@chestnutlabs/gcode-parser 0.1.0 → 0.3.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.
package/dist/parse.d.ts CHANGED
@@ -56,6 +56,13 @@ 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;
59
66
  /**
60
67
  * DD-005 §4.3 read-only hooks: observe comments/commands during the parse.
61
68
  * 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
@@ -101,6 +101,62 @@ export function createEngine(input, opts) {
101
101
  let units = 'mm';
102
102
  let unitsSeen = false;
103
103
  let modalFeed = NaN;
104
+ // Motion-model modal state (DD-010 E10 phase 1). `xyzAbsolute` (G90/G91) and the extruder mode
105
+ // resolve the effective per-move E delta and the extrude/travel classification. Defaults are the
106
+ // firmware power-on convention: absolute XYZ, absolute E. `eModeExplicit` is set only by M82/M83;
107
+ // `lastE` is the running absolute-E datum (also reset by `G92 E`).
108
+ let xyzAbsolute = true;
109
+ let positioningSeen = false; // any G90/G91 observed → positioningMode 'known'
110
+ let eModeExplicit = null; // M82/M83; null = infer
111
+ let lastE = 0;
112
+ const extruderFollowsPositioning = opts.extruderFollowsPositioning === true;
113
+ // Arc-plane modal state (DD-010 D3, #157, E10 phase 2). G17=XY (default, power-on),
114
+ // G18=XZ, G19=YZ. Arc flattening runs in the active plane; the through axis ramps linearly.
115
+ let arcPlane = 'xy';
116
+ let arcPlaneSeen = false; // any G17/G18/G19 observed → arcPlanes 'known'
117
+ // Coordinate systems (DD-010 D4, #158, E10 phase 3). The IR stays in the logical/work frame
118
+ // (Option A): the emitted position is `commanded + activeWcsOffset + g92off`. FDM slicer files use
119
+ // the identity WCS with no G92, so every offset is 0 → the corpus is byte-identical. G54–G59 select
120
+ // a per-system offset (settable via G10 L2/L20); G92 X/Y/Z shifts the datum without motion; G53 is a
121
+ // one-shot machine-coordinate bypass for the following move.
122
+ let activeWcs = 0; // 0..5 → G54..G59; G54 (identity) is the power-on default
123
+ const wcsOffsets = Array.from({ length: 6 }, () => ({ x: 0, y: 0, z: 0 }));
124
+ let g92x = 0;
125
+ let g92y = 0;
126
+ let g92z = 0;
127
+ let coordSystemSeen = false; // any G53/G54–G59/G92-XYZ/G10 → coordinateSystem 'known'
128
+ let g53OneShot = false; // next move ignores the work offset (machine coordinates)
129
+ // Per-axis position certainty (DD-010 D4 amendment, #158). A G31 probe endpoint is reached at
130
+ // RUNTIME (workpiece contact), not the commanded value, so it marks the probed axes uncertain. A
131
+ // following G92 then RESYNCS the logical frame at the datum (rather than datum-shifting a stale
132
+ // position); an absolute move re-establishes certainty. Independent of the work offsets above.
133
+ let certainX = true;
134
+ let certainY = true;
135
+ let certainZ = true;
136
+ const offX = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].x + g92x);
137
+ const offY = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].y + g92y);
138
+ const offZ = () => (g53OneShot ? 0 : wcsOffsets[activeWcs].z + g92z);
139
+ /**
140
+ * Effective per-move E delta from the modal extruder mode (DD-010 D1/D2 precedence:
141
+ * explicit M82/M83 → firmware-conditioned G90/G91 → absolute default). Updates `lastE`.
142
+ * For relative E the delta is the raw word (byte-identical to the pre-E10 engine for the
143
+ * M83 corpus); for absolute E it is `eParam − lastE`.
144
+ */
145
+ const resolveEDelta = (eParam) => {
146
+ if (eParam === undefined)
147
+ return 0;
148
+ const absolute = eModeExplicit !== null ? eModeExplicit === 'absolute' : extruderFollowsPositioning ? xyzAbsolute : true; // firmware-neutral default: absolute, disclosed 'inferred'
149
+ const delta = absolute ? eParam - lastE : eParam;
150
+ lastE = absolute ? eParam : lastE + eParam;
151
+ return delta;
152
+ };
153
+ /**
154
+ * Logical (work-frame) target for one axis given the positioning mode (G90/G91) and the active
155
+ * work offset (DD-010 D4). Absolute: `word + off`; relative: `cur + word` (the offset is already
156
+ * baked into `cur`); an absent word holds the current logical position. With `off === 0` (the
157
+ * identity WCS + no G92) this reduces to the pre-#158 `word ?? cur`, so the corpus is byte-identical.
158
+ */
159
+ const nextAxis = (cur, word, off = 0) => xyzAbsolute ? (word !== undefined ? word + off : cur) : cur + (word ?? 0);
104
160
  // Floating origin (DD-001 §4.6): fixed at the first emitted segment's start.
105
161
  let originSet = false;
106
162
  let ox = 0;
@@ -120,6 +176,16 @@ export function createEngine(input, opts) {
120
176
  let stopReason;
121
177
  let truncatedAtByte;
122
178
  let currentSrcByte = 0;
179
+ // Retraction/unretraction events (DD-009 D1, #148): E-only moves emit no segment,
180
+ // so their position is captured here in ABSOLUTE coords and rebased to the origin
181
+ // at IR assembly (mirroring segment positions). Physical convention: E<0 pulls
182
+ // filament back = 'retract'; E>0 (no XYZ) pushes it out = 'unretract'.
183
+ const retractionEvents = [];
184
+ // M600 filament-swap color-change boundaries (DD-009 D2 amendment, #147): a marker
185
+ // with a position but no motion segment, captured here in ABSOLUTE coords and rebased
186
+ // to the origin at IR assembly (like retractions). Detected in the parser — M600 is
187
+ // firmware-universal — so a bare M600 is honored even with no dialect detected.
188
+ const colorChangeEvents = [];
123
189
  const emitSegment = (x, y, z, eDelta, kind) => {
124
190
  if (writer.count >= limits.maxSegments) {
125
191
  stopReason = {
@@ -244,13 +310,26 @@ export function createEngine(input, opts) {
244
310
  };
245
311
  const g0 = (p) => {
246
312
  const { x, y, z, e, f } = p;
313
+ // Classify on the true per-move delta, not the raw E word (DD-010 D1). For the M83/relative
314
+ // corpus the delta equals the word, so output is byte-identical to the pre-E10 engine.
315
+ const eDelta = resolveEDelta(e);
247
316
  if (x === undefined && y === undefined && z === undefined) {
248
- if (e > 0)
317
+ if (eDelta > 0)
249
318
  stats.retractions++;
250
- else if (e < 0)
319
+ else if (eDelta < 0)
251
320
  stats.deretractions++;
252
321
  else if (f !== undefined)
253
322
  stats.feedrateChanges++;
323
+ if (e !== undefined && eDelta !== 0) {
324
+ retractionEvents.push({
325
+ x: sx,
326
+ y: sy,
327
+ z: sz,
328
+ kind: eDelta < 0 ? 'retract' : 'unretract',
329
+ srcByte: currentSrcByte,
330
+ segIndex: writer.count
331
+ });
332
+ }
254
333
  else
255
334
  stats.others++;
256
335
  if (f !== undefined)
@@ -260,47 +339,75 @@ export function createEngine(input, opts) {
260
339
  stats.points++;
261
340
  if (f !== undefined)
262
341
  modalFeed = f;
263
- const pathType = e > 0 ? 'extrusion' : 'travel';
342
+ const pathType = eDelta > 0 ? 'extrusion' : 'travel';
264
343
  if (path === null || path.type !== pathType) {
265
344
  breakPath(pathType);
266
345
  }
267
- if (e > 0)
268
- stats.extrusionDistance += e;
269
- sx = x ?? sx;
270
- sy = y ?? sy;
271
- sz = z ?? sz;
272
- return emitSegment(sx, sy, sz, e ?? 0, pathType === 'extrusion' ? MoveKind.Extrude : MoveKind.Travel);
346
+ if (eDelta > 0)
347
+ stats.extrusionDistance += eDelta;
348
+ sx = nextAxis(sx, x, offX());
349
+ sy = nextAxis(sy, y, offY());
350
+ sz = nextAxis(sz, z, offZ());
351
+ // An absolute commanded move re-establishes certainty for the axes it names (#158).
352
+ if (xyzAbsolute) {
353
+ if (x !== undefined)
354
+ certainX = true;
355
+ if (y !== undefined)
356
+ certainY = true;
357
+ if (z !== undefined)
358
+ certainZ = true;
359
+ }
360
+ g53OneShot = false; // one-shot machine-coordinate bypass consumed by this move
361
+ return emitSegment(sx, sy, sz, eDelta, pathType === 'extrusion' ? MoveKind.Extrude : MoveKind.Travel);
273
362
  };
274
363
  const g2 = (p, cw) => {
275
- const { x, y, z, e, f } = p;
276
- let { i, j, r } = p;
364
+ const { e, f } = p;
365
+ let { r } = p;
277
366
  if (f !== undefined)
278
367
  modalFeed = f;
279
- const pathType = e ? 'extrusion' : 'travel';
368
+ // E is delta-based (DD-010 D1) so M82 arcs classify correctly and `lastE` stays consistent with g0.
369
+ const eDelta = resolveEDelta(e);
370
+ const pathType = eDelta ? 'extrusion' : 'travel';
280
371
  if (path === null || path.type !== pathType) {
281
372
  breakPath(pathType);
282
373
  }
283
- if (e > 0)
284
- stats.extrusionDistance += e;
374
+ if (eDelta > 0)
375
+ stats.extrusionDistance += eDelta;
376
+ // Absolute targets, honoring G90/G91 (DD-010 D2 — the deferred G91-arc geometry lands here, #157)
377
+ // and the active work offset (DD-010 D4, #158). I/J/K are ALWAYS current-relative center offsets.
378
+ const tx = nextAxis(sx, p.x, offX());
379
+ const ty = nextAxis(sy, p.y, offY());
380
+ const tz = nextAxis(sz, p.z, offZ());
381
+ g53OneShot = false; // one-shot machine-coordinate bypass consumed by this move
382
+ // Select the active arc plane (DD-010 D3): (sa,sb)→(ta,tb) is the in-plane arc with center offsets
383
+ // (oa,ob); sc→tc is the through axis (linear ramp). XY reproduces the pre-#157 math exactly, so the
384
+ // XY-arc corpus stays byte-identical; only G18/G19 arcs newly flatten in the correct plane.
385
+ const [sa, sb, sc, ta, tb, tc, oa, ob] = arcPlane === 'xz'
386
+ ? [sx, sz, sy, tx, tz, ty, p.i, p.k]
387
+ : arcPlane === 'yz'
388
+ ? [sy, sz, sx, ty, tz, tx, p.j, p.k]
389
+ : [sx, sy, sz, tx, ty, tz, p.i, p.j];
390
+ let ia = oa;
391
+ let ib = ob;
285
392
  if (r) {
286
- const deltaX = x - sx; // assume abs mode (inherited)
287
- const deltaY = y - sy;
288
- const minR = Math.sqrt(Math.pow(deltaX / 2, 2) + Math.pow(deltaY / 2, 2));
393
+ const deltaA = ta - sa;
394
+ const deltaB = tb - sb;
395
+ const minR = Math.sqrt(Math.pow(deltaA / 2, 2) + Math.pow(deltaB / 2, 2));
289
396
  r = Math.max(r, minR);
290
- const dSquared = Math.pow(deltaX, 2) + Math.pow(deltaY, 2);
397
+ const dSquared = Math.pow(deltaA, 2) + Math.pow(deltaB, 2);
291
398
  const hSquared = Math.pow(r, 2) - dSquared / 4;
292
399
  let hDivD = Math.sqrt(hSquared / dSquared);
293
400
  if ((cw && r < 0.0) || (!cw && r > 0.0))
294
401
  hDivD = -hDivD;
295
- i = deltaX / 2 + deltaY * hDivD;
296
- j = deltaY / 2 - deltaX * hDivD;
297
- }
298
- const wholeCircle = sx == x && sy == y;
299
- const centerX = sx + i;
300
- const centerY = sy + j;
301
- const arcRadius = Math.sqrt(i * i + j * j);
302
- const arcCurrentAngle = Math.atan2(-j, -i);
303
- const finalTheta = Math.atan2(y - centerY, x - centerX);
402
+ ia = deltaA / 2 + deltaB * hDivD;
403
+ ib = deltaB / 2 - deltaA * hDivD;
404
+ }
405
+ const wholeCircle = sa == ta && sb == tb;
406
+ const centerA = sa + ia;
407
+ const centerB = sb + ib;
408
+ const arcRadius = Math.sqrt(ia * ia + ib * ib);
409
+ const arcCurrentAngle = Math.atan2(-ib, -ia);
410
+ const finalTheta = Math.atan2(tb - centerB, ta - centerA);
304
411
  let totalArc;
305
412
  if (wholeCircle) {
306
413
  totalArc = 2 * Math.PI;
@@ -317,26 +424,30 @@ export function createEngine(input, opts) {
317
424
  totalSegments = 1;
318
425
  let arcAngleIncrement = totalArc / totalSegments;
319
426
  arcAngleIncrement *= cw ? -1 : 1;
320
- const zDist = sz - (z || sz);
321
- const zStep = zDist / totalSegments;
427
+ // Through-axis linear ramp — preserves the inherited step (byte-identical for planar XY arcs,
428
+ // where the through axis is unchanged so the ramp is flat).
429
+ const cStep = (sc - tc) / totalSegments;
322
430
  const kind = (pathType === 'extrusion' ? MoveKind.Extrude : MoveKind.Travel) | MoveKind.ArcSegment;
323
- const eachE = e !== undefined ? e / Math.max(1, Math.ceil(totalSegments)) : 0;
324
- let px;
325
- let py;
326
- let pz = sz;
431
+ const eachE = e !== undefined ? eDelta / Math.max(1, Math.ceil(totalSegments)) : 0;
432
+ // Map an in-plane point (pa,pb) + through-axis pc back to (x,y,z) for the active plane.
433
+ const emitArc = (pa, pb, pcv) => {
434
+ const [px, py, pz] = arcPlane === 'xz' ? [pa, pcv, pb] : arcPlane === 'yz' ? [pcv, pa, pb] : [pa, pb, pcv];
435
+ return emitSegment(px, py, pz, eachE, kind);
436
+ };
437
+ let pc = sc;
327
438
  let currentAngle = arcCurrentAngle;
328
439
  for (let moveIdx = 0; moveIdx < totalSegments - 1; moveIdx++) {
329
440
  currentAngle += arcAngleIncrement;
330
- px = centerX + arcRadius * Math.cos(currentAngle);
331
- py = centerY + arcRadius * Math.sin(currentAngle);
332
- pz += zStep;
333
- if (!emitSegment(px, py, pz, eachE, kind))
441
+ const pa = centerA + arcRadius * Math.cos(currentAngle);
442
+ const pb = centerB + arcRadius * Math.sin(currentAngle);
443
+ pc += cStep;
444
+ if (!emitArc(pa, pb, pc))
334
445
  return false;
335
446
  }
336
- sx = x || sx;
337
- sy = y || sy;
338
- sz = z || sz;
339
- return emitSegment(sx, sy, sz, eachE, kind);
447
+ sx = tx;
448
+ sy = ty;
449
+ sz = tz;
450
+ return emitArc(ta, tb, tc);
340
451
  };
341
452
  const onComment = opts.onComment;
342
453
  const onCommand = opts.onCommand;
@@ -395,6 +506,144 @@ export function createEngine(input, opts) {
395
506
  case 't7':
396
507
  tool = Number(cmd.gcode.slice(1));
397
508
  break;
509
+ case 'm600':
510
+ // Manual filament swap = color boundary (#147). A marker, not motion — do
511
+ // NOT break/finish the current path; record position (current head), the
512
+ // next segment index (slot boundary), and the active tool for provenance.
513
+ colorChangeEvents.push({ x: sx, y: sy, z: sz, segIndex: writer.count, srcByte: offset, tool });
514
+ break;
515
+ // Motion-model modal commands (DD-010 E10 phase 1).
516
+ case 'g90':
517
+ xyzAbsolute = true;
518
+ positioningSeen = true;
519
+ break;
520
+ case 'g91':
521
+ xyzAbsolute = false;
522
+ positioningSeen = true;
523
+ break;
524
+ // Arc-plane selection (DD-010 D3, #157, E10 phase 2).
525
+ case 'g17':
526
+ arcPlane = 'xy';
527
+ arcPlaneSeen = true;
528
+ break;
529
+ case 'g18':
530
+ arcPlane = 'xz';
531
+ arcPlaneSeen = true;
532
+ break;
533
+ case 'g19':
534
+ arcPlane = 'yz';
535
+ arcPlaneSeen = true;
536
+ break;
537
+ case 'm82':
538
+ eModeExplicit = 'absolute';
539
+ break;
540
+ case 'm83':
541
+ eModeExplicit = 'relative';
542
+ break;
543
+ // Coordinate systems (DD-010 D4, #158, E10 phase 3).
544
+ case 'g53':
545
+ // One-shot: the following move is in machine coordinates (ignores the work offset).
546
+ g53OneShot = true;
547
+ coordSystemSeen = true;
548
+ break;
549
+ case 'g54':
550
+ case 'g55':
551
+ case 'g56':
552
+ case 'g57':
553
+ case 'g58':
554
+ case 'g59':
555
+ activeWcs = Number(cmd.gcode.slice(1)) - 54; // g54→0 … g59→5
556
+ coordSystemSeen = true;
557
+ break;
558
+ case 'g10': {
559
+ // Set a work-coordinate offset. L2 P<n>: set the offset directly. L20 P<n>: set it so the
560
+ // current position reads the given value in WCS n. P1→G54 … P6→G59 (default: active system).
561
+ const l = cmd.params.l;
562
+ const idx = cmd.params.p !== undefined ? Math.round(cmd.params.p) - 1 : activeWcs;
563
+ if (idx >= 0 && idx < 6 && (l === 2 || l === 20)) {
564
+ const w = wcsOffsets[idx];
565
+ const setAxis = (word, cur, axis) => {
566
+ if (word === undefined)
567
+ return;
568
+ w[axis] = l === 2 ? word : cur - word; // L20: offset = current logical − desired reading
569
+ };
570
+ setAxis(cmd.params.x, sx, 'x');
571
+ setAxis(cmd.params.y, sy, 'y');
572
+ setAxis(cmd.params.z, sz, 'z');
573
+ coordSystemSeen = true;
574
+ }
575
+ break;
576
+ }
577
+ case 'g31': {
578
+ // Probe move (DD-010 D4 amendment, #158): the endpoint is reached at RUNTIME (workpiece
579
+ // contact), NOT the commanded value. Do not advance the position or draw a fabricated probe
580
+ // move; mark the probed axes runtime-dependent so a following G92 resyncs the logical frame.
581
+ let probed = false;
582
+ if (cmd.params.x !== undefined) {
583
+ certainX = false;
584
+ probed = true;
585
+ }
586
+ if (cmd.params.y !== undefined) {
587
+ certainY = false;
588
+ probed = true;
589
+ }
590
+ if (cmd.params.z !== undefined) {
591
+ certainZ = false;
592
+ probed = true;
593
+ }
594
+ if (probed) {
595
+ 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);
596
+ }
597
+ break;
598
+ }
599
+ case 'g92': {
600
+ // Datum WITHOUT motion (DD-010 D4 + probe amendment, #158). `G92 E<v>` rebases the absolute-E
601
+ // origin (also used in phase 1). For X/Y/Z, when the position is KNOWN it is a datum SHIFT (the
602
+ // work offset is set so the current logical position reads <v>, preserving continuity). When the
603
+ // axis is runtime-dependent (post-probe) it is a logical RESYNC: the current logical position is
604
+ // declared to be <v>, the offset is reset, certainty is restored, and the current path is
605
+ // finalized so the next move starts a NEW frame at the datum — no fabricated move is drawn
606
+ // across the unknown probe result.
607
+ if (cmd.params.e !== undefined)
608
+ lastE = cmd.params.e;
609
+ let resync = false;
610
+ if (cmd.params.x !== undefined) {
611
+ if (certainX)
612
+ g92x = sx - cmd.params.x - wcsOffsets[activeWcs].x;
613
+ else {
614
+ sx = cmd.params.x + wcsOffsets[activeWcs].x;
615
+ g92x = 0;
616
+ certainX = true;
617
+ resync = true;
618
+ }
619
+ coordSystemSeen = true;
620
+ }
621
+ if (cmd.params.y !== undefined) {
622
+ if (certainY)
623
+ g92y = sy - cmd.params.y - wcsOffsets[activeWcs].y;
624
+ else {
625
+ sy = cmd.params.y + wcsOffsets[activeWcs].y;
626
+ g92y = 0;
627
+ certainY = true;
628
+ resync = true;
629
+ }
630
+ coordSystemSeen = true;
631
+ }
632
+ if (cmd.params.z !== undefined) {
633
+ if (certainZ)
634
+ g92z = sz - cmd.params.z - wcsOffsets[activeWcs].z;
635
+ else {
636
+ sz = cmd.params.z + wcsOffsets[activeWcs].z;
637
+ g92z = 0;
638
+ certainZ = true;
639
+ resync = true;
640
+ }
641
+ coordSystemSeen = true;
642
+ }
643
+ if (resync)
644
+ finishPath(); // start a new frame at the datum; no move connects across the probe
645
+ break;
646
+ }
398
647
  default:
399
648
  warn('unsupported-command', `unsupported command '${cmd.gcode}' preserved as metadata`, offset);
400
649
  }
@@ -435,7 +684,23 @@ export function createEngine(input, opts) {
435
684
  feedrate: 'known',
436
685
  sourcePositions: 'known',
437
686
  featureRoles: 'unavailable',
438
- objects: 'unavailable'
687
+ objects: 'unavailable',
688
+ retractions: retractionEvents.length > 0 ? 'known' : 'unavailable',
689
+ colorChanges: colorChangeEvents.length > 0 ? 'known' : 'unavailable',
690
+ // Annotation-derived move kinds (DD-016, #182). The parser never sets these — they come from a
691
+ // slicer adapter's WIPE_START/END brackets (upgraded to 'known' via the sink). Seam has no
692
+ // per-move G-code signal and stays 'unavailable' (a future geometry-heuristic DD may change it).
693
+ wipeMoves: 'unavailable',
694
+ seamMoves: 'unavailable',
695
+ // Motion-model modes (DD-010 E10 phase 1). 'known' when the governing command was seen
696
+ // (M82/M83, or a firmware-known G90/G91 for E); 'inferred' when defaulted (absolute).
697
+ extrusionMode: eModeExplicit !== null || (extruderFollowsPositioning && positioningSeen) ? 'known' : 'inferred',
698
+ positioningMode: positioningSeen ? 'known' : 'inferred',
699
+ // Arc plane (DD-010 D3, #157): 'known' once a G17/G18/G19 was seen, else 'inferred' (XY assumed).
700
+ arcPlanes: arcPlaneSeen ? 'known' : 'inferred',
701
+ // Coordinate system (DD-010 D4, #158): 'known' once a G53/G54–G59/G92-XYZ/G10 was seen, else
702
+ // 'inferred' (identity WCS / no offset assumed — the FDM-slicer default).
703
+ coordinateSystem: coordSystemSeen ? 'known' : 'inferred'
439
704
  };
440
705
  if (layersCapability === 'unavailable') {
441
706
  warn('layers-unavailable', 'No planar layer index; all segments assigned to layer 0.');
@@ -463,6 +728,22 @@ export function createEngine(input, opts) {
463
728
  layers: irLayers,
464
729
  tools,
465
730
  objects: [],
731
+ retractions: retractionEvents.map((r) => ({
732
+ x: r.x - ox,
733
+ y: r.y - oy,
734
+ z: r.z - oz,
735
+ kind: r.kind,
736
+ srcByte: r.srcByte,
737
+ segIndex: r.segIndex
738
+ })),
739
+ colorChanges: colorChangeEvents.map((c) => ({
740
+ x: c.x - ox,
741
+ y: c.y - oy,
742
+ z: c.z - oz,
743
+ segIndex: c.segIndex,
744
+ srcByte: c.srcByte,
745
+ tool: c.tool
746
+ })),
466
747
  bounds,
467
748
  boundsWithTravel,
468
749
  sourceIndex
@@ -518,13 +799,23 @@ export function createEngine(input, opts) {
518
799
  feedrate: 'known',
519
800
  sourcePositions: 'unavailable', // no per-slice source index is built
520
801
  featureRoles: 'unavailable',
521
- objects: 'unavailable'
802
+ objects: 'unavailable',
803
+ retractions: 'unavailable', // markers resolve on the final IR, not on preview slices
804
+ colorChanges: 'unavailable', // color-change boundaries resolve on the final IR
805
+ wipeMoves: 'unavailable', // annotation move kinds resolve on the final IR (DD-016)
806
+ seamMoves: 'unavailable',
807
+ extrusionMode: 'inferred', // motion modes resolve fully on the final IR (E10)
808
+ positioningMode: 'inferred',
809
+ arcPlanes: 'inferred',
810
+ coordinateSystem: 'inferred'
522
811
  }
523
812
  },
524
813
  segments: channels,
525
814
  layers: channels.count > 0 ? [{ z: lastZ, segStart: 0, segEnd: channels.count - 1 }] : [],
526
815
  tools: [...toolsSeen].sort((a, b) => a - b).map((id) => ({ id })),
527
816
  objects: [],
817
+ retractions: [],
818
+ colorChanges: [],
528
819
  bounds,
529
820
  boundsWithTravel,
530
821
  sourceIndex: { byteOffsets: new Uint32Array(0), segmentIndices: new Uint32Array(0) }
package/dist/worker.js CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { createDialectRunner, cura, klipper, 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 = [
@@ -22,9 +23,10 @@ const BUILTIN_ADAPTERS = [
22
23
  marlin(),
23
24
  repRap()
24
25
  ];
25
- /** Built-in container adapters (DD-005 §4.4): .gcode.3mf. */
26
+ /** Built-in container adapters (DD-005 §4.4): .gcode.3mf and Prusa binary G-code (.bgcode, #188). */
26
27
  const BUILTIN_CONTAINERS = [
27
- { id: 'gcode-3mf', sniff: sniffGcode3mf, open: (bytes) => openGcode3mf(bytes) }
28
+ { id: 'gcode-3mf', sniff: sniffGcode3mf, open: (bytes) => openGcode3mf(bytes) },
29
+ { id: 'bgcode', sniff: (prefix) => sniffBgcode(prefix), open: (bytes) => openBgcodeContainer(bytes) }
28
30
  ];
29
31
  const post = (msg, transfer) => self.postMessage(msg, transfer);
30
32
  const handle = createWorkerHandler(post, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnutlabs/gcode-parser",
3
- "version": "0.1.0",
3
+ "version": "0.3.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.1.0",
53
- "@chestnutlabs/gcode-dialects": "0.1.0",
54
- "@chestnutlabs/gcode-containers": "0.1.0"
52
+ "@chestnutlabs/toolpath-core": "0.3.0",
53
+ "@chestnutlabs/gcode-dialects": "0.3.0",
54
+ "@chestnutlabs/gcode-containers": "0.3.0",
55
+ "@chestnutlabs/gcode-bgcode": "0.3.0"
55
56
  }
56
57
  }