@chestnutlabs/gcode-parser 0.2.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;
@@ -254,19 +310,22 @@ export function createEngine(input, opts) {
254
310
  };
255
311
  const g0 = (p) => {
256
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);
257
316
  if (x === undefined && y === undefined && z === undefined) {
258
- if (e > 0)
317
+ if (eDelta > 0)
259
318
  stats.retractions++;
260
- else if (e < 0)
319
+ else if (eDelta < 0)
261
320
  stats.deretractions++;
262
321
  else if (f !== undefined)
263
322
  stats.feedrateChanges++;
264
- if (e !== undefined && e !== 0) {
323
+ if (e !== undefined && eDelta !== 0) {
265
324
  retractionEvents.push({
266
325
  x: sx,
267
326
  y: sy,
268
327
  z: sz,
269
- kind: e < 0 ? 'retract' : 'unretract',
328
+ kind: eDelta < 0 ? 'retract' : 'unretract',
270
329
  srcByte: currentSrcByte,
271
330
  segIndex: writer.count
272
331
  });
@@ -280,47 +339,75 @@ export function createEngine(input, opts) {
280
339
  stats.points++;
281
340
  if (f !== undefined)
282
341
  modalFeed = f;
283
- const pathType = e > 0 ? 'extrusion' : 'travel';
342
+ const pathType = eDelta > 0 ? 'extrusion' : 'travel';
284
343
  if (path === null || path.type !== pathType) {
285
344
  breakPath(pathType);
286
345
  }
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);
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);
293
362
  };
294
363
  const g2 = (p, cw) => {
295
- const { x, y, z, e, f } = p;
296
- let { i, j, r } = p;
364
+ const { e, f } = p;
365
+ let { r } = p;
297
366
  if (f !== undefined)
298
367
  modalFeed = f;
299
- 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';
300
371
  if (path === null || path.type !== pathType) {
301
372
  breakPath(pathType);
302
373
  }
303
- if (e > 0)
304
- 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;
305
392
  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));
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));
309
396
  r = Math.max(r, minR);
310
- const dSquared = Math.pow(deltaX, 2) + Math.pow(deltaY, 2);
397
+ const dSquared = Math.pow(deltaA, 2) + Math.pow(deltaB, 2);
311
398
  const hSquared = Math.pow(r, 2) - dSquared / 4;
312
399
  let hDivD = Math.sqrt(hSquared / dSquared);
313
400
  if ((cw && r < 0.0) || (!cw && r > 0.0))
314
401
  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);
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);
324
411
  let totalArc;
325
412
  if (wholeCircle) {
326
413
  totalArc = 2 * Math.PI;
@@ -337,26 +424,30 @@ export function createEngine(input, opts) {
337
424
  totalSegments = 1;
338
425
  let arcAngleIncrement = totalArc / totalSegments;
339
426
  arcAngleIncrement *= cw ? -1 : 1;
340
- const zDist = sz - (z || sz);
341
- 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;
342
430
  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;
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;
347
438
  let currentAngle = arcCurrentAngle;
348
439
  for (let moveIdx = 0; moveIdx < totalSegments - 1; moveIdx++) {
349
440
  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))
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))
354
445
  return false;
355
446
  }
356
- sx = x || sx;
357
- sy = y || sy;
358
- sz = z || sz;
359
- return emitSegment(sx, sy, sz, eachE, kind);
447
+ sx = tx;
448
+ sy = ty;
449
+ sz = tz;
450
+ return emitArc(ta, tb, tc);
360
451
  };
361
452
  const onComment = opts.onComment;
362
453
  const onCommand = opts.onCommand;
@@ -421,6 +512,138 @@ export function createEngine(input, opts) {
421
512
  // next segment index (slot boundary), and the active tool for provenance.
422
513
  colorChangeEvents.push({ x: sx, y: sy, z: sz, segIndex: writer.count, srcByte: offset, tool });
423
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
+ }
424
647
  default:
425
648
  warn('unsupported-command', `unsupported command '${cmd.gcode}' preserved as metadata`, offset);
426
649
  }
@@ -463,7 +686,21 @@ export function createEngine(input, opts) {
463
686
  featureRoles: 'unavailable',
464
687
  objects: 'unavailable',
465
688
  retractions: retractionEvents.length > 0 ? 'known' : 'unavailable',
466
- colorChanges: colorChangeEvents.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'
467
704
  };
468
705
  if (layersCapability === 'unavailable') {
469
706
  warn('layers-unavailable', 'No planar layer index; all segments assigned to layer 0.');
@@ -564,7 +801,13 @@ export function createEngine(input, opts) {
564
801
  featureRoles: 'unavailable',
565
802
  objects: 'unavailable',
566
803
  retractions: 'unavailable', // markers resolve on the final IR, not on preview slices
567
- colorChanges: 'unavailable' // color-change boundaries resolve on the final IR
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'
568
811
  }
569
812
  },
570
813
  segments: channels,
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.2.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.2.0",
53
- "@chestnutlabs/gcode-dialects": "0.2.0",
54
- "@chestnutlabs/gcode-containers": "0.2.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
  }