@chestnutlabs/gcode-dialects 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/README.md +1 -1
- package/dist/annotate.d.ts +19 -1
- package/dist/annotate.d.ts.map +1 -1
- package/dist/annotate.js +54 -0
- package/dist/cnc.d.ts +10 -0
- package/dist/cnc.d.ts.map +1 -0
- package/dist/cnc.js +142 -0
- package/dist/contracts.d.ts +12 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/cura.d.ts.map +1 -1
- package/dist/cura.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/orca-bambu.d.ts.map +1 -1
- package/dist/orca-bambu.js +31 -2
- package/dist/prusaslicer.d.ts +9 -1
- package/dist/prusaslicer.d.ts.map +1 -1
- package/dist/prusaslicer.js +56 -2
- package/dist/sink.d.ts +9 -2
- package/dist/sink.d.ts.map +1 -1
- package/dist/sink.js +54 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ enforces it).
|
|
|
7
7
|
|
|
8
8
|
Supported dialects: **PrusaSlicer, OrcaSlicer/Bambu Studio, Cura, Klipper, Marlin,
|
|
9
9
|
RepRap-flavor** — see the evidence-dated
|
|
10
|
-
[compatibility matrix](
|
|
10
|
+
[compatibility matrix](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/compatibility/dialects-and-containers.md).
|
|
11
11
|
|
|
12
12
|
Every annotation is capability-tagged (`known | inferred | approximated | unavailable`); consumers
|
|
13
13
|
render what is known and disclose what is not.
|
package/dist/annotate.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* IR's byte-ordered sourceIndex resolves them to segment ranges in `finalize`
|
|
6
6
|
* (DD-005 §4.3: "annotation ranges are [segAtEvent, segBeforeNextEvent]").
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type MachineGeometry, type Point2, type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
9
9
|
import type { AnnotationSink } from './contracts.js';
|
|
10
10
|
/** First segment whose producing command starts at or after `byte`. */
|
|
11
11
|
export declare function segAtOrAfterByte(ir: ToolpathIR, byte: number): number;
|
|
@@ -19,6 +19,24 @@ export interface RangeMarker {
|
|
|
19
19
|
* value of 0 acts as a range terminator (back to unknown/none).
|
|
20
20
|
*/
|
|
21
21
|
export declare function applyMarkerRanges(ir: ToolpathIR, markers: RangeMarker[], write: (segStart: number, segEnd: number, value: number) => void): number;
|
|
22
|
+
/** One end of a wipe bracket (DD-016, #182): the source byte and whether it OPENED the bracket. */
|
|
23
|
+
export interface WipeMark {
|
|
24
|
+
srcByte: number;
|
|
25
|
+
open: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify a comment as a PrusaSlicer/OrcaSlicer wipe-bracket marker. The convention is bare
|
|
29
|
+
* `;WIPE_START` / `;WIPE_END` (the semicolon is already stripped before onComment); tolerant of a
|
|
30
|
+
* leading space and casing. Returns null for any other comment.
|
|
31
|
+
*/
|
|
32
|
+
export declare function matchWipeComment(comment: string): 'start' | 'end' | null;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve WIPE_START/END byte marks to segment ranges and add `MoveKind.Wipe` additively via the sink
|
|
35
|
+
* (DD-016 §4.2). Segments emitted between a START and its END are the wipe moves. Unbalanced or nested
|
|
36
|
+
* markers degrade safely: a stray END is ignored, a second START before an END is folded into the open
|
|
37
|
+
* bracket, and a START left open at end-of-file closes at the last segment. Returns ranges applied.
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyWipeRanges(ir: ToolpathIR, marks: WipeMark[], sink: AnnotationSink): number;
|
|
22
40
|
export interface SegMarker {
|
|
23
41
|
segIndex: number;
|
|
24
42
|
value: number;
|
package/dist/annotate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"annotate.d.ts","sourceRoot":"","sources":["../src/annotate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"annotate.d.ts","sourceRoot":"","sources":["../src/annotate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC3G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAC/D,MAAM,CAYR;AAED,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI,CAKxE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAwB/F;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,EAAE,EACpB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAC/D,MAAM,CAYR;AAED,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGpF;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAU9D;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EAAE,EAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,eAAe,CAejB;AASD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAkB5D;AAED,mGAAmG;AACnG,qBAAa,kBAAkB;IAEjB,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IADjD,OAAO,CAAC,MAAM,CAAgF;gBACjE,oBAAoB,SAAa;IAE9D,uFAAuF;IACvF,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,mFAAmF;IACnF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI;CA8BlD"}
|
package/dist/annotate.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared annotation helpers for slicer adapters (DD-005 phase 3, issue #75).
|
|
3
|
+
*
|
|
4
|
+
* Comment markers arrive with a source-byte position but no segment index; the
|
|
5
|
+
* IR's byte-ordered sourceIndex resolves them to segment ranges in `finalize`
|
|
6
|
+
* (DD-005 §4.3: "annotation ranges are [segAtEvent, segBeforeNextEvent]").
|
|
7
|
+
*/
|
|
8
|
+
import { MoveKind } from '@chestnutlabs/toolpath-core';
|
|
1
9
|
/** First segment whose producing command starts at or after `byte`. */
|
|
2
10
|
export function segAtOrAfterByte(ir, byte) {
|
|
3
11
|
const offsets = ir.sourceIndex.byteOffsets;
|
|
@@ -31,6 +39,52 @@ export function applyMarkerRanges(ir, markers, write) {
|
|
|
31
39
|
}
|
|
32
40
|
return applied;
|
|
33
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Classify a comment as a PrusaSlicer/OrcaSlicer wipe-bracket marker. The convention is bare
|
|
44
|
+
* `;WIPE_START` / `;WIPE_END` (the semicolon is already stripped before onComment); tolerant of a
|
|
45
|
+
* leading space and casing. Returns null for any other comment.
|
|
46
|
+
*/
|
|
47
|
+
export function matchWipeComment(comment) {
|
|
48
|
+
const t = comment.trim().toUpperCase();
|
|
49
|
+
if (t === 'WIPE_START')
|
|
50
|
+
return 'start';
|
|
51
|
+
if (t === 'WIPE_END')
|
|
52
|
+
return 'end';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve WIPE_START/END byte marks to segment ranges and add `MoveKind.Wipe` additively via the sink
|
|
57
|
+
* (DD-016 §4.2). Segments emitted between a START and its END are the wipe moves. Unbalanced or nested
|
|
58
|
+
* markers degrade safely: a stray END is ignored, a second START before an END is folded into the open
|
|
59
|
+
* bracket, and a START left open at end-of-file closes at the last segment. Returns ranges applied.
|
|
60
|
+
*/
|
|
61
|
+
export function applyWipeRanges(ir, marks, sink) {
|
|
62
|
+
let applied = 0;
|
|
63
|
+
let openByte = -1;
|
|
64
|
+
for (const mark of marks) {
|
|
65
|
+
if (mark.open) {
|
|
66
|
+
if (openByte < 0)
|
|
67
|
+
openByte = mark.srcByte;
|
|
68
|
+
}
|
|
69
|
+
else if (openByte >= 0) {
|
|
70
|
+
const start = segAtOrAfterByte(ir, openByte);
|
|
71
|
+
const end = segAtOrAfterByte(ir, mark.srcByte); // first segment AFTER the wipe block
|
|
72
|
+
if (end - 1 >= start) {
|
|
73
|
+
sink.addMoveKind(start, end - 1, MoveKind.Wipe);
|
|
74
|
+
applied++;
|
|
75
|
+
}
|
|
76
|
+
openByte = -1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (openByte >= 0) {
|
|
80
|
+
const start = segAtOrAfterByte(ir, openByte);
|
|
81
|
+
if (ir.segments.count - 1 >= start) {
|
|
82
|
+
sink.addMoveKind(start, ir.segments.count - 1, MoveKind.Wipe);
|
|
83
|
+
applied++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return applied;
|
|
87
|
+
}
|
|
34
88
|
/**
|
|
35
89
|
* Apply SEGMENT-indexed markers (CommandEvent.segIndex — exact, no byte
|
|
36
90
|
* resolution needed) as contiguous ranges; value 0 terminates (DD-005 phase 5).
|
package/dist/cnc.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DialectAdapter } from './contracts.js';
|
|
2
|
+
export type ValidationTier = 'validated' | 'experimental';
|
|
3
|
+
export type MachineClass = 'laser' | 'mill' | 'plotter';
|
|
4
|
+
/** GRBL diode/CO₂ laser (LightBurn and GRBL-laser post-processors). */
|
|
5
|
+
export declare function grblLaser(): DialectAdapter;
|
|
6
|
+
/** GRBL CNC milling (constant-power spindle). */
|
|
7
|
+
export declare function grblMill(): DialectAdapter;
|
|
8
|
+
/** LinuxCNC / EMC milling. */
|
|
9
|
+
export declare function linuxCnc(): DialectAdapter;
|
|
10
|
+
//# sourceMappingURL=cnc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cnc.d.ts","sourceRoot":"","sources":["../src/cnc.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAElF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,cAAc,CAAC;AAC1D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAoGxD,uEAAuE;AACvE,wBAAgB,SAAS,IAAI,cAAc,CAyB1C;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,IAAI,cAAc,CAoBzC;AAED,8BAA8B;AAC9B,wBAAgB,QAAQ,IAAI,cAAc,CAsBzC"}
|
package/dist/cnc.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** Non-extrusion capability claims whose confidence the validation tier governs (DD-012 D6). */
|
|
2
|
+
const NON_EXTRUSION_CAPS = ['cutMoves', 'toolPower', 'cannedCycles'];
|
|
3
|
+
function scoreEvidence(head) {
|
|
4
|
+
// Strip line comments (';…' and LinuxCNC/RS274 '(…)') so words inside them never score as markers.
|
|
5
|
+
const code = head
|
|
6
|
+
.split('\n')
|
|
7
|
+
.map((l) => l.split(';')[0].replace(/\([^)]*\)/g, ''))
|
|
8
|
+
.join('\n');
|
|
9
|
+
const has = (re) => re.test(code);
|
|
10
|
+
// G-code words run together without spaces (`G1M3`, `g1z-.1`), so a LEADING word-boundary fails.
|
|
11
|
+
// Match the command by its number + a trailing NON-digit lookahead: `M0*3(?!\d)` matches `M3`/`M03`
|
|
12
|
+
// (incl. mid-line `s3400 m3` and concatenated `G1M3`) but not `M30`/`M300`.
|
|
13
|
+
return {
|
|
14
|
+
// Extrusion = an `E` value on a G0–G3 MOTION line — NOT bare `E` (LinuxCNC laser uses `M67 E0 Q…`
|
|
15
|
+
// for analog power, which must not be mistaken for FDM extrusion).
|
|
16
|
+
extrusion: has(/G0*[0-3](?!\d)[^\n;]*E-?[.\d]/i) || has(/M8[23](?!\d)/i),
|
|
17
|
+
m3: has(/M0*3(?!\d)/i),
|
|
18
|
+
m4: has(/M0*4(?!\d)/i),
|
|
19
|
+
power: has(/S\d/i),
|
|
20
|
+
plunge: has(/Z\s*-\s*[.\d]/i), // negative Z incl. leading-dot decimals (`Z-.1`)
|
|
21
|
+
lightBurn: /LightBurn/i.test(head), // header-only marker; keep the raw head (comments allowed)
|
|
22
|
+
laserMode: /\$32\s*=\s*1/.test(code),
|
|
23
|
+
linuxCnc: /\b(?:LinuxCNC|EMC2?)\b/i.test(head),
|
|
24
|
+
grblBanner: /\bGrbl\s*[0-9]/i.test(head),
|
|
25
|
+
oWord: has(/[oO][\s<]*\w*\s*(?:sub|call|if|while|do|repeat|return|endsub)\b/im)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeCncDialect(spec) {
|
|
29
|
+
return {
|
|
30
|
+
id: spec.id,
|
|
31
|
+
displayName: spec.displayName,
|
|
32
|
+
kind: 'firmware',
|
|
33
|
+
detect(input) {
|
|
34
|
+
const hit = spec.detect(input);
|
|
35
|
+
return hit === null
|
|
36
|
+
? null
|
|
37
|
+
: { dialectId: spec.id, kind: 'firmware', confidence: hit.confidence, evidence: hit.evidence };
|
|
38
|
+
},
|
|
39
|
+
finalize(ir, sink) {
|
|
40
|
+
sink.setRaw('cnc.controller', spec.id);
|
|
41
|
+
sink.setRaw('cnc.machineClass', spec.machineClass);
|
|
42
|
+
sink.setRaw('cnc.validationTier', spec.tier);
|
|
43
|
+
sink.setRaw('cnc.toolPowerLabel', spec.toolPowerLabel);
|
|
44
|
+
// Validation tier (DD-012 D6): until hardware-validated, non-extrusion claims are `inferred`,
|
|
45
|
+
// never `known`. Only downgrade claims the file actually made (present as 'known') — never
|
|
46
|
+
// fabricate a claim for a feature the file did not use.
|
|
47
|
+
if (spec.tier === 'experimental') {
|
|
48
|
+
let downgraded = false;
|
|
49
|
+
for (const cap of NON_EXTRUSION_CAPS) {
|
|
50
|
+
if (ir.header.capabilities[cap] === 'known') {
|
|
51
|
+
sink.upgradeCapability(cap, 'inferred');
|
|
52
|
+
downgraded = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (downgraded) {
|
|
56
|
+
sink.warn('cnc-dialect-experimental', `${spec.displayName} support is EXPERIMENTAL (spec-derived, not yet hardware-validated); ` +
|
|
57
|
+
`non-extrusion classification is reported as 'inferred'.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** GRBL diode/CO₂ laser (LightBurn and GRBL-laser post-processors). */
|
|
64
|
+
export function grblLaser() {
|
|
65
|
+
return makeCncDialect({
|
|
66
|
+
id: 'grbl-laser',
|
|
67
|
+
displayName: 'GRBL laser (LightBurn)',
|
|
68
|
+
machineClass: 'laser',
|
|
69
|
+
toolPowerLabel: 'laser power (S)',
|
|
70
|
+
// Hardware-validated 2026-07-29 (DD-012 D8; see docs/design/DD-012-hardware-validation-log.md):
|
|
71
|
+
// a real GRBL/LightBurn laser run — 6161 moves incl. fill + offset-fill, full 0–1000 S power ramp —
|
|
72
|
+
// confirmed machine-class detection, Cut-vs-rapid classification, and the toolPower channel against
|
|
73
|
+
// the physical cut. Claims report `known`, not `inferred`. (Mill/LinuxCNC remain experimental.)
|
|
74
|
+
tier: 'validated',
|
|
75
|
+
detect(input) {
|
|
76
|
+
const e = scoreEvidence(input.headText);
|
|
77
|
+
if (e.extrusion || e.linuxCnc)
|
|
78
|
+
return null;
|
|
79
|
+
if (e.lightBurn)
|
|
80
|
+
return { evidence: 'LightBurn header', confidence: 'known' };
|
|
81
|
+
if (e.laserMode)
|
|
82
|
+
return { evidence: 'GRBL $32=1 (laser mode)', confidence: 'inferred' };
|
|
83
|
+
// No-header GRBL laser (the common LaserGRBL case): a tool-on command with power, and NO Z
|
|
84
|
+
// plunging — lasers are planar; a mill would cut down into Z. Works whether the file uses M3
|
|
85
|
+
// (constant power) or M4 (dynamic).
|
|
86
|
+
if ((e.m3 || e.m4) && e.power && !e.plunge) {
|
|
87
|
+
return { evidence: `${e.m4 ? 'M4 dynamic' : 'M3'} + S power, planar (no Z plunge)`, confidence: 'inferred' };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** GRBL CNC milling (constant-power spindle). */
|
|
94
|
+
export function grblMill() {
|
|
95
|
+
return makeCncDialect({
|
|
96
|
+
id: 'grbl-mill',
|
|
97
|
+
displayName: 'GRBL mill',
|
|
98
|
+
machineClass: 'mill',
|
|
99
|
+
toolPowerLabel: 'spindle RPM (S)',
|
|
100
|
+
tier: 'experimental', // → 'validated' after a real GRBL-mill run (DD-012 §8)
|
|
101
|
+
detect(input) {
|
|
102
|
+
const e = scoreEvidence(input.headText);
|
|
103
|
+
if (e.extrusion || e.lightBurn || e.laserMode || e.linuxCnc)
|
|
104
|
+
return null;
|
|
105
|
+
// A spindle (M3) that plunges into the material (negative Z) with no extrusion — a milling
|
|
106
|
+
// fingerprint that distinguishes it from a planar laser. GRBL/family-level, so a banner is a
|
|
107
|
+
// stronger signal but not required (most CAM output has no banner).
|
|
108
|
+
if (e.m3 && e.plunge) {
|
|
109
|
+
const ev = e.grblBanner ? 'Grbl banner + M3 + Z-plunge' : 'M3 spindle + Z-plunge, no extrusion';
|
|
110
|
+
return { evidence: ev, confidence: 'inferred' };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** LinuxCNC / EMC milling. */
|
|
117
|
+
export function linuxCnc() {
|
|
118
|
+
return makeCncDialect({
|
|
119
|
+
id: 'linuxcnc',
|
|
120
|
+
displayName: 'LinuxCNC mill',
|
|
121
|
+
machineClass: 'mill',
|
|
122
|
+
toolPowerLabel: 'spindle RPM (S)',
|
|
123
|
+
tier: 'experimental', // → 'validated' after a real LinuxCNC run (DD-012 §8)
|
|
124
|
+
detect(input) {
|
|
125
|
+
const e = scoreEvidence(input.headText);
|
|
126
|
+
if (e.extrusion)
|
|
127
|
+
return null;
|
|
128
|
+
if (e.linuxCnc)
|
|
129
|
+
return { evidence: 'LinuxCNC/EMC header', confidence: 'known' };
|
|
130
|
+
// RS274NGC O-word subroutines / flow control (`O100 sub`, `o<name> call`) are the LinuxCNC
|
|
131
|
+
// family's distinguishing dialect feature — GRBL/TinyG do not have them.
|
|
132
|
+
if (e.oWord)
|
|
133
|
+
return { evidence: 'RS274NGC O-word subroutine/flow', confidence: 'inferred' };
|
|
134
|
+
// A `%` program envelope with a spindle and no extrusion (also common in Fanuc-style output;
|
|
135
|
+
// reported family-level, not as a specific controller).
|
|
136
|
+
if (/^\s*%/m.test(input.headText) && e.m3) {
|
|
137
|
+
return { evidence: '%-program envelope + M3 spindle, no extrusion', confidence: 'inferred' };
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/dist/contracts.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Data contracts (MachineGeometry, DialectMetadata, DialectDetection) live in
|
|
10
10
|
* @chestnutlabs/toolpath-core (see its metadata.ts for the rationale).
|
|
11
11
|
*/
|
|
12
|
-
import type { Confidence, DialectDetection, MachineGeometry, FilamentInfo, ThumbnailData, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
12
|
+
import type { Confidence, DialectDetection, MachineGeometry, FilamentInfo, FilamentUsage, PrintEstimate, ThumbnailData, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
13
13
|
/** Read-only normalized command event — mirror of the parser's hook payload. */
|
|
14
14
|
export interface CommandEvent {
|
|
15
15
|
gcode: string;
|
|
@@ -29,11 +29,22 @@ export interface DetectInput {
|
|
|
29
29
|
export interface AnnotationSink {
|
|
30
30
|
/** Write a FeatureRole index for segments [segStart, segEnd] (bounds validated at apply). */
|
|
31
31
|
setFeature(segStart: number, segEnd: number, role: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Additively OR an annotation move-kind bit onto segments [segStart, segEnd] (DD-016 §4.2).
|
|
34
|
+
* Allow-listed to `MoveKind.Wipe | MoveKind.Seam` — the ONLY exception to the sink's kind
|
|
35
|
+
* immutability, and additive-only: it never clears an existing Extrude/Travel bit and never
|
|
36
|
+
* reclassifies a move. Non-allow-listed bits are dropped with a warning (bounded failure).
|
|
37
|
+
*/
|
|
38
|
+
addMoveKind(segStart: number, segEnd: number, kindBits: number): void;
|
|
32
39
|
/** `objectValue` is the 1-based object-channel value (DD-001: v indexes ir.objects[v-1]). */
|
|
33
40
|
setObject(segStart: number, segEnd: number, objectValue: number): void;
|
|
34
41
|
defineObject(objectValue: number, name: string): void;
|
|
35
42
|
setMachine(machine: MachineGeometry): void;
|
|
36
43
|
setFilament(info: FilamentInfo): void;
|
|
44
|
+
/** Slicer-reported total filament consumption (#183). Later calls merge non-undefined fields. */
|
|
45
|
+
setFilamentUsage(usage: FilamentUsage): void;
|
|
46
|
+
/** The slicer's own print-time estimate (#183). Prefer the 'normal'/default mode when several. */
|
|
47
|
+
setPrintEstimate(estimate: PrintEstimate): void;
|
|
37
48
|
/** Enrich a tool's identity (material, '#RRGGBB' color) — merged into ir.tools (DD-005 phase 5). */
|
|
38
49
|
setToolInfo(tool: number, info: {
|
|
39
50
|
material?: string;
|
package/dist/contracts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,6BAA6B,CAAC;AAErC,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACjE,6FAA6F;IAC7F,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACvE,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC3C,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,oGAAoG;IACpG,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChF,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;IACxC,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACpD,gFAAgF;IAChF,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3F,SAAS,CAAC,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9E,QAAQ,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACzE"}
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"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"}
|
package/dist/cura.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cura.d.ts","sourceRoot":"","sources":["../src/cura.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiCrE,wBAAgB,IAAI,IAAI,cAAc,
|
|
1
|
+
{"version":3,"file":"cura.d.ts","sourceRoot":"","sources":["../src/cura.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiCrE,wBAAgB,IAAI,IAAI,cAAc,CAqDrC"}
|
package/dist/cura.js
CHANGED
|
@@ -54,6 +54,25 @@ export function cura() {
|
|
|
54
54
|
s.printerName = trimmed.slice('TARGET_MACHINE.NAME:'.length).trim();
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
+
// Cura print-time comment: `;TIME:<seconds>` (total; per-layer `;TIME_ELAPSED:` is ignored).
|
|
58
|
+
if (trimmed.startsWith('TIME:')) {
|
|
59
|
+
const sec = Number(trimmed.slice('TIME:'.length).trim());
|
|
60
|
+
if (Number.isFinite(sec) && sec > 0) {
|
|
61
|
+
sink.setPrintEstimate({ seconds: sec, source: { adapterId: 'cura', evidence: ';TIME:', srcByte } });
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Cura filament comment: `;Filament used: 2.22m` (metres; multi-material → first value).
|
|
66
|
+
if (trimmed.toLowerCase().startsWith('filament used:')) {
|
|
67
|
+
const metres = parseFloat(trimmed.slice('filament used:'.length).trim());
|
|
68
|
+
if (Number.isFinite(metres)) {
|
|
69
|
+
sink.setFilamentUsage({
|
|
70
|
+
lengthMm: metres * 1000,
|
|
71
|
+
source: { adapterId: 'cura', evidence: ';Filament used:', srcByte }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
57
76
|
const kv = parseKeyValue(comment);
|
|
58
77
|
if (kv !== null && kv.key === 'target_machine')
|
|
59
78
|
s.printerName = kv.value;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { prusaSlicer } from './prusaslicer.js';
|
|
|
15
15
|
export { orcaBambu } from './orca-bambu.js';
|
|
16
16
|
export { cura } from './cura.js';
|
|
17
17
|
export { klipper, marlin, repRap } from './firmware.js';
|
|
18
|
+
export { grblLaser, grblMill, linuxCnc } from './cnc.js';
|
|
19
|
+
export type { ValidationTier, MachineClass } from './cnc.js';
|
|
18
20
|
export { segAtOrAfterByte, applyMarkerRanges, parseKeyValue, parseAreaPoints, bedFromPoints, decodeBase64, ThumbnailCollector } from './annotate.js';
|
|
19
21
|
export type { RangeMarker } from './annotate.js';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,UAAU,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,aAAa,EACb,YAAY,EACZ,kBAAkB,EACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,UAAU,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,aAAa,EACb,YAAY,EACZ,kBAAkB,EACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { prusaSlicer } from './prusaslicer.js';
|
|
|
4
4
|
export { orcaBambu } from './orca-bambu.js';
|
|
5
5
|
export { cura } from './cura.js';
|
|
6
6
|
export { klipper, marlin, repRap } from './firmware.js';
|
|
7
|
+
export { grblLaser, grblMill, linuxCnc } from './cnc.js';
|
|
7
8
|
export { segAtOrAfterByte, applyMarkerRanges, parseKeyValue, parseAreaPoints, bedFromPoints, decodeBase64, ThumbnailCollector } from './annotate.js';
|
package/dist/orca-bambu.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orca-bambu.d.ts","sourceRoot":"","sources":["../src/orca-bambu.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"orca-bambu.d.ts","sourceRoot":"","sources":["../src/orca-bambu.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAmErE,wBAAgB,SAAS,IAAI,cAAc,CAiH1C"}
|
package/dist/orca-bambu.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* container metadata as evidence (DD-005 §4.1 DetectInput.containerMeta).
|
|
10
10
|
*/
|
|
11
11
|
import { FeatureRole } from '@chestnutlabs/toolpath-core';
|
|
12
|
-
import {
|
|
12
|
+
import { parseFilamentTotals, parseHmsToSeconds } from './prusaslicer.js';
|
|
13
|
+
import { ThumbnailCollector, applyMarkerRanges, applyWipeRanges, bedFromPoints, matchWipeComment, parseAreaPoints, parseKeyValue } from './annotate.js';
|
|
13
14
|
/** Orca/Bambu `; FEATURE:` names → DD-001 FeatureRole. */
|
|
14
15
|
const FEATURE_MAP = {
|
|
15
16
|
'inner wall': FeatureRole.Perimeter,
|
|
@@ -34,7 +35,14 @@ const state = new WeakMap();
|
|
|
34
35
|
function stateFor(sink) {
|
|
35
36
|
let s = state.get(sink);
|
|
36
37
|
if (s === undefined) {
|
|
37
|
-
s = {
|
|
38
|
+
s = {
|
|
39
|
+
features: [],
|
|
40
|
+
objects: [],
|
|
41
|
+
wipes: [],
|
|
42
|
+
objectNames: new Map(),
|
|
43
|
+
bedPoints: null,
|
|
44
|
+
thumbs: new ThumbnailCollector()
|
|
45
|
+
};
|
|
38
46
|
state.set(sink, s);
|
|
39
47
|
}
|
|
40
48
|
return s;
|
|
@@ -79,10 +87,29 @@ export function orcaBambu() {
|
|
|
79
87
|
s.objects.push({ srcByte, value: 0 }); // range terminator
|
|
80
88
|
return;
|
|
81
89
|
}
|
|
90
|
+
const wipe = matchWipeComment(comment);
|
|
91
|
+
if (wipe !== null) {
|
|
92
|
+
s.wipes.push({ srcByte, open: wipe === 'start' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
82
95
|
if (s.thumbs.isActive || trimmed.toLowerCase().startsWith('thumbnail')) {
|
|
83
96
|
s.thumbs.feed(comment, sink);
|
|
84
97
|
return;
|
|
85
98
|
}
|
|
99
|
+
// Orca/Bambu print-time comments are colon-separated (`; total estimated time: 1h 2m 3s`).
|
|
100
|
+
const timeMatch = /^(?:total estimated time|model printing time)\s*:\s*(.+)$/i.exec(trimmed);
|
|
101
|
+
if (timeMatch !== null) {
|
|
102
|
+
const sec = parseHmsToSeconds(timeMatch[1]);
|
|
103
|
+
if (sec !== null) {
|
|
104
|
+
sink.setPrintEstimate({
|
|
105
|
+
seconds: sec,
|
|
106
|
+
source: { adapterId: 'orca-bambu', evidence: `; ${trimmed}`, srcByte }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (parseFilamentTotals(comment, srcByte, 'orca-bambu', sink))
|
|
112
|
+
return;
|
|
86
113
|
const kv = parseKeyValue(comment);
|
|
87
114
|
if (kv === null)
|
|
88
115
|
return;
|
|
@@ -116,6 +143,8 @@ export function orcaBambu() {
|
|
|
116
143
|
sink.defineObject(value, name);
|
|
117
144
|
sink.upgradeCapability('objects', 'known');
|
|
118
145
|
}
|
|
146
|
+
if (applyWipeRanges(ir, s.wipes, sink) > 0)
|
|
147
|
+
sink.upgradeCapability('wipeMoves', 'known');
|
|
119
148
|
if (s.bedPoints !== null) {
|
|
120
149
|
const points = parseAreaPoints(s.bedPoints);
|
|
121
150
|
if (points !== null) {
|
package/dist/prusaslicer.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
import type { DialectAdapter } from './contracts.js';
|
|
1
|
+
import type { AnnotationSink, DialectAdapter } from './contracts.js';
|
|
2
|
+
/** Parse PrusaSlicer/Orca `<h>h <m>m <s>s` (any part optional) → seconds; null when zero/unparsed. */
|
|
3
|
+
export declare function parseHmsToSeconds(text: string): number | null;
|
|
4
|
+
/**
|
|
5
|
+
* Prusa-style filament totals (`; filament used [mm|cm3] = X`, `; total filament used [g] = X`) —
|
|
6
|
+
* shared by PrusaSlicer and the Prusa-derived Orca/Bambu adapter (#183). Multi-word bracketed keys
|
|
7
|
+
* that `parseKeyValue` can't tokenize. Returns true when the comment was a filament total.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseFilamentTotals(comment: string, srcByte: number, adapterId: string, sink: AnnotationSink): boolean;
|
|
2
10
|
export declare function prusaSlicer(): DialectAdapter;
|
|
3
11
|
//# sourceMappingURL=prusaslicer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prusaslicer.d.ts","sourceRoot":"","sources":["../src/prusaslicer.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"prusaslicer.d.ts","sourceRoot":"","sources":["../src/prusaslicer.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAgCrE,sGAAsG;AACtG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,GACnB,OAAO,CAkBT;AAuBD,wBAAgB,WAAW,IAAI,cAAc,CAmF5C"}
|
package/dist/prusaslicer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `; thumbnail begin/end` PNG thumbnails. Geometry is untouchable by contract.
|
|
8
8
|
*/
|
|
9
9
|
import { FeatureRole } from '@chestnutlabs/toolpath-core';
|
|
10
|
-
import { ThumbnailCollector, applyMarkerRanges, bedFromPoints, parseAreaPoints, parseKeyValue } from './annotate.js';
|
|
10
|
+
import { ThumbnailCollector, applyMarkerRanges, applyWipeRanges, bedFromPoints, matchWipeComment, parseAreaPoints, parseKeyValue } from './annotate.js';
|
|
11
11
|
/** Prusa `;TYPE:` names → DD-001 FeatureRole (unknown names → Custom, honestly generic). */
|
|
12
12
|
const TYPE_MAP = {
|
|
13
13
|
perimeter: FeatureRole.Perimeter,
|
|
@@ -26,11 +26,43 @@ const TYPE_MAP = {
|
|
|
26
26
|
'wipe tower': FeatureRole.Custom,
|
|
27
27
|
custom: FeatureRole.Custom
|
|
28
28
|
};
|
|
29
|
+
/** Parse PrusaSlicer/Orca `<h>h <m>m <s>s` (any part optional) → seconds; null when zero/unparsed. */
|
|
30
|
+
export function parseHmsToSeconds(text) {
|
|
31
|
+
const m = /^\s*(?:(\d+)\s*h)?\s*(?:(\d+)\s*m)?\s*(?:(\d+)\s*s)?\s*$/.exec(text);
|
|
32
|
+
if (m === null)
|
|
33
|
+
return null;
|
|
34
|
+
const total = Number(m[1] ?? 0) * 3600 + Number(m[2] ?? 0) * 60 + Number(m[3] ?? 0);
|
|
35
|
+
return total > 0 ? total : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Prusa-style filament totals (`; filament used [mm|cm3] = X`, `; total filament used [g] = X`) —
|
|
39
|
+
* shared by PrusaSlicer and the Prusa-derived Orca/Bambu adapter (#183). Multi-word bracketed keys
|
|
40
|
+
* that `parseKeyValue` can't tokenize. Returns true when the comment was a filament total.
|
|
41
|
+
*/
|
|
42
|
+
export function parseFilamentTotals(comment, srcByte, adapterId, sink) {
|
|
43
|
+
const source = { adapterId, evidence: '; filament used comments', srcByte };
|
|
44
|
+
let m = /^\s*filament used \[mm\]\s*=\s*([\d.]+)/i.exec(comment);
|
|
45
|
+
if (m !== null) {
|
|
46
|
+
sink.setFilamentUsage({ lengthMm: parseFloat(m[1]), source });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
m = /^\s*filament used \[cm3\]\s*=\s*([\d.]+)/i.exec(comment);
|
|
50
|
+
if (m !== null) {
|
|
51
|
+
sink.setFilamentUsage({ volumeCm3: parseFloat(m[1]), source });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
m = /^\s*total filament used \[g\]\s*=\s*([\d.]+)/i.exec(comment);
|
|
55
|
+
if (m !== null) {
|
|
56
|
+
sink.setFilamentUsage({ weightG: parseFloat(m[1]), source });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
29
61
|
const state = new WeakMap();
|
|
30
62
|
function stateFor(sink) {
|
|
31
63
|
let s = state.get(sink);
|
|
32
64
|
if (s === undefined) {
|
|
33
|
-
s = { markers: [], bedPoints: null, thumbs: new ThumbnailCollector() };
|
|
65
|
+
s = { markers: [], wipes: [], bedPoints: null, thumbs: new ThumbnailCollector() };
|
|
34
66
|
state.set(sink, s);
|
|
35
67
|
}
|
|
36
68
|
return s;
|
|
@@ -58,10 +90,30 @@ export function prusaSlicer() {
|
|
|
58
90
|
s.markers.push({ srcByte, value: role ?? FeatureRole.Custom });
|
|
59
91
|
return;
|
|
60
92
|
}
|
|
93
|
+
const wipe = matchWipeComment(comment);
|
|
94
|
+
if (wipe !== null) {
|
|
95
|
+
s.wipes.push({ srcByte, open: wipe === 'start' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
61
98
|
if (s.thumbs.isActive || comment.trimStart().toLowerCase().startsWith('thumbnail')) {
|
|
62
99
|
s.thumbs.feed(comment, sink);
|
|
63
100
|
return;
|
|
64
101
|
}
|
|
102
|
+
// Filament totals + print time — multi-word keys `parseKeyValue` can't tokenize (#183).
|
|
103
|
+
if (parseFilamentTotals(comment, srcByte, 'prusaslicer', sink))
|
|
104
|
+
return;
|
|
105
|
+
const time = /^\s*estimated printing time \(normal mode\)\s*=\s*(.+)$/i.exec(comment);
|
|
106
|
+
if (time !== null) {
|
|
107
|
+
const sec = parseHmsToSeconds(time[1].trim());
|
|
108
|
+
if (sec !== null) {
|
|
109
|
+
sink.setPrintEstimate({
|
|
110
|
+
seconds: sec,
|
|
111
|
+
mode: 'normal',
|
|
112
|
+
source: { adapterId: 'prusaslicer', evidence: '; estimated printing time', srcByte }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
65
117
|
const kv = parseKeyValue(comment);
|
|
66
118
|
if (kv === null)
|
|
67
119
|
return;
|
|
@@ -83,6 +135,8 @@ export function prusaSlicer() {
|
|
|
83
135
|
const applied = applyMarkerRanges(ir, s.markers, (a, b, v) => sink.setFeature(a, b, v));
|
|
84
136
|
if (applied > 0)
|
|
85
137
|
sink.upgradeCapability('featureRoles', 'known');
|
|
138
|
+
if (applyWipeRanges(ir, s.wipes, sink) > 0)
|
|
139
|
+
sink.upgradeCapability('wipeMoves', 'known');
|
|
86
140
|
if (s.bedPoints !== null) {
|
|
87
141
|
const points = parseAreaPoints(s.bedPoints);
|
|
88
142
|
if (points !== null) {
|
package/dist/sink.d.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* exist until `finish()`), then applied to the finished IR in one validated
|
|
6
6
|
* pass. Bounds are clamped, budgets enforced, and the sink physically cannot
|
|
7
7
|
* reach geometry — it only ever touches `feature`, `object`, header
|
|
8
|
-
* capabilities/warnings,
|
|
8
|
+
* capabilities/warnings, the metadata object beside the IR, and (DD-016 §4.2)
|
|
9
|
+
* an ADDITIVE allow-listed move-kind bit (`Wipe`/`Seam`) that never clears or
|
|
10
|
+
* reclassifies a move.
|
|
9
11
|
*/
|
|
10
|
-
import type
|
|
12
|
+
import { type Confidence, type DialectMetadata, type FilamentInfo, type FilamentUsage, type MachineGeometry, type PrintEstimate, type ThumbnailData, type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
11
13
|
import type { AnnotationSink } from './contracts.js';
|
|
12
14
|
export declare class BufferedAnnotationSink implements AnnotationSink {
|
|
13
15
|
private readonly adapterId;
|
|
@@ -15,6 +17,8 @@ export declare class BufferedAnnotationSink implements AnnotationSink {
|
|
|
15
17
|
private objects;
|
|
16
18
|
private machine;
|
|
17
19
|
private filaments;
|
|
20
|
+
private filamentUsage;
|
|
21
|
+
private printEstimate;
|
|
18
22
|
private toolInfos;
|
|
19
23
|
private thumbnails;
|
|
20
24
|
private raw;
|
|
@@ -25,9 +29,12 @@ export declare class BufferedAnnotationSink implements AnnotationSink {
|
|
|
25
29
|
private pushOp;
|
|
26
30
|
setFeature(segStart: number, segEnd: number, role: number): void;
|
|
27
31
|
setObject(segStart: number, segEnd: number, objectId: number): void;
|
|
32
|
+
addMoveKind(segStart: number, segEnd: number, kindBits: number): void;
|
|
28
33
|
defineObject(id: number, name: string): void;
|
|
29
34
|
setMachine(machine: MachineGeometry): void;
|
|
30
35
|
setFilament(info: FilamentInfo): void;
|
|
36
|
+
setFilamentUsage(usage: FilamentUsage): void;
|
|
37
|
+
setPrintEstimate(estimate: PrintEstimate): void;
|
|
31
38
|
setToolInfo(tool: number, info: {
|
|
32
39
|
material?: string;
|
|
33
40
|
colorHex?: string;
|
package/dist/sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,UAAU,EAEhB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAcrD,qBAAa,sBAAuB,YAAW,cAAc;IAc/C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAbtC,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,SAAS,CAA+D;IAChF,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,GAAG,CAA6B;IACxC,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAS;gBAEI,SAAS,EAAE,MAAM;IAE9C,OAAO,CAAC,MAAM;IAYd,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInE,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAcrE,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI5C,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAI1C,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIrC,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAW5C,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAK/C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAI/E,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAIxC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAI5D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAS3D;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;CAoE9G"}
|
package/dist/sink.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnnotationSink implementation (DD-005 §4.1).
|
|
3
|
+
*
|
|
4
|
+
* Writes are BUFFERED as ops during the parse (the IR's final channels don't
|
|
5
|
+
* exist until `finish()`), then applied to the finished IR in one validated
|
|
6
|
+
* pass. Bounds are clamped, budgets enforced, and the sink physically cannot
|
|
7
|
+
* reach geometry — it only ever touches `feature`, `object`, header
|
|
8
|
+
* capabilities/warnings, the metadata object beside the IR, and (DD-016 §4.2)
|
|
9
|
+
* an ADDITIVE allow-listed move-kind bit (`Wipe`/`Seam`) that never clears or
|
|
10
|
+
* reclassifies a move.
|
|
11
|
+
*/
|
|
12
|
+
import { MoveKind } from '@chestnutlabs/toolpath-core';
|
|
13
|
+
/** The only move-kind bits an adapter may add (DD-016 §4.2/D6) — additive annotation kinds, never motion. */
|
|
14
|
+
const ALLOWED_ANNOTATION_KINDS = MoveKind.Wipe | MoveKind.Seam;
|
|
1
15
|
const MAX_RANGE_OPS = 1_000_000;
|
|
2
16
|
const MAX_OBJECTS = 10_000;
|
|
3
17
|
const MAX_RAW_ENTRIES = 512;
|
|
@@ -10,6 +24,8 @@ export class BufferedAnnotationSink {
|
|
|
10
24
|
objects = new Map();
|
|
11
25
|
machine = null;
|
|
12
26
|
filaments = [];
|
|
27
|
+
filamentUsage = null;
|
|
28
|
+
printEstimate = null;
|
|
13
29
|
toolInfos = new Map();
|
|
14
30
|
thumbnails = [];
|
|
15
31
|
raw = new Map();
|
|
@@ -37,6 +53,16 @@ export class BufferedAnnotationSink {
|
|
|
37
53
|
setObject(segStart, segEnd, objectId) {
|
|
38
54
|
this.pushOp({ channel: 'object', start: segStart, end: segEnd, value: objectId >>> 0 });
|
|
39
55
|
}
|
|
56
|
+
addMoveKind(segStart, segEnd, kindBits) {
|
|
57
|
+
// Allow-list guard (DD-016 D6): only additive Wipe/Seam bits; anything else is a contract
|
|
58
|
+
// violation, dropped with a bounded warning rather than corrupting the kind channel.
|
|
59
|
+
const bits = kindBits & ALLOWED_ANNOTATION_KINDS;
|
|
60
|
+
if (bits === 0 || (kindBits & ~ALLOWED_ANNOTATION_KINDS) !== 0) {
|
|
61
|
+
this.warn('dialect-kind-not-allowlisted', `adapter '${this.adapterId}' tried to set non-allow-listed move-kind bits 0x${(kindBits >>> 0).toString(16)}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.pushOp({ channel: 'kind', start: segStart, end: segEnd, value: bits });
|
|
65
|
+
}
|
|
40
66
|
defineObject(id, name) {
|
|
41
67
|
if (this.objects.size < MAX_OBJECTS)
|
|
42
68
|
this.objects.set(id, name.slice(0, 256));
|
|
@@ -48,6 +74,21 @@ export class BufferedAnnotationSink {
|
|
|
48
74
|
if (this.filaments.length < 64)
|
|
49
75
|
this.filaments.push(info);
|
|
50
76
|
}
|
|
77
|
+
setFilamentUsage(usage) {
|
|
78
|
+
// Merge non-undefined fields (a slicer emits length/volume/weight on separate comment lines).
|
|
79
|
+
const prev = this.filamentUsage;
|
|
80
|
+
this.filamentUsage = {
|
|
81
|
+
lengthMm: usage.lengthMm ?? prev?.lengthMm,
|
|
82
|
+
volumeCm3: usage.volumeCm3 ?? prev?.volumeCm3,
|
|
83
|
+
weightG: usage.weightG ?? prev?.weightG,
|
|
84
|
+
source: prev?.source ?? usage.source
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
setPrintEstimate(estimate) {
|
|
88
|
+
// First estimate wins (adapters send the default/'normal' mode first; ignore later silent-mode).
|
|
89
|
+
if (this.printEstimate === null)
|
|
90
|
+
this.printEstimate = estimate;
|
|
91
|
+
}
|
|
51
92
|
setToolInfo(tool, info) {
|
|
52
93
|
if (this.toolInfos.size < 64)
|
|
53
94
|
this.toolInfos.set(tool, info);
|
|
@@ -86,6 +127,13 @@ export class BufferedAnnotationSink {
|
|
|
86
127
|
const end = Math.min(op.end + 1, count);
|
|
87
128
|
if (end <= start)
|
|
88
129
|
continue;
|
|
130
|
+
if (op.channel === 'kind') {
|
|
131
|
+
// DD-016: additive OR — preserve the base Extrude/Travel classification, never overwrite it.
|
|
132
|
+
const kind = ir.segments.kind;
|
|
133
|
+
for (let i = start; i < end; i++)
|
|
134
|
+
kind[i] |= op.value;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
89
137
|
ir.segments[op.channel].fill(op.value, start, end);
|
|
90
138
|
touched[op.channel] = true;
|
|
91
139
|
}
|
|
@@ -130,6 +178,12 @@ export class BufferedAnnotationSink {
|
|
|
130
178
|
if (this.filaments.length > 0) {
|
|
131
179
|
metadata.filaments = [...(metadata.filaments ?? []), ...this.filaments];
|
|
132
180
|
}
|
|
181
|
+
if (this.filamentUsage !== null && metadata.filamentUsage === undefined) {
|
|
182
|
+
metadata.filamentUsage = this.filamentUsage;
|
|
183
|
+
}
|
|
184
|
+
if (this.printEstimate !== null && metadata.printEstimate === undefined) {
|
|
185
|
+
metadata.printEstimate = this.printEstimate;
|
|
186
|
+
}
|
|
133
187
|
if (this.thumbnails.length > 0) {
|
|
134
188
|
metadata.thumbnails = [...(metadata.thumbnails ?? []), ...this.thumbnails];
|
|
135
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chestnutlabs/gcode-dialects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Slicer/firmware dialect adapters for ToolpathIR annotation (DD-005). Adapters annotate metadata and optional channels — they can never alter geometry.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gcode",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"test": "vitest run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chestnutlabs/toolpath-core": "0.
|
|
48
|
+
"@chestnutlabs/toolpath-core": "0.4.0"
|
|
49
49
|
}
|
|
50
50
|
}
|