@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.
- package/dist/growable.d.ts +16 -1
- package/dist/growable.d.ts.map +1 -1
- package/dist/growable.js +38 -5
- package/dist/parse.d.ts +15 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +554 -111
- package/dist/worker.js +10 -4
- package/package.json +5 -4
package/dist/growable.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/growable.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
-
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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,
|
package/dist/parse.d.ts.map
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
45
|
-
const
|
|
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
|
|
51
|
-
// Faithful replication of the inherited parseParams reduce (odd indices are values):
|
|
58
|
+
const codes = [];
|
|
52
59
|
const params = {};
|
|
53
|
-
|
|
54
|
-
for (let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
383
|
+
if (eDelta > 0)
|
|
259
384
|
stats.retractions++;
|
|
260
|
-
else if (
|
|
385
|
+
else if (eDelta < 0)
|
|
261
386
|
stats.deretractions++;
|
|
262
387
|
else if (f !== undefined)
|
|
263
388
|
stats.feedrateChanges++;
|
|
264
|
-
if (e !== undefined &&
|
|
389
|
+
if (e !== undefined && eDelta !== 0) {
|
|
265
390
|
retractionEvents.push({
|
|
266
391
|
x: sx,
|
|
267
392
|
y: sy,
|
|
268
393
|
z: sz,
|
|
269
|
-
kind:
|
|
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
|
-
|
|
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 (
|
|
288
|
-
stats.extrusionDistance +=
|
|
289
|
-
sx = x
|
|
290
|
-
sy = y
|
|
291
|
-
sz = z
|
|
292
|
-
|
|
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 {
|
|
296
|
-
let {
|
|
433
|
+
const { e, f } = p;
|
|
434
|
+
let { r } = p;
|
|
297
435
|
if (f !== undefined)
|
|
298
436
|
modalFeed = f;
|
|
299
|
-
|
|
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 (
|
|
304
|
-
stats.extrusionDistance +=
|
|
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
|
|
307
|
-
const
|
|
308
|
-
const minR = Math.sqrt(Math.pow(
|
|
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(
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
const wholeCircle =
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
const arcRadius = Math.sqrt(
|
|
322
|
-
const arcCurrentAngle = Math.atan2(-
|
|
323
|
-
const finalTheta = Math.atan2(
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!
|
|
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 =
|
|
357
|
-
sy =
|
|
358
|
-
sz =
|
|
359
|
-
return
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.
|
|
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.
|
|
53
|
-
"@chestnutlabs/gcode-dialects": "0.
|
|
54
|
-
"@chestnutlabs/gcode-containers": "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
|
}
|