@chestnutlabs/gcode-dialects 0.1.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 +19 -0
- package/dist/annotate.d.ts +52 -0
- package/dist/annotate.d.ts.map +1 -0
- package/dist/annotate.js +157 -0
- package/dist/contracts.d.ts +58 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +1 -0
- package/dist/cura.d.ts +3 -0
- package/dist/cura.d.ts.map +1 -0
- package/dist/cura.js +70 -0
- package/dist/firmware.d.ts +5 -0
- package/dist/firmware.d.ts.map +1 -0
- package/dist/firmware.js +109 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/orca-bambu.d.ts +3 -0
- package/dist/orca-bambu.d.ts.map +1 -0
- package/dist/orca-bambu.js +138 -0
- package/dist/prusaslicer.d.ts +3 -0
- package/dist/prusaslicer.d.ts.map +1 -0
- package/dist/prusaslicer.js +99 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +87 -0
- package/dist/sink.d.ts +47 -0
- package/dist/sink.d.ts.map +1 -0
- package/dist/sink.js +140 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @chestnutlabs/gcode-dialects
|
|
2
|
+
|
|
3
|
+
Slicer/firmware **dialect adapters** for the Chestnut Labs G-code Preview stack (design: DD-005).
|
|
4
|
+
Dialect adapters *annotate* a parse — features, object boundaries, bed geometry, thumbnails,
|
|
5
|
+
multi-tool metadata — and are forbidden from changing geometry (a geometry-invariance gate
|
|
6
|
+
enforces it).
|
|
7
|
+
|
|
8
|
+
Supported dialects: **PrusaSlicer, OrcaSlicer/Bambu Studio, Cura, Klipper, Marlin,
|
|
9
|
+
RepRap-flavor** — see the evidence-dated
|
|
10
|
+
[compatibility matrix](../../docs/compatibility/dialects-and-containers.md).
|
|
11
|
+
|
|
12
|
+
Every annotation is capability-tagged (`known | inferred | approximated | unavailable`); consumers
|
|
13
|
+
render what is known and disclose what is not.
|
|
14
|
+
|
|
15
|
+
This package is bundled into the parser's default *batteries* worker — most applications never
|
|
16
|
+
import it directly. Import it to build a **custom worker** with a chosen adapter subset, or to
|
|
17
|
+
register your own adapter against the annotator contract.
|
|
18
|
+
|
|
19
|
+
Part of [Chestnut Labs G-code Preview](../../README.md) · MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
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 type { MachineGeometry, Point2, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
9
|
+
import type { AnnotationSink } from './contracts.js';
|
|
10
|
+
/** First segment whose producing command starts at or after `byte`. */
|
|
11
|
+
export declare function segAtOrAfterByte(ir: ToolpathIR, byte: number): number;
|
|
12
|
+
export interface RangeMarker {
|
|
13
|
+
srcByte: number;
|
|
14
|
+
value: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Apply byte-positioned markers as contiguous channel ranges: each marker
|
|
18
|
+
* covers [its first segment, the next marker's first segment - 1]. A marker
|
|
19
|
+
* value of 0 acts as a range terminator (back to unknown/none).
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyMarkerRanges(ir: ToolpathIR, markers: RangeMarker[], write: (segStart: number, segEnd: number, value: number) => void): number;
|
|
22
|
+
export interface SegMarker {
|
|
23
|
+
segIndex: number;
|
|
24
|
+
value: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Apply SEGMENT-indexed markers (CommandEvent.segIndex — exact, no byte
|
|
28
|
+
* resolution needed) as contiguous ranges; value 0 terminates (DD-005 phase 5).
|
|
29
|
+
*/
|
|
30
|
+
export declare function applySegmentMarkers(markers: SegMarker[], segmentCount: number, write: (segStart: number, segEnd: number, value: number) => void): number;
|
|
31
|
+
/** Parse `key = value` / `key: value` slicer comment lines (Prusa tail, Orca header/config blocks). */
|
|
32
|
+
export declare function parseKeyValue(comment: string): {
|
|
33
|
+
key: string;
|
|
34
|
+
value: string;
|
|
35
|
+
} | null;
|
|
36
|
+
/** Parse "0x0,250x0,250x210,0x210" bed/printable-area point lists. */
|
|
37
|
+
export declare function parseAreaPoints(value: string): Point2[] | null;
|
|
38
|
+
/** Points → MachineGeometry bed (axis-aligned quad becomes rect, else polygon). */
|
|
39
|
+
export declare function bedFromPoints(points: Point2[], adapterId: string, evidence: string, heightMm: number | undefined, printerName: string | undefined, srcByte: number | undefined): MachineGeometry;
|
|
40
|
+
/** Dependency-free base64 decode (thumbnail comments); returns null on invalid input. */
|
|
41
|
+
export declare function decodeBase64(text: string): Uint8Array | null;
|
|
42
|
+
/** Shared thumbnail-comment accumulator (`thumbnail begin WxH LEN` … base64 … `thumbnail end`). */
|
|
43
|
+
export declare class ThumbnailCollector {
|
|
44
|
+
private readonly maxBytesPerThumbnail;
|
|
45
|
+
private active;
|
|
46
|
+
constructor(maxBytesPerThumbnail?: number);
|
|
47
|
+
/** True while inside a begin/end block — the adapter must route EVERY comment here. */
|
|
48
|
+
get isActive(): boolean;
|
|
49
|
+
/** Feed one comment line; emits to the sink when a bounded thumbnail completes. */
|
|
50
|
+
feed(comment: string, sink: AnnotationSink): void;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=annotate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotate.d.ts","sourceRoot":"","sources":["../src/annotate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACvF,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,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
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** First segment whose producing command starts at or after `byte`. */
|
|
2
|
+
export function segAtOrAfterByte(ir, byte) {
|
|
3
|
+
const offsets = ir.sourceIndex.byteOffsets;
|
|
4
|
+
let lo = 0;
|
|
5
|
+
let hi = offsets.length;
|
|
6
|
+
while (lo < hi) {
|
|
7
|
+
const mid = (lo + hi) >> 1;
|
|
8
|
+
if (offsets[mid] < byte)
|
|
9
|
+
lo = mid + 1;
|
|
10
|
+
else
|
|
11
|
+
hi = mid;
|
|
12
|
+
}
|
|
13
|
+
return lo < offsets.length ? ir.sourceIndex.segmentIndices[lo] : ir.segments.count;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply byte-positioned markers as contiguous channel ranges: each marker
|
|
17
|
+
* covers [its first segment, the next marker's first segment - 1]. A marker
|
|
18
|
+
* value of 0 acts as a range terminator (back to unknown/none).
|
|
19
|
+
*/
|
|
20
|
+
export function applyMarkerRanges(ir, markers, write) {
|
|
21
|
+
let applied = 0;
|
|
22
|
+
for (let i = 0; i < markers.length; i++) {
|
|
23
|
+
if (markers[i].value === 0)
|
|
24
|
+
continue;
|
|
25
|
+
const start = segAtOrAfterByte(ir, markers[i].srcByte);
|
|
26
|
+
const end = (i + 1 < markers.length ? segAtOrAfterByte(ir, markers[i + 1].srcByte) : ir.segments.count) - 1;
|
|
27
|
+
if (end >= start) {
|
|
28
|
+
write(start, end, markers[i].value);
|
|
29
|
+
applied++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return applied;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Apply SEGMENT-indexed markers (CommandEvent.segIndex — exact, no byte
|
|
36
|
+
* resolution needed) as contiguous ranges; value 0 terminates (DD-005 phase 5).
|
|
37
|
+
*/
|
|
38
|
+
export function applySegmentMarkers(markers, segmentCount, write) {
|
|
39
|
+
let applied = 0;
|
|
40
|
+
for (let i = 0; i < markers.length; i++) {
|
|
41
|
+
if (markers[i].value === 0)
|
|
42
|
+
continue;
|
|
43
|
+
const start = markers[i].segIndex;
|
|
44
|
+
const end = (i + 1 < markers.length ? markers[i + 1].segIndex : segmentCount) - 1;
|
|
45
|
+
if (end >= start && start < segmentCount) {
|
|
46
|
+
write(start, Math.min(end, segmentCount - 1), markers[i].value);
|
|
47
|
+
applied++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return applied;
|
|
51
|
+
}
|
|
52
|
+
/** Parse `key = value` / `key: value` slicer comment lines (Prusa tail, Orca header/config blocks). */
|
|
53
|
+
export function parseKeyValue(comment) {
|
|
54
|
+
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*(.*\S)\s*$/.exec(comment);
|
|
55
|
+
return m === null ? null : { key: m[1].toLowerCase(), value: m[2] };
|
|
56
|
+
}
|
|
57
|
+
/** Parse "0x0,250x0,250x210,0x210" bed/printable-area point lists. */
|
|
58
|
+
export function parseAreaPoints(value) {
|
|
59
|
+
const parts = value.split(',');
|
|
60
|
+
if (parts.length < 3 || parts.length > 64)
|
|
61
|
+
return null;
|
|
62
|
+
const points = [];
|
|
63
|
+
for (const p of parts) {
|
|
64
|
+
const m = /^\s*(-?\d+(?:\.\d+)?)x(-?\d+(?:\.\d+)?)\s*$/.exec(p);
|
|
65
|
+
if (m === null)
|
|
66
|
+
return null;
|
|
67
|
+
points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });
|
|
68
|
+
}
|
|
69
|
+
return points;
|
|
70
|
+
}
|
|
71
|
+
/** Points → MachineGeometry bed (axis-aligned quad becomes rect, else polygon). */
|
|
72
|
+
export function bedFromPoints(points, adapterId, evidence, heightMm, printerName, srcByte) {
|
|
73
|
+
const xs = points.map((p) => p.x);
|
|
74
|
+
const ys = points.map((p) => p.y);
|
|
75
|
+
const min = { x: Math.min(...xs), y: Math.min(...ys) };
|
|
76
|
+
const max = { x: Math.max(...xs), y: Math.max(...ys) };
|
|
77
|
+
const rect = points.length === 4 && points.every((p) => (p.x === min.x || p.x === max.x) && (p.y === min.y || p.y === max.y));
|
|
78
|
+
return {
|
|
79
|
+
bed: rect ? { kind: 'rect', min, max } : { kind: 'polygon', points },
|
|
80
|
+
origin: { x: 0, y: 0 },
|
|
81
|
+
heightMm,
|
|
82
|
+
printerName,
|
|
83
|
+
confidence: 'inferred', // read from slicer COMMENTS, not machine config
|
|
84
|
+
source: { adapterId, evidence, srcByte }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
88
|
+
const B64_LOOKUP = (() => {
|
|
89
|
+
const t = new Int8Array(128).fill(-1);
|
|
90
|
+
for (let i = 0; i < B64.length; i++)
|
|
91
|
+
t[B64.charCodeAt(i)] = i;
|
|
92
|
+
return t;
|
|
93
|
+
})();
|
|
94
|
+
/** Dependency-free base64 decode (thumbnail comments); returns null on invalid input. */
|
|
95
|
+
export function decodeBase64(text) {
|
|
96
|
+
const clean = text.replace(/[\s=]+/g, '');
|
|
97
|
+
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
|
|
98
|
+
let acc = 0;
|
|
99
|
+
let bits = 0;
|
|
100
|
+
let o = 0;
|
|
101
|
+
for (let i = 0; i < clean.length; i++) {
|
|
102
|
+
const c = clean.charCodeAt(i);
|
|
103
|
+
const v = c < 128 ? B64_LOOKUP[c] : -1;
|
|
104
|
+
if (v === -1)
|
|
105
|
+
return null;
|
|
106
|
+
acc = (acc << 6) | v;
|
|
107
|
+
bits += 6;
|
|
108
|
+
if (bits >= 8) {
|
|
109
|
+
bits -= 8;
|
|
110
|
+
out[o++] = (acc >> bits) & 0xff;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out.subarray(0, o);
|
|
114
|
+
}
|
|
115
|
+
/** Shared thumbnail-comment accumulator (`thumbnail begin WxH LEN` … base64 … `thumbnail end`). */
|
|
116
|
+
export class ThumbnailCollector {
|
|
117
|
+
maxBytesPerThumbnail;
|
|
118
|
+
active = null;
|
|
119
|
+
constructor(maxBytesPerThumbnail = 512 * 1024) {
|
|
120
|
+
this.maxBytesPerThumbnail = maxBytesPerThumbnail;
|
|
121
|
+
}
|
|
122
|
+
/** True while inside a begin/end block — the adapter must route EVERY comment here. */
|
|
123
|
+
get isActive() {
|
|
124
|
+
return this.active !== null;
|
|
125
|
+
}
|
|
126
|
+
/** Feed one comment line; emits to the sink when a bounded thumbnail completes. */
|
|
127
|
+
feed(comment, sink) {
|
|
128
|
+
const trimmed = comment.trim();
|
|
129
|
+
const begin = /^thumbnail(?:_JPG|_QOI)? begin (\d+)[x ](\d+) (\d+)/i.exec(trimmed);
|
|
130
|
+
if (begin !== null) {
|
|
131
|
+
const declared = parseInt(begin[3]);
|
|
132
|
+
this.active =
|
|
133
|
+
declared <= this.maxBytesPerThumbnail
|
|
134
|
+
? { width: parseInt(begin[1]), height: parseInt(begin[2]), b64: [], bytes: 0 }
|
|
135
|
+
: null;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (/^thumbnail(?:_JPG|_QOI)? end/i.test(trimmed)) {
|
|
139
|
+
if (this.active !== null) {
|
|
140
|
+
const bytes = decodeBase64(this.active.b64.join(''));
|
|
141
|
+
if (bytes !== null && bytes.byteLength > 0) {
|
|
142
|
+
sink.addThumbnail({ width: this.active.width, height: this.active.height, mime: 'image/png', bytes });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.active = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.active !== null) {
|
|
149
|
+
this.active.bytes += trimmed.length;
|
|
150
|
+
if (this.active.bytes > (this.maxBytesPerThumbnail * 4) / 3 + 8) {
|
|
151
|
+
this.active = null; // lied about its size — drop, stay bounded
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.active.b64.push(trimmed);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialect adapter behavioral contracts (DD-005 §4.1/§4.3, as amended).
|
|
3
|
+
*
|
|
4
|
+
* Adapters are ANNOTATORS: through the AnnotationSink they may write the
|
|
5
|
+
* optional `feature`/`object` channels, result metadata, and capability
|
|
6
|
+
* upgrades — the sink exposes no way to touch positions, kinds, layers, or
|
|
7
|
+
* counts, and the geometry-invariance CI gate proves it stays that way.
|
|
8
|
+
*
|
|
9
|
+
* Data contracts (MachineGeometry, DialectMetadata, DialectDetection) live in
|
|
10
|
+
* @chestnutlabs/toolpath-core (see its metadata.ts for the rationale).
|
|
11
|
+
*/
|
|
12
|
+
import type { Confidence, DialectDetection, MachineGeometry, FilamentInfo, ThumbnailData, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
13
|
+
/** Read-only normalized command event — mirror of the parser's hook payload. */
|
|
14
|
+
export interface CommandEvent {
|
|
15
|
+
gcode: string;
|
|
16
|
+
params: Readonly<Record<string, number>>;
|
|
17
|
+
rawLine: string;
|
|
18
|
+
srcByte: number;
|
|
19
|
+
segIndex: number;
|
|
20
|
+
}
|
|
21
|
+
export interface DetectInput {
|
|
22
|
+
/** First decoded window (≤ 64 KB). */
|
|
23
|
+
headText: string;
|
|
24
|
+
/** Last decoded window (≤ 16 KB); '' for non-seekable streams until finalize (§4.5). */
|
|
25
|
+
tailText: string;
|
|
26
|
+
/** Container metadata when the input came through gcode-containers (phase 2). */
|
|
27
|
+
containerMeta?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
export interface AnnotationSink {
|
|
30
|
+
/** Write a FeatureRole index for segments [segStart, segEnd] (bounds validated at apply). */
|
|
31
|
+
setFeature(segStart: number, segEnd: number, role: number): void;
|
|
32
|
+
/** `objectValue` is the 1-based object-channel value (DD-001: v indexes ir.objects[v-1]). */
|
|
33
|
+
setObject(segStart: number, segEnd: number, objectValue: number): void;
|
|
34
|
+
defineObject(objectValue: number, name: string): void;
|
|
35
|
+
setMachine(machine: MachineGeometry): void;
|
|
36
|
+
setFilament(info: FilamentInfo): void;
|
|
37
|
+
/** Enrich a tool's identity (material, '#RRGGBB' color) — merged into ir.tools (DD-005 phase 5). */
|
|
38
|
+
setToolInfo(tool: number, info: {
|
|
39
|
+
material?: string;
|
|
40
|
+
colorHex?: string;
|
|
41
|
+
}): void;
|
|
42
|
+
addThumbnail(thumb: ThumbnailData): void;
|
|
43
|
+
setRaw(key: string, value: string): void;
|
|
44
|
+
upgradeCapability(key: string, confidence: Confidence): void;
|
|
45
|
+
warn(code: string, message: string, srcByte?: number): void;
|
|
46
|
+
}
|
|
47
|
+
export interface DialectAdapter {
|
|
48
|
+
id: string;
|
|
49
|
+
displayName: string;
|
|
50
|
+
/** 'slicer' and 'firmware' adapters COMPOSE (amendment 1); one winner per kind. */
|
|
51
|
+
kind: 'slicer' | 'firmware' | 'generic';
|
|
52
|
+
detect(input: DetectInput): DialectDetection | null;
|
|
53
|
+
/** Per-adapter serializable configuration (validated by the adapter itself). */
|
|
54
|
+
onComment?(comment: string, srcByte: number, sink: AnnotationSink, config?: unknown): void;
|
|
55
|
+
onCommand?(event: CommandEvent, sink: AnnotationSink, config?: unknown): void;
|
|
56
|
+
finalize?(ir: ToolpathIR, sink: AnnotationSink, config?: unknown): void;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cura.d.ts
ADDED
|
@@ -0,0 +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,CAkCrC"}
|
package/dist/cura.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cura dialect adapter (DD-005 phase 4, issue #76).
|
|
3
|
+
*
|
|
4
|
+
* Annotates `;TYPE:` feature roles (Cura's UPPERCASE vocabulary) and picks up
|
|
5
|
+
* the printer name where newer Cura versions expose it. Bed geometry is NOT
|
|
6
|
+
* derivable from standard Cura output (its settings tail encodes machine size
|
|
7
|
+
* inside a packed settings blob — out of v1 scope; matrix records `partial`).
|
|
8
|
+
*/
|
|
9
|
+
import { FeatureRole } from '@chestnutlabs/toolpath-core';
|
|
10
|
+
import { applyMarkerRanges, parseKeyValue } from './annotate.js';
|
|
11
|
+
/** Cura `;TYPE:` names → DD-001 FeatureRole. */
|
|
12
|
+
const TYPE_MAP = {
|
|
13
|
+
'wall-outer': FeatureRole.ExternalPerimeter,
|
|
14
|
+
'wall-inner': FeatureRole.Perimeter,
|
|
15
|
+
skin: FeatureRole.SolidInfill,
|
|
16
|
+
fill: FeatureRole.Infill,
|
|
17
|
+
support: FeatureRole.Support,
|
|
18
|
+
'support-interface': FeatureRole.Support,
|
|
19
|
+
skirt: FeatureRole.Skirt,
|
|
20
|
+
raft: FeatureRole.Brim,
|
|
21
|
+
'prime-tower': FeatureRole.Custom,
|
|
22
|
+
bridge: FeatureRole.Bridge
|
|
23
|
+
};
|
|
24
|
+
const state = new WeakMap();
|
|
25
|
+
function stateFor(sink) {
|
|
26
|
+
let s = state.get(sink);
|
|
27
|
+
if (s === undefined) {
|
|
28
|
+
s = { markers: [] };
|
|
29
|
+
state.set(sink, s);
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
export function cura() {
|
|
34
|
+
return {
|
|
35
|
+
id: 'cura',
|
|
36
|
+
displayName: 'Cura',
|
|
37
|
+
kind: 'slicer',
|
|
38
|
+
detect(input) {
|
|
39
|
+
const m = /Generated with (Cura_SteamEngine[^\n\r]*)/.exec(input.headText);
|
|
40
|
+
if (m !== null) {
|
|
41
|
+
return { dialectId: 'cura', kind: 'slicer', confidence: 'known', evidence: `header: ${m[1].trim()}` };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
},
|
|
45
|
+
onComment(comment, srcByte, sink) {
|
|
46
|
+
const s = stateFor(sink);
|
|
47
|
+
const trimmed = comment.trim();
|
|
48
|
+
if (trimmed.startsWith('TYPE:')) {
|
|
49
|
+
const role = TYPE_MAP[trimmed.slice(5).trim().toLowerCase()];
|
|
50
|
+
s.markers.push({ srcByte, value: role ?? FeatureRole.Custom });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (trimmed.startsWith('TARGET_MACHINE.NAME:')) {
|
|
54
|
+
s.printerName = trimmed.slice('TARGET_MACHINE.NAME:'.length).trim();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const kv = parseKeyValue(comment);
|
|
58
|
+
if (kv !== null && kv.key === 'target_machine')
|
|
59
|
+
s.printerName = kv.value;
|
|
60
|
+
},
|
|
61
|
+
finalize(ir, sink) {
|
|
62
|
+
const s = stateFor(sink);
|
|
63
|
+
const applied = applyMarkerRanges(ir, s.markers, (a, b, v) => sink.setFeature(a, b, v));
|
|
64
|
+
if (applied > 0)
|
|
65
|
+
sink.upgradeCapability('featureRoles', 'known');
|
|
66
|
+
if (s.printerName !== undefined)
|
|
67
|
+
sink.setRaw('printer_model', s.printerName);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firmware.d.ts","sourceRoot":"","sources":["../src/firmware.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAqCrE,wBAAgB,OAAO,IAAI,cAAc,CAkCxC;AAED,wBAAgB,MAAM,IAAI,cAAc,CAyBvC;AAED,wBAAgB,MAAM,IAAI,cAAc,CAevC"}
|
package/dist/firmware.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { applySegmentMarkers } from './annotate.js';
|
|
2
|
+
const objState = new WeakMap();
|
|
3
|
+
function objStateFor(sink) {
|
|
4
|
+
let s = objState.get(sink);
|
|
5
|
+
if (s === undefined) {
|
|
6
|
+
s = { markers: [], names: new Map() };
|
|
7
|
+
objState.set(sink, s);
|
|
8
|
+
}
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
11
|
+
function objectValueFor(s, name) {
|
|
12
|
+
let v = s.names.get(name);
|
|
13
|
+
if (v === undefined) {
|
|
14
|
+
v = s.names.size + 1;
|
|
15
|
+
s.names.set(name, v);
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
function finalizeObjects(ir, sink, s) {
|
|
20
|
+
const applied = applySegmentMarkers(s.markers, ir.segments.count, (a, b, v) => sink.setObject(a, b, v));
|
|
21
|
+
if (applied > 0) {
|
|
22
|
+
for (const [name, value] of s.names)
|
|
23
|
+
sink.defineObject(value, name);
|
|
24
|
+
sink.upgradeCapability('objects', 'known');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function klipper() {
|
|
28
|
+
return {
|
|
29
|
+
id: 'klipper',
|
|
30
|
+
displayName: 'Klipper',
|
|
31
|
+
kind: 'firmware',
|
|
32
|
+
detect(input) {
|
|
33
|
+
const m = /(EXCLUDE_OBJECT_DEFINE|SET_VELOCITY_LIMIT|ACTIVATE_EXTRUDER|SDCARD_PRINT_FILE)/.exec(input.headText);
|
|
34
|
+
if (m !== null) {
|
|
35
|
+
return { dialectId: 'klipper', kind: 'firmware', confidence: 'known', evidence: `command: ${m[1]}` };
|
|
36
|
+
}
|
|
37
|
+
if (/gcode_flavor\s*=\s*klipper/i.test(input.tailText)) {
|
|
38
|
+
return { dialectId: 'klipper', kind: 'firmware', confidence: 'known', evidence: 'gcode_flavor = klipper' };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
onCommand(event, sink) {
|
|
43
|
+
// Extended commands don't survive the inherited lexer — match the raw line.
|
|
44
|
+
const m = /^\s*EXCLUDE_OBJECT_(DEFINE|START|END)\b(.*)$/i.exec(event.rawLine);
|
|
45
|
+
if (m === null)
|
|
46
|
+
return;
|
|
47
|
+
const s = objStateFor(sink);
|
|
48
|
+
const name = /NAME=([^\s]+)/i.exec(m[2])?.[1];
|
|
49
|
+
const verb = m[1].toUpperCase();
|
|
50
|
+
if (verb === 'DEFINE') {
|
|
51
|
+
if (name !== undefined)
|
|
52
|
+
objectValueFor(s, name); // registers identity/order
|
|
53
|
+
}
|
|
54
|
+
else if (verb === 'START') {
|
|
55
|
+
if (name !== undefined)
|
|
56
|
+
s.markers.push({ segIndex: event.segIndex, value: objectValueFor(s, name) });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
s.markers.push({ segIndex: event.segIndex, value: 0 }); // END terminates
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
finalize(ir, sink) {
|
|
63
|
+
finalizeObjects(ir, sink, objStateFor(sink));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function marlin() {
|
|
68
|
+
return {
|
|
69
|
+
id: 'marlin',
|
|
70
|
+
displayName: 'Marlin',
|
|
71
|
+
kind: 'firmware',
|
|
72
|
+
detect(input) {
|
|
73
|
+
if (/^;\s*FLAVOR\s*:\s*Marlin/im.test(input.headText)) {
|
|
74
|
+
return { dialectId: 'marlin', kind: 'firmware', confidence: 'known', evidence: ';FLAVOR:Marlin header' };
|
|
75
|
+
}
|
|
76
|
+
if (/gcode_flavor\s*=\s*marlin/i.test(input.tailText)) {
|
|
77
|
+
return { dialectId: 'marlin', kind: 'firmware', confidence: 'inferred', evidence: 'gcode_flavor = marlin' };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
onCommand(event, sink) {
|
|
82
|
+
// M486 S<idx> starts object idx (S-1 = none); M486 carries no names.
|
|
83
|
+
if (event.gcode !== 'm486' || event.params.s === undefined)
|
|
84
|
+
return;
|
|
85
|
+
const s = objStateFor(sink);
|
|
86
|
+
const idx = event.params.s;
|
|
87
|
+
s.markers.push({ segIndex: event.segIndex, value: idx >= 0 ? objectValueFor(s, `object ${idx}`) : 0 });
|
|
88
|
+
},
|
|
89
|
+
finalize(ir, sink) {
|
|
90
|
+
finalizeObjects(ir, sink, objStateFor(sink));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function repRap() {
|
|
95
|
+
return {
|
|
96
|
+
id: 'reprap',
|
|
97
|
+
displayName: 'RepRap-style',
|
|
98
|
+
kind: 'firmware',
|
|
99
|
+
detect(input) {
|
|
100
|
+
if (/^;\s*FLAVOR\s*:\s*RepRap/im.test(input.headText)) {
|
|
101
|
+
return { dialectId: 'reprap', kind: 'firmware', confidence: 'known', evidence: ';FLAVOR:RepRap header' };
|
|
102
|
+
}
|
|
103
|
+
if (/gcode_flavor\s*=\s*reprap/i.test(input.tailText)) {
|
|
104
|
+
return { dialectId: 'reprap', kind: 'firmware', confidence: 'inferred', evidence: 'gcode_flavor = reprap' };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-dialects — dialect adapter contracts & registry (DD-005).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 (#73): behavioral contracts, buffered annotation sink, and the
|
|
5
|
+
* composition runner. Built-in vendor adapters arrive in phases 3–5.
|
|
6
|
+
* Depends only on @chestnutlabs/toolpath-core; the parser consumes the runner
|
|
7
|
+
* through its own structural DialectRunnerFactory type (worker entries compose
|
|
8
|
+
* the two — DD-002 §5 boundaries).
|
|
9
|
+
*/
|
|
10
|
+
export type { DialectAdapter, DetectInput, AnnotationSink, CommandEvent } from './contracts.js';
|
|
11
|
+
export { createDialectRunner } from './registry.js';
|
|
12
|
+
export type { DialectRunnerFactory, DialectRun, DialectRunInput, DialectRunResult } from './registry.js';
|
|
13
|
+
export { BufferedAnnotationSink } from './sink.js';
|
|
14
|
+
export { prusaSlicer } from './prusaslicer.js';
|
|
15
|
+
export { orcaBambu } from './orca-bambu.js';
|
|
16
|
+
export { cura } from './cura.js';
|
|
17
|
+
export { klipper, marlin, repRap } from './firmware.js';
|
|
18
|
+
export { segAtOrAfterByte, applyMarkerRanges, parseKeyValue, parseAreaPoints, bedFromPoints, decodeBase64, ThumbnailCollector } from './annotate.js';
|
|
19
|
+
export type { RangeMarker } from './annotate.js';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createDialectRunner } from './registry.js';
|
|
2
|
+
export { BufferedAnnotationSink } from './sink.js';
|
|
3
|
+
export { prusaSlicer } from './prusaslicer.js';
|
|
4
|
+
export { orcaBambu } from './orca-bambu.js';
|
|
5
|
+
export { cura } from './cura.js';
|
|
6
|
+
export { klipper, marlin, repRap } from './firmware.js';
|
|
7
|
+
export { segAtOrAfterByte, applyMarkerRanges, parseKeyValue, parseAreaPoints, bedFromPoints, decodeBase64, ThumbnailCollector } from './annotate.js';
|
|
@@ -0,0 +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;AAuDrE,wBAAgB,SAAS,IAAI,cAAc,CA8F1C"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrcaSlicer / Bambu Studio dialect adapter (DD-005 phase 3, issue #75).
|
|
3
|
+
*
|
|
4
|
+
* Annotates: `; FEATURE:` roles (→ `featureRoles: 'known'`), Bambu object
|
|
5
|
+
* start/stop comments (→ `object` channel + `objects: 'known'`), bed geometry
|
|
6
|
+
* from `printable_area` config comments (bare .gcode files — inside a
|
|
7
|
+
* `.gcode.3mf` the container's machine config already provides `known`
|
|
8
|
+
* geometry and outranks this), and printer identity. Detection also accepts
|
|
9
|
+
* container metadata as evidence (DD-005 §4.1 DetectInput.containerMeta).
|
|
10
|
+
*/
|
|
11
|
+
import { FeatureRole } from '@chestnutlabs/toolpath-core';
|
|
12
|
+
import { ThumbnailCollector, applyMarkerRanges, bedFromPoints, parseAreaPoints, parseKeyValue } from './annotate.js';
|
|
13
|
+
/** Orca/Bambu `; FEATURE:` names → DD-001 FeatureRole. */
|
|
14
|
+
const FEATURE_MAP = {
|
|
15
|
+
'inner wall': FeatureRole.Perimeter,
|
|
16
|
+
'outer wall': FeatureRole.ExternalPerimeter,
|
|
17
|
+
'overhang wall': FeatureRole.Perimeter,
|
|
18
|
+
'sparse infill': FeatureRole.Infill,
|
|
19
|
+
'internal solid infill': FeatureRole.SolidInfill,
|
|
20
|
+
'top surface': FeatureRole.SolidInfill,
|
|
21
|
+
'bottom surface': FeatureRole.SolidInfill,
|
|
22
|
+
ironing: FeatureRole.SolidInfill,
|
|
23
|
+
bridge: FeatureRole.Bridge,
|
|
24
|
+
support: FeatureRole.Support,
|
|
25
|
+
'support interface': FeatureRole.Support,
|
|
26
|
+
'support transition': FeatureRole.Support,
|
|
27
|
+
skirt: FeatureRole.Skirt,
|
|
28
|
+
brim: FeatureRole.Brim,
|
|
29
|
+
'prime tower': FeatureRole.Custom,
|
|
30
|
+
'gap infill': FeatureRole.Infill,
|
|
31
|
+
custom: FeatureRole.Custom
|
|
32
|
+
};
|
|
33
|
+
const state = new WeakMap();
|
|
34
|
+
function stateFor(sink) {
|
|
35
|
+
let s = state.get(sink);
|
|
36
|
+
if (s === undefined) {
|
|
37
|
+
s = { features: [], objects: [], objectNames: new Map(), bedPoints: null, thumbs: new ThumbnailCollector() };
|
|
38
|
+
state.set(sink, s);
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
export function orcaBambu() {
|
|
43
|
+
return {
|
|
44
|
+
id: 'orca-bambu',
|
|
45
|
+
displayName: 'OrcaSlicer / Bambu Studio',
|
|
46
|
+
kind: 'slicer',
|
|
47
|
+
detect(input) {
|
|
48
|
+
const m = /(BambuStudio[^\n\r]*|generated by OrcaSlicer[^\n\r]*)/.exec(input.headText);
|
|
49
|
+
if (m !== null) {
|
|
50
|
+
return { dialectId: 'orca-bambu', kind: 'slicer', confidence: 'known', evidence: `header: ${m[1].trim()}` };
|
|
51
|
+
}
|
|
52
|
+
if (input.containerMeta !== undefined && input.containerMeta['printer_model'] !== undefined) {
|
|
53
|
+
return {
|
|
54
|
+
dialectId: 'orca-bambu',
|
|
55
|
+
kind: 'slicer',
|
|
56
|
+
confidence: 'inferred',
|
|
57
|
+
evidence: `container metadata: printer_model=${input.containerMeta['printer_model']}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
onComment(comment, srcByte, sink) {
|
|
63
|
+
const s = stateFor(sink);
|
|
64
|
+
const trimmed = comment.trim();
|
|
65
|
+
if (trimmed.startsWith('FEATURE:')) {
|
|
66
|
+
const role = FEATURE_MAP[trimmed.slice(8).trim().toLowerCase()];
|
|
67
|
+
s.features.push({ srcByte, value: role ?? FeatureRole.Custom });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const start = /^start printing object,?\s*(?:unique\s+)?id:?\s*(\d+)(?:\s+name:\s*(.+))?/i.exec(trimmed);
|
|
71
|
+
if (start !== null) {
|
|
72
|
+
const value = parseInt(start[1]) + 1; // object channel is 1-based (0 = none)
|
|
73
|
+
s.objects.push({ srcByte, value });
|
|
74
|
+
if (start[2] !== undefined)
|
|
75
|
+
s.objectNames.set(value, start[2].trim());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (/^stop printing object/i.test(trimmed)) {
|
|
79
|
+
s.objects.push({ srcByte, value: 0 }); // range terminator
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (s.thumbs.isActive || trimmed.toLowerCase().startsWith('thumbnail')) {
|
|
83
|
+
s.thumbs.feed(comment, sink);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const kv = parseKeyValue(comment);
|
|
87
|
+
if (kv === null)
|
|
88
|
+
return;
|
|
89
|
+
if (kv.key === 'printable_area') {
|
|
90
|
+
s.bedPoints = kv.value;
|
|
91
|
+
s.bedSrcByte = srcByte;
|
|
92
|
+
}
|
|
93
|
+
else if (kv.key === 'printable_height') {
|
|
94
|
+
const h = parseFloat(kv.value);
|
|
95
|
+
if (Number.isFinite(h))
|
|
96
|
+
s.heightMm = h;
|
|
97
|
+
}
|
|
98
|
+
else if (kv.key === 'printer_model') {
|
|
99
|
+
s.printerModel = kv.value;
|
|
100
|
+
}
|
|
101
|
+
else if (kv.key === 'filament_type') {
|
|
102
|
+
s.filamentTypes = kv.value.split(';').map((v) => v.trim());
|
|
103
|
+
}
|
|
104
|
+
else if (kv.key === 'filament_colour') {
|
|
105
|
+
s.filamentColours = kv.value.split(';').map((v) => v.trim());
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
finalize(ir, sink) {
|
|
109
|
+
const s = stateFor(sink);
|
|
110
|
+
const features = applyMarkerRanges(ir, s.features, (a, b, v) => sink.setFeature(a, b, v));
|
|
111
|
+
if (features > 0)
|
|
112
|
+
sink.upgradeCapability('featureRoles', 'known');
|
|
113
|
+
const objects = applyMarkerRanges(ir, s.objects, (a, b, v) => sink.setObject(a, b, v));
|
|
114
|
+
if (objects > 0) {
|
|
115
|
+
for (const [value, name] of s.objectNames)
|
|
116
|
+
sink.defineObject(value, name);
|
|
117
|
+
sink.upgradeCapability('objects', 'known');
|
|
118
|
+
}
|
|
119
|
+
if (s.bedPoints !== null) {
|
|
120
|
+
const points = parseAreaPoints(s.bedPoints);
|
|
121
|
+
if (points !== null) {
|
|
122
|
+
sink.setMachine(bedFromPoints(points, 'orca-bambu', '; printable_area config comment', s.heightMm, s.printerModel, s.bedSrcByte));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
sink.warn('dialect-bed-unparsed', 'printable_area comment present but unparseable');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (s.printerModel !== undefined)
|
|
129
|
+
sink.setRaw('printer_model', s.printerModel);
|
|
130
|
+
// Multi-tool/AMS metadata (phase 5): filament slots enrich ir.tools.
|
|
131
|
+
const slots = Math.max(s.filamentTypes?.length ?? 0, s.filamentColours?.length ?? 0);
|
|
132
|
+
for (let i = 0; i < Math.min(slots, 64); i++) {
|
|
133
|
+
sink.setFilament({ slot: i, type: s.filamentTypes?.[i], color: s.filamentColours?.[i] });
|
|
134
|
+
sink.setToolInfo(i, { material: s.filamentTypes?.[i], colorHex: s.filamentColours?.[i] });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prusaslicer.d.ts","sourceRoot":"","sources":["../src/prusaslicer.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiDrE,wBAAgB,WAAW,IAAI,cAAc,CA+D5C"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrusaSlicer dialect adapter (DD-005 phase 3, issue #75).
|
|
3
|
+
*
|
|
4
|
+
* Annotates: `;TYPE:` feature roles (→ `featureRoles: 'known'`), bed geometry
|
|
5
|
+
* from the settings-block comments (`bed_shape`, `max_print_height`,
|
|
6
|
+
* `printer_model` — confidence `inferred`, provenance recorded), and bounded
|
|
7
|
+
* `; thumbnail begin/end` PNG thumbnails. Geometry is untouchable by contract.
|
|
8
|
+
*/
|
|
9
|
+
import { FeatureRole } from '@chestnutlabs/toolpath-core';
|
|
10
|
+
import { ThumbnailCollector, applyMarkerRanges, bedFromPoints, parseAreaPoints, parseKeyValue } from './annotate.js';
|
|
11
|
+
/** Prusa `;TYPE:` names → DD-001 FeatureRole (unknown names → Custom, honestly generic). */
|
|
12
|
+
const TYPE_MAP = {
|
|
13
|
+
perimeter: FeatureRole.Perimeter,
|
|
14
|
+
'external perimeter': FeatureRole.ExternalPerimeter,
|
|
15
|
+
'overhang perimeter': FeatureRole.Perimeter,
|
|
16
|
+
'internal infill': FeatureRole.Infill,
|
|
17
|
+
'solid infill': FeatureRole.SolidInfill,
|
|
18
|
+
'top solid infill': FeatureRole.SolidInfill,
|
|
19
|
+
'bridge infill': FeatureRole.Bridge,
|
|
20
|
+
'internal bridge infill': FeatureRole.Bridge,
|
|
21
|
+
'support material': FeatureRole.Support,
|
|
22
|
+
'support material interface': FeatureRole.Support,
|
|
23
|
+
'skirt/brim': FeatureRole.Skirt,
|
|
24
|
+
skirt: FeatureRole.Skirt,
|
|
25
|
+
brim: FeatureRole.Brim,
|
|
26
|
+
'wipe tower': FeatureRole.Custom,
|
|
27
|
+
custom: FeatureRole.Custom
|
|
28
|
+
};
|
|
29
|
+
const state = new WeakMap();
|
|
30
|
+
function stateFor(sink) {
|
|
31
|
+
let s = state.get(sink);
|
|
32
|
+
if (s === undefined) {
|
|
33
|
+
s = { markers: [], bedPoints: null, thumbs: new ThumbnailCollector() };
|
|
34
|
+
state.set(sink, s);
|
|
35
|
+
}
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
export function prusaSlicer() {
|
|
39
|
+
return {
|
|
40
|
+
id: 'prusaslicer',
|
|
41
|
+
displayName: 'PrusaSlicer',
|
|
42
|
+
kind: 'slicer',
|
|
43
|
+
detect(input) {
|
|
44
|
+
const m = /generated by (PrusaSlicer[^\n\r]*)/.exec(input.headText);
|
|
45
|
+
if (m !== null) {
|
|
46
|
+
return { dialectId: 'prusaslicer', kind: 'slicer', confidence: 'known', evidence: `header: ${m[1].trim()}` };
|
|
47
|
+
}
|
|
48
|
+
// Settings live at the tail; a tail-only match still identifies the file.
|
|
49
|
+
if (/;\s*generated by PrusaSlicer/.test(input.tailText) || /\n;\s*bed_shape\s*=/.test(input.tailText)) {
|
|
50
|
+
return { dialectId: 'prusaslicer', kind: 'slicer', confidence: 'inferred', evidence: 'settings block (tail)' };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
},
|
|
54
|
+
onComment(comment, srcByte, sink) {
|
|
55
|
+
const s = stateFor(sink);
|
|
56
|
+
if (comment.startsWith('TYPE:')) {
|
|
57
|
+
const role = TYPE_MAP[comment.slice(5).trim().toLowerCase()];
|
|
58
|
+
s.markers.push({ srcByte, value: role ?? FeatureRole.Custom });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (s.thumbs.isActive || comment.trimStart().toLowerCase().startsWith('thumbnail')) {
|
|
62
|
+
s.thumbs.feed(comment, sink);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const kv = parseKeyValue(comment);
|
|
66
|
+
if (kv === null)
|
|
67
|
+
return;
|
|
68
|
+
if (kv.key === 'bed_shape') {
|
|
69
|
+
s.bedPoints = kv.value;
|
|
70
|
+
s.bedSrcByte = srcByte;
|
|
71
|
+
}
|
|
72
|
+
else if (kv.key === 'max_print_height') {
|
|
73
|
+
const h = parseFloat(kv.value);
|
|
74
|
+
if (Number.isFinite(h))
|
|
75
|
+
s.heightMm = h;
|
|
76
|
+
}
|
|
77
|
+
else if (kv.key === 'printer_model') {
|
|
78
|
+
s.printerModel = kv.value;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
finalize(ir, sink) {
|
|
82
|
+
const s = stateFor(sink);
|
|
83
|
+
const applied = applyMarkerRanges(ir, s.markers, (a, b, v) => sink.setFeature(a, b, v));
|
|
84
|
+
if (applied > 0)
|
|
85
|
+
sink.upgradeCapability('featureRoles', 'known');
|
|
86
|
+
if (s.bedPoints !== null) {
|
|
87
|
+
const points = parseAreaPoints(s.bedPoints);
|
|
88
|
+
if (points !== null) {
|
|
89
|
+
sink.setMachine(bedFromPoints(points, 'prusaslicer', '; bed_shape settings comment', s.heightMm, s.printerModel, s.bedSrcByte));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
sink.warn('dialect-bed-unparsed', 'bed_shape comment present but unparseable');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (s.printerModel !== undefined)
|
|
96
|
+
sink.setRaw('printer_model', s.printerModel);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialect registry & run composition (DD-005 §4.1/§4.5, as amended).
|
|
3
|
+
*
|
|
4
|
+
* Explicit, tree-shakeable: `createDialectRunner(adapters)` — no global
|
|
5
|
+
* side-effect registry. The returned factory matches the parser's structural
|
|
6
|
+
* `DialectRunnerFactory` shape (the parser never imports this package;
|
|
7
|
+
* worker entries compose the two).
|
|
8
|
+
*
|
|
9
|
+
* Composition (amendment 1): detection picks at most ONE adapter per `kind`
|
|
10
|
+
* (highest confidence wins; ties select none for that kind — never a guess),
|
|
11
|
+
* and all selected adapters run together. Adapter exceptions are contained:
|
|
12
|
+
* a throwing adapter is disabled for the run with an `adapter-failed` warning.
|
|
13
|
+
*/
|
|
14
|
+
import type { DialectDetection, DialectMetadata, ToolpathIR, Warning } from '@chestnutlabs/toolpath-core';
|
|
15
|
+
import type { CommandEvent, DialectAdapter } from './contracts.js';
|
|
16
|
+
export interface DialectRunInput {
|
|
17
|
+
/** 'auto' runs detection over all registered adapters; an array restricts by id. */
|
|
18
|
+
selection: 'auto' | string[];
|
|
19
|
+
config?: Record<string, unknown>;
|
|
20
|
+
headText: string;
|
|
21
|
+
tailText: string;
|
|
22
|
+
containerMeta?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export interface DialectRunResult {
|
|
25
|
+
metadata?: DialectMetadata;
|
|
26
|
+
warnings: Warning[];
|
|
27
|
+
}
|
|
28
|
+
export interface DialectRun {
|
|
29
|
+
detections: DialectDetection[];
|
|
30
|
+
onComment: (text: string, srcByte: number) => void;
|
|
31
|
+
onCommand: (event: CommandEvent) => void;
|
|
32
|
+
finalize(ir: ToolpathIR): DialectRunResult;
|
|
33
|
+
}
|
|
34
|
+
export interface DialectRunnerFactory {
|
|
35
|
+
adapterIds: string[];
|
|
36
|
+
createRun(input: DialectRunInput): DialectRun | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function createDialectRunner(adapters: DialectAdapter[]): DialectRunnerFactory;
|
|
39
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC1G,OAAO,KAAK,EAAE,YAAY,EAAe,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKhF,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,gBAAgB,CAAC;CAC5C;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,GAAG,IAAI,CAAC;CACtD;AAUD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,oBAAoB,CAsFpF"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BufferedAnnotationSink } from './sink.js';
|
|
2
|
+
const CONFIDENCE_RANK = { known: 3, inferred: 2, approximated: 1, unavailable: 0 };
|
|
3
|
+
export function createDialectRunner(adapters) {
|
|
4
|
+
return {
|
|
5
|
+
adapterIds: adapters.map((a) => a.id),
|
|
6
|
+
createRun(input) {
|
|
7
|
+
const pool = input.selection === 'auto' ? adapters : adapters.filter((a) => input.selection.includes(a.id));
|
|
8
|
+
const detect = {
|
|
9
|
+
headText: input.headText,
|
|
10
|
+
tailText: input.tailText,
|
|
11
|
+
containerMeta: input.containerMeta
|
|
12
|
+
};
|
|
13
|
+
// Detection: best confidence per kind; ambiguous ties select none for that kind.
|
|
14
|
+
const byKind = new Map();
|
|
15
|
+
for (const adapter of pool) {
|
|
16
|
+
let detection = null;
|
|
17
|
+
try {
|
|
18
|
+
detection = adapter.detect(detect);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
detection = null;
|
|
22
|
+
}
|
|
23
|
+
if (detection === null)
|
|
24
|
+
continue;
|
|
25
|
+
const list = byKind.get(adapter.kind) ?? [];
|
|
26
|
+
list.push({ adapter, detection });
|
|
27
|
+
byKind.set(adapter.kind, list);
|
|
28
|
+
}
|
|
29
|
+
const active = [];
|
|
30
|
+
for (const list of byKind.values()) {
|
|
31
|
+
list.sort((a, b) => CONFIDENCE_RANK[b.detection.confidence] - CONFIDENCE_RANK[a.detection.confidence]);
|
|
32
|
+
const best = list[0];
|
|
33
|
+
const tied = list.length > 1 &&
|
|
34
|
+
CONFIDENCE_RANK[list[1].detection.confidence] === CONFIDENCE_RANK[best.detection.confidence];
|
|
35
|
+
if (tied)
|
|
36
|
+
continue; // never a guess presented as a decision
|
|
37
|
+
active.push({
|
|
38
|
+
adapter: best.adapter,
|
|
39
|
+
detection: best.detection,
|
|
40
|
+
sink: new BufferedAnnotationSink(best.adapter.id),
|
|
41
|
+
config: input.config?.[best.adapter.id],
|
|
42
|
+
failed: false
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (active.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
const contain = (a, fn) => {
|
|
48
|
+
if (a.failed)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
fn();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
a.failed = true;
|
|
55
|
+
a.sink.warn('adapter-failed', `adapter '${a.adapter.id}' failed: ${err instanceof Error ? err.message : err}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
detections: active.map((a) => a.detection),
|
|
60
|
+
onComment: (text, srcByte) => {
|
|
61
|
+
for (const a of active) {
|
|
62
|
+
if (a.adapter.onComment)
|
|
63
|
+
contain(a, () => a.adapter.onComment?.(text, srcByte, a.sink, a.config));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
onCommand: (event) => {
|
|
67
|
+
for (const a of active) {
|
|
68
|
+
if (a.adapter.onCommand)
|
|
69
|
+
contain(a, () => a.adapter.onCommand?.(event, a.sink, a.config));
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
finalize(ir) {
|
|
73
|
+
const metadata = { dialects: active.map((a) => a.detection) };
|
|
74
|
+
const conflicts = [];
|
|
75
|
+
for (const a of active) {
|
|
76
|
+
contain(a, () => a.adapter.finalize?.(ir, a.sink, a.config));
|
|
77
|
+
a.sink.apply(ir, metadata, (code, message) => conflicts.push({ code, message, severity: 'warn', count: 1 }));
|
|
78
|
+
}
|
|
79
|
+
for (const w of conflicts)
|
|
80
|
+
ir.header.warnings.push(w);
|
|
81
|
+
ir.header.dialects.push(...active.map((a) => ({ id: a.detection.dialectId, confidence: a.detection.confidence })));
|
|
82
|
+
return { metadata, warnings: conflicts };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/sink.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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, and the metadata object beside the IR.
|
|
9
|
+
*/
|
|
10
|
+
import type { Confidence, DialectMetadata, FilamentInfo, MachineGeometry, ThumbnailData, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
11
|
+
import type { AnnotationSink } from './contracts.js';
|
|
12
|
+
export declare class BufferedAnnotationSink implements AnnotationSink {
|
|
13
|
+
private readonly adapterId;
|
|
14
|
+
private ops;
|
|
15
|
+
private objects;
|
|
16
|
+
private machine;
|
|
17
|
+
private filaments;
|
|
18
|
+
private toolInfos;
|
|
19
|
+
private thumbnails;
|
|
20
|
+
private raw;
|
|
21
|
+
private capabilities;
|
|
22
|
+
private warnings;
|
|
23
|
+
private overflow;
|
|
24
|
+
constructor(adapterId: string);
|
|
25
|
+
private pushOp;
|
|
26
|
+
setFeature(segStart: number, segEnd: number, role: number): void;
|
|
27
|
+
setObject(segStart: number, segEnd: number, objectId: number): void;
|
|
28
|
+
defineObject(id: number, name: string): void;
|
|
29
|
+
setMachine(machine: MachineGeometry): void;
|
|
30
|
+
setFilament(info: FilamentInfo): void;
|
|
31
|
+
setToolInfo(tool: number, info: {
|
|
32
|
+
material?: string;
|
|
33
|
+
colorHex?: string;
|
|
34
|
+
}): void;
|
|
35
|
+
addThumbnail(thumb: ThumbnailData): void;
|
|
36
|
+
setRaw(key: string, value: string): void;
|
|
37
|
+
upgradeCapability(key: string, confidence: Confidence): void;
|
|
38
|
+
warn(code: string, message: string, srcByte?: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Apply buffered annotations to the finished IR (channels + header) and merge
|
|
41
|
+
* this adapter's contribution into the shared metadata object. Range ops are
|
|
42
|
+
* clamped to the real segment count — a lying adapter cannot write outside
|
|
43
|
+
* the buffers, and geometry channels are unreachable by construction.
|
|
44
|
+
*/
|
|
45
|
+
apply(ir: ToolpathIR, metadata: DialectMetadata, conflictWarn: (code: string, message: string) => void): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,YAAY,EACZ,eAAe,EACf,aAAa,EACb,UAAU,EAEX,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAWrD,qBAAa,sBAAuB,YAAW,cAAc;IAY/C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAXtC,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAsB;IACvC,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,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,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;CAwD9G"}
|
package/dist/sink.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const MAX_RANGE_OPS = 1_000_000;
|
|
2
|
+
const MAX_OBJECTS = 10_000;
|
|
3
|
+
const MAX_RAW_ENTRIES = 512;
|
|
4
|
+
const MAX_RAW_VALUE = 4_096;
|
|
5
|
+
const MAX_WARNINGS = 100;
|
|
6
|
+
const MAX_THUMBNAILS = 8;
|
|
7
|
+
export class BufferedAnnotationSink {
|
|
8
|
+
adapterId;
|
|
9
|
+
ops = [];
|
|
10
|
+
objects = new Map();
|
|
11
|
+
machine = null;
|
|
12
|
+
filaments = [];
|
|
13
|
+
toolInfos = new Map();
|
|
14
|
+
thumbnails = [];
|
|
15
|
+
raw = new Map();
|
|
16
|
+
capabilities = new Map();
|
|
17
|
+
warnings = [];
|
|
18
|
+
overflow = false;
|
|
19
|
+
constructor(adapterId) {
|
|
20
|
+
this.adapterId = adapterId;
|
|
21
|
+
}
|
|
22
|
+
pushOp(op) {
|
|
23
|
+
if (this.ops.length >= MAX_RANGE_OPS) {
|
|
24
|
+
if (!this.overflow) {
|
|
25
|
+
this.overflow = true;
|
|
26
|
+
this.warn('dialect-annotation-truncated', `adapter '${this.adapterId}' exceeded the annotation op budget`);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (op.end < op.start || op.start < 0)
|
|
31
|
+
return;
|
|
32
|
+
this.ops.push(op);
|
|
33
|
+
}
|
|
34
|
+
setFeature(segStart, segEnd, role) {
|
|
35
|
+
this.pushOp({ channel: 'feature', start: segStart, end: segEnd, value: role & 0xff });
|
|
36
|
+
}
|
|
37
|
+
setObject(segStart, segEnd, objectId) {
|
|
38
|
+
this.pushOp({ channel: 'object', start: segStart, end: segEnd, value: objectId >>> 0 });
|
|
39
|
+
}
|
|
40
|
+
defineObject(id, name) {
|
|
41
|
+
if (this.objects.size < MAX_OBJECTS)
|
|
42
|
+
this.objects.set(id, name.slice(0, 256));
|
|
43
|
+
}
|
|
44
|
+
setMachine(machine) {
|
|
45
|
+
this.machine = machine;
|
|
46
|
+
}
|
|
47
|
+
setFilament(info) {
|
|
48
|
+
if (this.filaments.length < 64)
|
|
49
|
+
this.filaments.push(info);
|
|
50
|
+
}
|
|
51
|
+
setToolInfo(tool, info) {
|
|
52
|
+
if (this.toolInfos.size < 64)
|
|
53
|
+
this.toolInfos.set(tool, info);
|
|
54
|
+
}
|
|
55
|
+
addThumbnail(thumb) {
|
|
56
|
+
if (this.thumbnails.length < MAX_THUMBNAILS)
|
|
57
|
+
this.thumbnails.push(thumb);
|
|
58
|
+
}
|
|
59
|
+
setRaw(key, value) {
|
|
60
|
+
if (this.raw.size < MAX_RAW_ENTRIES)
|
|
61
|
+
this.raw.set(key.slice(0, 128), value.slice(0, MAX_RAW_VALUE));
|
|
62
|
+
}
|
|
63
|
+
upgradeCapability(key, confidence) {
|
|
64
|
+
this.capabilities.set(key, confidence);
|
|
65
|
+
}
|
|
66
|
+
warn(code, message, srcByte) {
|
|
67
|
+
const existing = this.warnings.find((w) => w.code === code);
|
|
68
|
+
if (existing) {
|
|
69
|
+
existing.count = (existing.count ?? 1) + 1;
|
|
70
|
+
}
|
|
71
|
+
else if (this.warnings.length < MAX_WARNINGS) {
|
|
72
|
+
this.warnings.push({ code, message, severity: 'warn', srcByte, count: 1 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Apply buffered annotations to the finished IR (channels + header) and merge
|
|
77
|
+
* this adapter's contribution into the shared metadata object. Range ops are
|
|
78
|
+
* clamped to the real segment count — a lying adapter cannot write outside
|
|
79
|
+
* the buffers, and geometry channels are unreachable by construction.
|
|
80
|
+
*/
|
|
81
|
+
apply(ir, metadata, conflictWarn) {
|
|
82
|
+
const count = ir.segments.count;
|
|
83
|
+
const touched = { feature: false, object: false };
|
|
84
|
+
for (const op of this.ops) {
|
|
85
|
+
const start = Math.min(op.start, count);
|
|
86
|
+
const end = Math.min(op.end + 1, count);
|
|
87
|
+
if (end <= start)
|
|
88
|
+
continue;
|
|
89
|
+
ir.segments[op.channel].fill(op.value, start, end);
|
|
90
|
+
touched[op.channel] = true;
|
|
91
|
+
}
|
|
92
|
+
// Object channel semantics (DD-001): value v > 0 indexes ir.objects[v-1].
|
|
93
|
+
for (const [channelValue, name] of [...this.objects.entries()].sort((a, b) => a[0] - b[0])) {
|
|
94
|
+
if (channelValue < 1)
|
|
95
|
+
continue;
|
|
96
|
+
while (ir.objects.length < channelValue) {
|
|
97
|
+
ir.objects.push({ id: String(ir.objects.length + 1) });
|
|
98
|
+
}
|
|
99
|
+
ir.objects[channelValue - 1] = { id: String(channelValue), name };
|
|
100
|
+
}
|
|
101
|
+
for (const [toolId, info] of this.toolInfos) {
|
|
102
|
+
let entry = ir.tools.find((t) => t.id === toolId);
|
|
103
|
+
if (entry === undefined) {
|
|
104
|
+
entry = { id: toolId };
|
|
105
|
+
ir.tools.push(entry);
|
|
106
|
+
ir.tools.sort((a, b) => a.id - b.id);
|
|
107
|
+
}
|
|
108
|
+
if (info.material !== undefined)
|
|
109
|
+
entry.material = info.material;
|
|
110
|
+
if (info.colorHex !== undefined) {
|
|
111
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(info.colorHex.trim());
|
|
112
|
+
if (m !== null) {
|
|
113
|
+
const v = parseInt(m[1], 16);
|
|
114
|
+
entry.color = { r: ((v >> 16) & 0xff) / 255, g: ((v >> 8) & 0xff) / 255, b: (v & 0xff) / 255, a: 1 };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const [key, confidence] of this.capabilities) {
|
|
119
|
+
ir.header.capabilities[key] = confidence;
|
|
120
|
+
}
|
|
121
|
+
for (const w of this.warnings) {
|
|
122
|
+
ir.header.warnings.push(w);
|
|
123
|
+
}
|
|
124
|
+
if (this.machine !== null) {
|
|
125
|
+
if (metadata.machine !== undefined) {
|
|
126
|
+
conflictWarn('dialect-conflict', `adapter '${this.adapterId}' overwrote machine geometry`);
|
|
127
|
+
}
|
|
128
|
+
metadata.machine = this.machine;
|
|
129
|
+
}
|
|
130
|
+
if (this.filaments.length > 0) {
|
|
131
|
+
metadata.filaments = [...(metadata.filaments ?? []), ...this.filaments];
|
|
132
|
+
}
|
|
133
|
+
if (this.thumbnails.length > 0) {
|
|
134
|
+
metadata.thumbnails = [...(metadata.thumbnails ?? []), ...this.thumbnails];
|
|
135
|
+
}
|
|
136
|
+
if (this.raw.size > 0) {
|
|
137
|
+
metadata.raw = { ...(metadata.raw ?? {}), ...Object.fromEntries(this.raw) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chestnutlabs/gcode-dialects",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Slicer/firmware dialect adapters for ToolpathIR annotation (DD-005). Adapters annotate metadata and optional channels \u2014 they can never alter geometry.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"gcode",
|
|
7
|
+
"3d-printing",
|
|
8
|
+
"toolpath",
|
|
9
|
+
"cnc",
|
|
10
|
+
"chestnutlabs",
|
|
11
|
+
"slicer",
|
|
12
|
+
"prusaslicer",
|
|
13
|
+
"cura",
|
|
14
|
+
"bambu",
|
|
15
|
+
"klipper",
|
|
16
|
+
"marlin",
|
|
17
|
+
"reprap"
|
|
18
|
+
],
|
|
19
|
+
"author": "Chestnut Labs",
|
|
20
|
+
"homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
|
|
21
|
+
"bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
|
|
25
|
+
"directory": "packages/gcode-dialects"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"types": "dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@chestnutlabs/toolpath-core": "0.1.0"
|
|
49
|
+
}
|
|
50
|
+
}
|