-
#252
b2053beThanks @sobechestnut-dev! - feat: canned drilling cycle expansion — G81/G82/G83 (DD-012 phase 2, #189)CNC canned drilling cycles previously produced zero geometry — holes vanished. They now expand to explicit sub-moves so the drilling is real, classified toolpath:
- G81/G82 (drill / drill-with-dwell): rapid to the hole XY, rapid down to the R plane, feed to
depth (
Cut), rapid retract. - G83 (peck): the plunge is a peck loop — feed down by
Q, rapid-retract to R between pecks, until reaching depth; each down-feed is aCut. - G98/G99 set the retract plane (initial Z vs R); G80 cancels; a
G0–G3motion also cancels. - Modal repeat: with a cycle active, a bare
X/Yline drills another hole (retaining Z/R/Q and the initial plane) — the common CNC hole-pattern form. - Rapids are
Travel, plunges areCut; new capabilitycannedCycles(knownonce a cycle is seen, elseunavailable).
FDM output is unchanged (no canned cycles in FDM); the native-golden corpus gains only the additive
cannedCyclescapability, with no geometry change. - G81/G82 (drill / drill-with-dwell): rapid to the hole XY, rapid down to the R plane, feed to
depth (
-
#253
13fd5c6Thanks @sobechestnut-dev! - feat: non-extrusion dialect families + validation tiers (DD-012 phase 3, #189)Adds controller detection and the validation-tier honesty mechanism for CNC/laser toolpaths:
- New dialects (
@chestnutlabs/gcode-dialects): GRBL laser (LightBurn /$32laser mode /M4+S), GRBL mill and LinuxCNC (M3spindle,%/banner envelopes). Registered in the batteries worker. - Each dialect adds provenance (
cnc.controller,cnc.machineClass,cnc.toolPowerLabel) and a validation tier (cnc.validationTier). Per DD-012 D6: an experimental dialect reports its non-extrusion claims (cutMoves/toolPower/cannedCycles) asinferred(neverknown), with acnc-dialect-experimentaldisclosure — and only for claims the file actually made (an unused feature is never fabricated). - All launch dialects ship
experimental(synthetic fixtures only). A singletier: 'validated'flip per controller promotes its claims toknownonce confirmed on real hardware (DD-012 §8/§15).
Geometry is untouched (dialects only annotate/label). FDM detection is unaffected — CNC dialects do not match FDM output.
- New dialects (
-
#248
1029580Thanks @sobechestnut-dev! - feat: non-extrusionCutmove classification + tool-state modal (DD-012 phase 1, #189)Non-extrusion toolpaths (CNC / laser / plotter) no longer collapse their productive moves into
Travel. The parser now tracks a tool-engaged modal state —M3/M4(spindle/laser on, incl. theM03/M04leading-zero form) engage it,M5disengages — and classifies a move with no extrusionEwhile the tool is engaged as the newMoveKind.Cutbit (a CNC/laser/plotter counterpart toExtrude, composing withArcSegmentlike the other kinds).- New IR move kind
MoveKind.Cut = 1 << 7(@chestnutlabs/toolpath-core). - New capability
cutMoves:knownonce a tool-state modal is seen (a CNC/laser/plotter file),unavailablefor FDM. - FDM is byte-identical: FDM slices never issue
M3/M4, soCutis never set and every move staysExtrude/Travelexactly as before (verified against the native-golden corpus; the CNC fixturesdemo-easel/demo-mach3are documented intentional adapter-divergences).
Modal tool-state value channels (laser power / spindle RPM via
S), canned-cycle expansion, and dialect families follow in later DD-012 phases. - New IR move kind
-
#251
11f317dThanks @sobechestnut-dev! - feat: modal motion continuation — bare coordinate lines repeat the last G0–G3 (DD-012 phase 2, #189)CNC/LinuxCNC-style G-code frequently omits the
Gword on repeated moves (G1 X0 Y0then bareX10 Y0/X20 Y0). The parser previously dropped those lines entirely — a three-move path produced a single segment. It now tracks the activeG0–G3motion mode and treats a line whose leading word is a coordinate axis (X/Y/Z, with noG/M/Tcommand) as a continuation of that mode, so the full toolpath is emitted and classified/colored consistently (incl. inlineSfortoolPower).FDM output is byte-identical — slicers always emit the
Gword, so the continuation path never triggers (the native-golden corpus is unchanged). This unblocks canned-cycle repeat (next phase). -
#250
8fec7c3Thanks @sobechestnut-dev! - feat: opt-in modal tool-power channel (DD-012 phase 1 — theModalChannelmechanism, #189)Adds the shared, opt-in
ModalChannelmechanism DD-012 D3 is built around, and its first channel:toolPower— the modal spindle/laserSvalue while a tool is engaged.ParseOptions.modalChannels?: readonly string[]— request per-segment modal channels by id. Supported id:'toolPower'. Unknown ids are ignored with amodal-channel-unsupportedwarning.ToolpathSegments.modal?: Readonly<Record<string, Float32Array>>— one Float32 column per requested channel, present only when requested. An unset value isNaN(an honest "no value here"), never a fabricated0.toolPoweris the modalS(set onM3/M4and inline on GRBL-laser motion lines) while engaged,NaNwhen the tool is off (M5).- New capability
toolPower: surfaced only when the channel is requested —knownonce a tool-state modal is seen, elseunavailable. - Default parse pays nothing: no
modalChannels⇒ nomodalon the IR, no extra columns, FDM output unchanged. The budget-aware SoA writer (DD-003) grows the opt-in columns in lockstep and accounts their bytes.
Presentation (Watts vs RPM) is a dialect label, not a separate channel (DD-012 D4); #180's fan/temp/accel color channels reuse this same mechanism in a later phase.
-
#256
b84bea9Thanks @sobechestnut-dev! - feat: lexer handles multi-command lines, N-word line numbers, and bare S/F (#189)Real CNC/laser G-code (GRBL, LinuxCNC, TinyG, Mach3, Fanuc) is written very differently from FDM slicer output, and the inherited first-word lexer silently dropped most of it. The lexer now:
- Reads every G/M/T command word on a line, not just the first —
G20 G17 G90,G91 G81 …,S3400 M3now all apply. This was the biggest gap:M3spindle-on andG81canned cycles were being dropped as params, so mills showed noCutmoves and drilled holes vanished. - Strips
N-word line numbers (N10 G1 X…) — Fanuc/Mach/TinyG number every line, which previously reduced whole files to zero geometry. - Latches bare
S/Flines (standaloneS1000/F600) into modal power/feed — common in GRBL-laser output. - Guards against letters embedded in extended-command words (
EXCLUDE_OBJECT … POLYGON=…): a command/param is only taken when a real number follows the letter, soTinM486 T<count>/M104 T<tool>stays a parameter (not a tool select), and theGinPOLYGONnever becomes a move.
Validated against real public sample files: a LinuxCNC arc-spiral went from 16 → 5,506 parsed segments, a TinyG program from 0 → 344. FDM output is byte-identical — slicers emit one clean command per line, so the multi-command path never runs for them (the real-G-code native-golden corpus is unchanged except for fewer spurious
unsupported-commandwarnings;demo-mach3and one adversarial binary fixture are documented intentional divergences). - Reads every G/M/T command word on a line, not just the first —
-
#260
879b60aThanks @sobechestnut-dev! - fix: G0 rapids classify as Travel, not Cut, even while the tool is engaged (#189)The non-extrusion
Cut/Travelclassifier keyed only on tool-state (M3/M4latched), so on a router — where the spindle stays on across rapids — everyG0reposition was counted as a cutting move. DD-012 D2 §4.2 already specifies that rapids stayTravel; this brings the implementation in line: only a feed move (G1/G2/G3) with the tool engaged and noEdelta isCut; aG0rapid isTravelregardless of tool state (a GRBL-laser also gates the beam off duringG0).Surfaced by the CNC/laser validation harness on real files — e.g. the
easelrouter fixture went from 742 cut / 0 rapids to a correct 737 cut / 5 rapids (its 5G0moves). Geometry is unchanged (only thekindcolumn shifts); FDM output is byte-identical sinceCutis never evaluated there. -
Updated dependencies [
3f06e5b,13fd5c6,1029580,8fec7c3,3e244ae]:- @chestnutlabs/gcode-dialects@0.4.0
- @chestnutlabs/toolpath-core@0.4.0
- @chestnutlabs/gcode-bgcode@0.4.0
- @chestnutlabs/gcode-containers@0.4.0
-
#238
75f9f2bThanks @sobechestnut-dev! - Register.bgcodeas a container adapter so it flows through the existing parser pipeline (DD-011 phase 4c, #188). A.bgcodefile now "just works" throughGcodeParseSessionwithcontainers: 'auto'— sniffed by magic, decoded to plain G-code, and parsed to the same IR as the plain.gcode(proven by the golden-equivalence test).gcode-bgcode:openBgcodeContainer(bytes)implements the DD-005 §4.4{ id, sniff, open }shape (single plate;openPlate(0)streams the decoded G-code).openBgcode(bytes, { metadata: true })now also decodes the metadata (INI) and thumbnail blocks, so the adapter surfaces machine geometry frombed_shape, whitelisted slicer settings (feeding dialect detection + provenance), and thumbnails.gcode-parser: the batteries worker registers thebgcodeadapter besidegcode-3mf.
Verified end-to-end: a real Prusa XL cube
.bgcodeparses through the session to 11,417 segments with a 360×360 bed andprinter_modelmetadata. -
#193
5f3b16aThanks @sobechestnut-dev! - Motion-model correctness — E10 phase 1 (#156/#155, DD-010 D1/D2 + G92 E-datum).The interpreter now models the extruder mode (
M82/M83) and positioning mode (G90/G91) and classifies extrude-vs-travel from the true per-move E delta, not the raw E word:- M82 (absolute E) — an E-unchanged move is now correctly
Travel(was mis-classified as extrude), andstats.extrusionDistanceis delta-summed (was inflated by the cumulative E). This is the audit's highest-impact gap (#156). - M83 (relative E) and the common slicer shape (
G90+M83) are byte-identical to before. - G90/G91 set the XYZ positioning mode; relative moves accumulate.
G92 E<v>resets the extruder datum.G92 X/Y/Zis disclosed as unhandled (g92-xyz-unhandledwarning) — deferred to phase 3. - The
G90/G91↔E interaction is firmware-conditioned (DD-010 D2): Marlin/Klipper let G90/G91 steer E; RepRapFirmware keeps E independent. Supplied via the newparseOptions.extruderFollowsPositioninghint (defaultfalse); the byte-exact engine never sniffs firmware. When unspecified, the E mode defaults to absolute (the firmware power-on convention), disclosedinferred. - New capabilities
extrusionModeandpositioningMode('known'when the governing command was seen, else'inferred').
Output change (documented): all segment positions,
kind,tool,layer, andsrcByteare byte-identical across the corpus. Seven hand-crafted dialect fixtures use absolute E without anM82/M83; their per-segment extrusion delta andextrusionDistanceare now corrected (previously inflated by the raw-E-as-delta assumption). No renderer/adapter API change. - M82 (absolute E) — an E-unchanged move is now correctly
-
#208
dc1c535Thanks @sobechestnut-dev! - Motion-model correctness — E10 phase 2: arc-plane selection (#157, DD-010 D3).Arc flattening (
G2/G3) now runs in the active plane selected byG17(XY, default),G18(XZ), orG19(YZ), instead of always assuming XY:- The arc math is plane-parameterized: the in-plane pair uses the two relevant center offsets
(
I/Jfor XY,I/Kfor XZ,J/Kfor YZ) and the through axis ramps linearly.G17reproduces the previous XY math exactly — the whole XY-arc corpus is byte-identical. G18/G19arcs (mainly CNC) previously mis-flattened onto XY (I/J interpretation,Kignored); they now render in the correct plane.- The deferred G91-arc geometry lands here too: arc endpoints honor the positioning mode
(
G90absolute /G91relative);I/J/Kremain current-relative center offsets in both. - New capability
arcPlanes('known'once a plane word is seen, else'inferred'= XY assumed).
Output change (documented): all corpus segment positions/kinds/extrusion stay byte-identical (the only golden change is the additive
arcPlanescapability key); non-XY arcs are new output only for files that useG18/G19. No renderer/adapter API change. - The arc math is plane-parameterized: the in-plane pair uses the two relevant center offsets
(
-
#208
dc1c535Thanks @sobechestnut-dev! - Motion-model correctness — E10 phase 3: coordinate systems + probe-aware datum (#158, DD-010 D4).The interpreter now honors work-coordinate systems and
G92X/Y/Z, keeping the IR in the logical (work) frame so identity-WCS files stay byte-identical:G54–G59select an active work offset (settable viaG10 L2/L20);G53is a one-shot machine-coordinate bypass for the next move.G92X/Y/Z is a datum shift when the current position is known (continuity preserved — e.g.G92 X0at X50 thenG1 X10→ X60).- Probe awareness (DD-010 D4 amendment):
G31reaches its endpoint at runtime, so it marks the probed axes uncertain and is disclosed via a newprobe-position-runtime-dependentwarning (never advanced to the un-reached commanded value). AG92after a probe is a logical resync — the current logical position is declared to be the given value and a new frame starts at the datum, so no fabricated move is drawn across the unknown probe result. Themach3fixture's post-probe path now renders in its authored logical range instead of a stale-position shift. - New capability
coordinateSystem('known'once any G53/G54–G59/G92-XYZ/G10 is seen, else'inferred'= identity WCS).
Output change (documented): the identity-WCS corpus stays byte-identical (the only golden change is the additive
coordinateSystemkey). Themach3fixture intentionally diverges from the inherited engine (which ignoredG31andG92 Z) — a documented semantic correction, pinned by its native golden and excluded from strict adapter-equivalence. No renderer/adapter API change. -
#228
2d2b32bThanks @sobechestnut-dev! - Populate theMoveKind.Wipebit from slicer wipe brackets (DD-016 phase 1, #182).The
Wipe/Seamkind bits were reserved but never set. Wipe's only reliable signal is a slicer comment (;WIPE_START/;WIPE_END), which the DD-005 sink invariant bars the annotation layer from turning intokind. DD-016 resolves this with a narrow, additive sink amendment:AnnotationSink.addMoveKind(segStart, segEnd, kindBits)— allow-listed toWipe/Seamonly, additive (ORs the bit, never clears or reclassifies a move); non-allow-listed bits are dropped with a bounded warning.- The PrusaSlicer and Orca/Bambu adapters detect
;WIPE_START/;WIPE_ENDand mark the bracketed segments asWipe. - New capabilities:
wipeMoves(knownwhen a bracket was parsed, elseunavailable— never fabricated) andseamMoves(alwaysunavailable; seam has no per-move G-code signal).
Golden-safe: the base Extrude/Travel classification is unchanged, so the golden-equivalence gate (kind masked to
Extrude|Travel) is byte-identical; only the additivewipeMoves/seamMovescapability lines were regenerated in the native goldens. No IR schema change, no new dependency. Renderer visibility for wipe moves lands in phase 2.
- Updated dependencies [
75f9f2b,83f0336,852db93,f2e79e4,bb3085a,8c0ee6e,b0ef69f,39348de,d161e80,82bd7ae,2d2b32b]:- @chestnutlabs/gcode-bgcode@0.3.0
- @chestnutlabs/toolpath-core@0.3.0
- @chestnutlabs/gcode-dialects@0.3.0
- @chestnutlabs/gcode-containers@0.3.0
-
#171
1c2e5b0Thanks @sobechestnut-dev! - Add M600 filament-swap color-change annotation (E9 phase 3, #147, DD-009 D2).The parser now records a sparse
colorChangesevents channel onToolpathIR({ x, y, z, segIndex, srcByte, tool }, capabilitycolorChanges) —M600is a marker with a position but no motion segment, captured in a side channel that leaves segment indices, scrub, and layer ranges untouched (mirrors theretractionschannel from #148). Detection lives in the parser (whereM600was previously discarded asunsupported-command), so a bareM600is honored even when no dialect is detected. A newcolorChangerenderer color mode shades segments by swap slot (the count of color changes at or before a segment) using the existing palette-index path — not thetoolchannel — so multi-material prints color by active filament across manual swaps. Capability- gated: offered only when the IR actually carries anM600. Exposed through the existingcolorModeoption, so all adapters andrenderStillsupport it with no new prop.DD-009 D2 was amended (maintainer-approved) to move detection from the dialect layer to the parser and realize the "dedicated color-change channel" as this sparse events channel.
-
#168
11c75bdThanks @sobechestnut-dev! - Add opt-in retraction/deretraction markers (E9 phase 1, #148, DD-009 D1).The parser now records a sparse
retractionsevents channel onToolpathIR({ x, y, z, kind, srcByte, segIndex }, capabilityretractions) — E-only retraction moves emit no segment, so they are captured positionally in a side channel that leaves segment indices, scrub, and layer ranges untouched. The renderer draws them as opt-in always-on-top markers (warm = retract, cool = unretract) viasetShowRetractions, clipped by the current layer/scrub window and shown only when the IR actually carries events. Exposed as ashowRetractionsprop across the Vue, React, and Svelte adapters (default off).
- Updated dependencies [
1c2e5b0,11c75bd]:- @chestnutlabs/toolpath-core@0.2.0
- @chestnutlabs/gcode-containers@0.2.0
- @chestnutlabs/gcode-dialects@0.2.0
- #141
c26879fThanks @sobechestnut-dev! - First published line of the Chestnut Labs G-code Preview stack (v0.1.0, DD-008): worker-based.gcode/.gcode.3mfparsing into a versionedToolpathIR, cross-vendor dialect annotation (PrusaSlicer, Orca/Bambu, Cura, Klipper, Marlin, RepRap-flavor), a Three.js renderer with layer clipping, scrub, tubes, build plates and the honest live-progress overlay, a framework-neutral preview controller, and first-class Vue/React/Svelte adapters with capability parity.