@chestnutlabs/toolpath-core 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 ADDED
@@ -0,0 +1,33 @@
1
+ # @chestnutlabs/toolpath-core
2
+
3
+ Neutral **`ToolpathIR`** and **capability model** for the Chestnut Labs G-code Preview toolpath stack.
4
+ This is the replacement seam for the whole stack: the parser **writes** it, renderers and analyzers **read**
5
+ it. See [`docs/design/DD-001`](../../docs/design/DD-001-toolpath-ir-and-capability-model.md).
6
+
7
+ **Design rules**
8
+
9
+ - The canonical IR is plain, **transferable** data — a small header plus **structure-of-arrays typed
10
+ buffers** (not object graphs). Positions are `Float32` deltas relative to a `Float64` `originOffset`
11
+ (floating origin, DD-001 §4.6).
12
+ - **Unknown is a valid state.** Optional/derived data carries a `Confidence`
13
+ (`known | inferred | approximated | unavailable`); missing values are sentinels (`NaN`, `0`), never
14
+ fabricated data.
15
+ - This package depends on **nothing** — no `three`, DOM, Vue, or AnyBridge (DD-002 boundary).
16
+
17
+ **Contents (scaffold)**
18
+
19
+ - `ir.ts` — `ToolpathIR`, header, segments (SoA), layers/tools/objects, capabilities, `MoveKind`,
20
+ `FeatureRole`, `IR_SCHEMA_VERSION`.
21
+ - `builder.ts` — `ToolpathIRBuilder` (accumulate segments → canonical typed-array IR).
22
+ - `source-index.ts` — `buildSourceIndex` / `segmentAtByte` (byte offset → segment; live-progress input).
23
+ - `bounds.ts` — extrude-only and travel-inclusive bounds.
24
+
25
+ ```bash
26
+ npm run build -w @chestnutlabs/toolpath-core # tsc -> dist
27
+ npm run typecheck -w @chestnutlabs/toolpath-core
28
+ npm test -w @chestnutlabs/toolpath-core
29
+ ```
30
+
31
+ > Status: **scaffold** (E1, issue #32). The builder accumulates into number arrays and converts to typed
32
+ > arrays in `finalize()`; E2's worker parser will write directly into growable typed buffers. Package
33
+ > extraction/publish is gated by DD-002 §7 and the release DD (E7/DD-008).
@@ -0,0 +1,11 @@
1
+ import { type ToolpathBounds, type ToolpathSegments, type Vec3 } from './ir.js';
2
+ export declare function emptyBounds(): ToolpathBounds;
3
+ /**
4
+ * Compute bounds over segment endpoints, converting Float32 deltas back to absolute
5
+ * coordinates via `origin`. Returns extrude-only bounds and travel-inclusive bounds.
6
+ */
7
+ export declare function computeSegmentBounds(seg: ToolpathSegments, origin: Vec3): {
8
+ bounds: ToolpathBounds;
9
+ boundsWithTravel: ToolpathBounds;
10
+ };
11
+ //# sourceMappingURL=bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../src/bounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAE1F,wBAAgB,WAAW,IAAI,cAAc,CAK5C;AAWD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,gBAAgB,EACrB,MAAM,EAAE,IAAI,GACX;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,gBAAgB,EAAE,cAAc,CAAA;CAAE,CAkB9D"}
package/dist/bounds.js ADDED
@@ -0,0 +1,44 @@
1
+ import { MoveKind } from './ir.js';
2
+ export function emptyBounds() {
3
+ return {
4
+ min: { x: Infinity, y: Infinity, z: Infinity },
5
+ max: { x: -Infinity, y: -Infinity, z: -Infinity }
6
+ };
7
+ }
8
+ function expand(b, x, y, z) {
9
+ if (x < b.min.x)
10
+ b.min.x = x;
11
+ if (y < b.min.y)
12
+ b.min.y = y;
13
+ if (z < b.min.z)
14
+ b.min.z = z;
15
+ if (x > b.max.x)
16
+ b.max.x = x;
17
+ if (y > b.max.y)
18
+ b.max.y = y;
19
+ if (z > b.max.z)
20
+ b.max.z = z;
21
+ }
22
+ /**
23
+ * Compute bounds over segment endpoints, converting Float32 deltas back to absolute
24
+ * coordinates via `origin`. Returns extrude-only bounds and travel-inclusive bounds.
25
+ */
26
+ export function computeSegmentBounds(seg, origin) {
27
+ const bounds = emptyBounds();
28
+ const boundsWithTravel = emptyBounds();
29
+ for (let i = 0; i < seg.count; i++) {
30
+ const sx = origin.x + seg.x0[i];
31
+ const sy = origin.y + seg.y0[i];
32
+ const sz = origin.z + seg.z0[i];
33
+ const ex = origin.x + seg.x1[i];
34
+ const ey = origin.y + seg.y1[i];
35
+ const ez = origin.z + seg.z1[i];
36
+ expand(boundsWithTravel, sx, sy, sz);
37
+ expand(boundsWithTravel, ex, ey, ez);
38
+ if ((seg.kind[i] & MoveKind.Extrude) !== 0) {
39
+ expand(bounds, sx, sy, sz);
40
+ expand(bounds, ex, ey, ez);
41
+ }
42
+ }
43
+ return { bounds, boundsWithTravel };
44
+ }
@@ -0,0 +1,73 @@
1
+ import { type Confidence, type ObjectInfo, type ToolInfo, type ToolpathIR, type Units, type Vec3, type Warning } from './ir.js';
2
+ /** One motion segment in absolute model coordinates. The builder stores it as a delta from the origin. */
3
+ export interface SegmentInput {
4
+ x0: number;
5
+ y0: number;
6
+ z0: number;
7
+ x1: number;
8
+ y1: number;
9
+ z1: number;
10
+ /** Extrusion delta; omit or 0 for travel. */
11
+ e?: number;
12
+ /** mm/min; omit when unknown (stored as NaN). */
13
+ feedrate?: number;
14
+ /** MoveKind bitflags. */
15
+ kind: number;
16
+ tool?: number;
17
+ layer: number;
18
+ feature?: number;
19
+ /** Object index + 1; 0/omitted = none. */
20
+ object?: number;
21
+ srcByte: number;
22
+ }
23
+ export interface BuilderOptions {
24
+ parserVersion?: string;
25
+ units?: Units;
26
+ unitsSource?: Confidence;
27
+ source?: {
28
+ id?: string;
29
+ byteLength?: number;
30
+ sha256?: string;
31
+ };
32
+ /** Fixed floating-origin reference; defaults to the first segment's start point. */
33
+ originOffset?: Vec3;
34
+ }
35
+ /**
36
+ * Accumulates segments and produces a canonical {@link ToolpathIR}.
37
+ *
38
+ * The builder accumulates into plain number arrays and converts to typed arrays in
39
+ * {@link ToolpathIRBuilder.finalize}. The *canonical* IR it returns is already the compact,
40
+ * transferable typed-array contract from DD-001; E2 (the worker parser) will additionally
41
+ * write directly into growable typed buffers to avoid the intermediate arrays.
42
+ */
43
+ export declare class ToolpathIRBuilder {
44
+ private readonly opts;
45
+ private originOffset;
46
+ private readonly x0;
47
+ private readonly y0;
48
+ private readonly z0;
49
+ private readonly x1;
50
+ private readonly y1;
51
+ private readonly z1;
52
+ private readonly e;
53
+ private readonly feedrate;
54
+ private readonly kind;
55
+ private readonly tool;
56
+ private readonly layer;
57
+ private readonly feature;
58
+ private readonly object;
59
+ private readonly srcByte;
60
+ private readonly toolsMap;
61
+ private readonly objectsList;
62
+ private readonly warnings;
63
+ private readonly capabilities;
64
+ constructor(opts?: BuilderOptions);
65
+ addSegment(s: SegmentInput): void;
66
+ addWarning(w: Warning): void;
67
+ setCapability(name: string, confidence: Confidence): void;
68
+ setTool(info: ToolInfo): void;
69
+ /** Register an object; returns its 1-based index for use in `SegmentInput.object`. */
70
+ addObject(info: ObjectInfo): number;
71
+ finalize(): ToolpathIR;
72
+ }
73
+ //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,UAAU,EAIf,KAAK,KAAK,EACV,KAAK,IAAI,EACT,KAAK,OAAO,EACb,MAAM,SAAS,CAAC;AAIjB,0GAA0G;AAC1G,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,oFAAoF;IACpF,YAAY,CAAC,EAAE,IAAI,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,YAAY,CAAc;IAElC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IAExC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+B;IACxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;gBAEnD,IAAI,GAAE,cAAmB;IAKrC,UAAU,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;IAwBjC,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAI5B,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAIzD,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAI7B,sFAAsF;IACtF,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAKnC,QAAQ,IAAI,UAAU;CA4CvB"}
@@ -0,0 +1,131 @@
1
+ import { IR_SCHEMA_VERSION } from './ir.js';
2
+ import { computeSegmentBounds } from './bounds.js';
3
+ import { buildSourceIndex } from './source-index.js';
4
+ /**
5
+ * Accumulates segments and produces a canonical {@link ToolpathIR}.
6
+ *
7
+ * The builder accumulates into plain number arrays and converts to typed arrays in
8
+ * {@link ToolpathIRBuilder.finalize}. The *canonical* IR it returns is already the compact,
9
+ * transferable typed-array contract from DD-001; E2 (the worker parser) will additionally
10
+ * write directly into growable typed buffers to avoid the intermediate arrays.
11
+ */
12
+ export class ToolpathIRBuilder {
13
+ opts;
14
+ originOffset;
15
+ x0 = [];
16
+ y0 = [];
17
+ z0 = [];
18
+ x1 = [];
19
+ y1 = [];
20
+ z1 = [];
21
+ e = [];
22
+ feedrate = [];
23
+ kind = [];
24
+ tool = [];
25
+ layer = [];
26
+ feature = [];
27
+ object = [];
28
+ srcByte = [];
29
+ toolsMap = new Map();
30
+ objectsList = [];
31
+ warnings = [];
32
+ capabilities = {};
33
+ constructor(opts = {}) {
34
+ this.opts = opts;
35
+ this.originOffset = opts.originOffset ?? null;
36
+ }
37
+ addSegment(s) {
38
+ if (this.originOffset === null) {
39
+ this.originOffset = { x: s.x0, y: s.y0, z: s.z0 };
40
+ }
41
+ const o = this.originOffset;
42
+ this.x0.push(s.x0 - o.x);
43
+ this.y0.push(s.y0 - o.y);
44
+ this.z0.push(s.z0 - o.z);
45
+ this.x1.push(s.x1 - o.x);
46
+ this.y1.push(s.y1 - o.y);
47
+ this.z1.push(s.z1 - o.z);
48
+ this.e.push(s.e ?? 0);
49
+ this.feedrate.push(s.feedrate ?? NaN);
50
+ this.kind.push(s.kind);
51
+ this.tool.push(s.tool ?? 0);
52
+ this.layer.push(s.layer);
53
+ this.feature.push(s.feature ?? 0);
54
+ this.object.push(s.object ?? 0);
55
+ this.srcByte.push(s.srcByte);
56
+ if (s.tool !== undefined && !this.toolsMap.has(s.tool)) {
57
+ this.toolsMap.set(s.tool, { id: s.tool });
58
+ }
59
+ }
60
+ addWarning(w) {
61
+ this.warnings.push(w);
62
+ }
63
+ setCapability(name, confidence) {
64
+ this.capabilities[name] = confidence;
65
+ }
66
+ setTool(info) {
67
+ this.toolsMap.set(info.id, info);
68
+ }
69
+ /** Register an object; returns its 1-based index for use in `SegmentInput.object`. */
70
+ addObject(info) {
71
+ this.objectsList.push(info);
72
+ return this.objectsList.length;
73
+ }
74
+ finalize() {
75
+ const count = this.x0.length;
76
+ const segments = {
77
+ count,
78
+ x0: Float32Array.from(this.x0),
79
+ y0: Float32Array.from(this.y0),
80
+ z0: Float32Array.from(this.z0),
81
+ x1: Float32Array.from(this.x1),
82
+ y1: Float32Array.from(this.y1),
83
+ z1: Float32Array.from(this.z1),
84
+ e: Float32Array.from(this.e),
85
+ feedrate: Float32Array.from(this.feedrate),
86
+ kind: Uint8Array.from(this.kind),
87
+ tool: Uint16Array.from(this.tool),
88
+ layer: Uint32Array.from(this.layer),
89
+ feature: Uint8Array.from(this.feature),
90
+ object: Uint32Array.from(this.object),
91
+ srcByte: Uint32Array.from(this.srcByte)
92
+ };
93
+ const origin = this.originOffset ?? { x: 0, y: 0, z: 0 };
94
+ const layers = deriveLayers(segments, origin.z);
95
+ const { bounds, boundsWithTravel } = computeSegmentBounds(segments, origin);
96
+ const sourceIndex = buildSourceIndex(segments.srcByte, count);
97
+ const header = {
98
+ irSchemaVersion: IR_SCHEMA_VERSION,
99
+ parserVersion: this.opts.parserVersion ?? 'unknown',
100
+ source: {
101
+ id: this.opts.source?.id,
102
+ byteLength: this.opts.source?.byteLength ?? 0,
103
+ sha256: this.opts.source?.sha256
104
+ },
105
+ units: this.opts.units ?? 'mm',
106
+ unitsSource: this.opts.unitsSource ?? 'unavailable',
107
+ originOffset: origin,
108
+ complete: true,
109
+ dialects: [],
110
+ warnings: this.warnings,
111
+ capabilities: this.capabilities
112
+ };
113
+ const tools = [...this.toolsMap.values()].sort((a, b) => a.id - b.id);
114
+ return { header, segments, layers, tools, objects: this.objectsList, bounds, boundsWithTravel, sourceIndex };
115
+ }
116
+ }
117
+ /** Group segments into layers by their `layer` index; layer Z is absolute. Assumes dense layer indices. */
118
+ function deriveLayers(seg, originZ) {
119
+ const byIndex = new Map();
120
+ for (let i = 0; i < seg.count; i++) {
121
+ const li = seg.layer[i];
122
+ const existing = byIndex.get(li);
123
+ if (existing === undefined) {
124
+ byIndex.set(li, { z: originZ + seg.z1[i], segStart: i, segEnd: i });
125
+ }
126
+ else {
127
+ existing.segEnd = i;
128
+ }
129
+ }
130
+ return [...byIndex.keys()].sort((a, b) => a - b).map((k) => byIndex.get(k));
131
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @chestnutlabs/toolpath-core — neutral ToolpathIR and capability model (DD-001).
3
+ *
4
+ * Public entry point. This package depends on nothing (no `three`, DOM, Vue, or AnyBridge)
5
+ * and is the replacement seam for the whole toolpath stack: the parser writes it, renderers
6
+ * and analyzers read it.
7
+ */
8
+ export * from './ir.js';
9
+ export * from './builder.js';
10
+ export * from './bounds.js';
11
+ export * from './source-index.js';
12
+ export * from './metadata.js';
13
+ export * from './progress.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @chestnutlabs/toolpath-core — neutral ToolpathIR and capability model (DD-001).
3
+ *
4
+ * Public entry point. This package depends on nothing (no `three`, DOM, Vue, or AnyBridge)
5
+ * and is the replacement seam for the whole toolpath stack: the parser writes it, renderers
6
+ * and analyzers read it.
7
+ */
8
+ export * from './ir.js';
9
+ export * from './builder.js';
10
+ export * from './bounds.js';
11
+ export * from './source-index.js';
12
+ export * from './metadata.js';
13
+ export * from './progress.js';
package/dist/ir.d.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * ToolpathIR — the neutral, versioned intermediate representation (DD-001).
3
+ *
4
+ * The canonical IR is plain, transferable data: a small metadata header plus
5
+ * structure-of-arrays (SoA) typed buffers. No class instances, DOM, `three`,
6
+ * Vue, or consumer (AnyBridge) types appear here. Positions are `Float32`
7
+ * deltas relative to a `Float64` `originOffset` (floating origin, DD-001 §4.6).
8
+ */
9
+ /** Bump on any breaking layout/semantic change. Separate from the package version. */
10
+ export declare const IR_SCHEMA_VERSION = 1;
11
+ /** Confidence of an optional/derived datum. `unavailable` is a valid state — never a fabricated 0. */
12
+ export type Confidence = 'known' | 'inferred' | 'approximated' | 'unavailable';
13
+ export type Units = 'mm' | 'in';
14
+ export type Severity = 'info' | 'warn' | 'error';
15
+ export interface Vec3 {
16
+ x: number;
17
+ y: number;
18
+ z: number;
19
+ }
20
+ export interface RGBA {
21
+ r: number;
22
+ g: number;
23
+ b: number;
24
+ a: number;
25
+ }
26
+ /** Bitflags stored in `ToolpathSegments.kind`. */
27
+ export declare const MoveKind: {
28
+ readonly None: 0;
29
+ readonly Extrude: number;
30
+ readonly Travel: number;
31
+ readonly Retract: number;
32
+ readonly Unretract: number;
33
+ readonly Wipe: number;
34
+ readonly ArcSegment: number;
35
+ readonly Seam: number;
36
+ };
37
+ export type MoveKindName = keyof typeof MoveKind;
38
+ /** Feature-role indices stored in `ToolpathSegments.feature` (0 = unknown). */
39
+ export declare const FeatureRole: {
40
+ readonly Unknown: 0;
41
+ readonly Perimeter: 1;
42
+ readonly ExternalPerimeter: 2;
43
+ readonly Infill: 3;
44
+ readonly SolidInfill: 4;
45
+ readonly Support: 5;
46
+ readonly Skirt: 6;
47
+ readonly Brim: 7;
48
+ readonly Bridge: 8;
49
+ readonly Travel: 9;
50
+ readonly Custom: 10;
51
+ };
52
+ export type FeatureRoleName = keyof typeof FeatureRole;
53
+ export interface Warning {
54
+ code: string;
55
+ message: string;
56
+ severity: Severity;
57
+ srcByte?: number;
58
+ count?: number;
59
+ }
60
+ export interface DialectDecision {
61
+ id: string;
62
+ version?: string;
63
+ confidence: Confidence;
64
+ }
65
+ export interface ToolpathIRHeader {
66
+ irSchemaVersion: number;
67
+ parserVersion: string;
68
+ source: {
69
+ id?: string;
70
+ byteLength: number;
71
+ sha256?: string;
72
+ };
73
+ units: Units;
74
+ unitsSource: Confidence;
75
+ /** Float64 reference point; segment positions are Float32 deltas from this (DD-001 §4.6). */
76
+ originOffset: Vec3;
77
+ complete: boolean;
78
+ truncatedAtByte?: number;
79
+ dialects: DialectDecision[];
80
+ warnings: Warning[];
81
+ /** Per-field confidence summary, e.g. { featureRoles: 'unavailable', units: 'known' }. */
82
+ capabilities: Record<string, Confidence>;
83
+ }
84
+ /**
85
+ * Ordered motion segments as parallel typed arrays of length `count`.
86
+ * Positions are Float32 deltas relative to `ToolpathIRHeader.originOffset`.
87
+ * Optional channels use sentinels: `feedrate` NaN = unknown; `feature`/`object` 0 = unknown/none.
88
+ */
89
+ export interface ToolpathSegments {
90
+ count: number;
91
+ x0: Float32Array;
92
+ y0: Float32Array;
93
+ z0: Float32Array;
94
+ x1: Float32Array;
95
+ y1: Float32Array;
96
+ z1: Float32Array;
97
+ /** Extrusion delta for the segment (0 for travel). */
98
+ e: Float32Array;
99
+ /** mm/min; NaN when unknown. */
100
+ feedrate: Float32Array;
101
+ /** MoveKind bitflags. */
102
+ kind: Uint8Array;
103
+ /** Index into `tools`. */
104
+ tool: Uint16Array;
105
+ /** Index into `layers`. */
106
+ layer: Uint32Array;
107
+ /** FeatureRole index; 0 = unknown. */
108
+ feature: Uint8Array;
109
+ /** Index into `objects` + 1; 0 = none/unknown. */
110
+ object: Uint32Array;
111
+ /** Byte offset in the source of the command that produced this segment. */
112
+ srcByte: Uint32Array;
113
+ }
114
+ export interface ToolpathLayer {
115
+ /** Absolute Z of the layer. */
116
+ z: number;
117
+ segStart: number;
118
+ segEnd: number;
119
+ }
120
+ export interface ToolInfo {
121
+ id: number;
122
+ color?: RGBA;
123
+ material?: string;
124
+ }
125
+ export interface ObjectInfo {
126
+ id: string;
127
+ name?: string;
128
+ }
129
+ export interface ToolpathBounds {
130
+ min: Vec3;
131
+ max: Vec3;
132
+ }
133
+ /** Sorted parallel arrays enabling `byteOffset -> segmentIndex` via binary search (E5/DD-006 input). */
134
+ export interface SourceIndex {
135
+ byteOffsets: Uint32Array;
136
+ segmentIndices: Uint32Array;
137
+ }
138
+ export interface ToolpathIR {
139
+ header: ToolpathIRHeader;
140
+ segments: ToolpathSegments;
141
+ layers: ToolpathLayer[];
142
+ tools: ToolInfo[];
143
+ objects: ObjectInfo[];
144
+ /** Bounds over extruding moves only. */
145
+ bounds: ToolpathBounds;
146
+ /** Bounds including travel moves. */
147
+ boundsWithTravel: ToolpathBounds;
148
+ sourceIndex: SourceIndex;
149
+ }
150
+ //# sourceMappingURL=ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../src/ir.ts"],"names":[],"mappings":"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"}
package/dist/ir.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ToolpathIR — the neutral, versioned intermediate representation (DD-001).
3
+ *
4
+ * The canonical IR is plain, transferable data: a small metadata header plus
5
+ * structure-of-arrays (SoA) typed buffers. No class instances, DOM, `three`,
6
+ * Vue, or consumer (AnyBridge) types appear here. Positions are `Float32`
7
+ * deltas relative to a `Float64` `originOffset` (floating origin, DD-001 §4.6).
8
+ */
9
+ /** Bump on any breaking layout/semantic change. Separate from the package version. */
10
+ export const IR_SCHEMA_VERSION = 1;
11
+ /** Bitflags stored in `ToolpathSegments.kind`. */
12
+ export const MoveKind = {
13
+ None: 0,
14
+ Extrude: 1 << 0,
15
+ Travel: 1 << 1,
16
+ Retract: 1 << 2,
17
+ Unretract: 1 << 3,
18
+ Wipe: 1 << 4,
19
+ ArcSegment: 1 << 5,
20
+ Seam: 1 << 6
21
+ };
22
+ /** Feature-role indices stored in `ToolpathSegments.feature` (0 = unknown). */
23
+ export const FeatureRole = {
24
+ Unknown: 0,
25
+ Perimeter: 1,
26
+ ExternalPerimeter: 2,
27
+ Infill: 3,
28
+ SolidInfill: 4,
29
+ Support: 5,
30
+ Skirt: 6,
31
+ Brim: 7,
32
+ Bridge: 8,
33
+ Travel: 9,
34
+ Custom: 10
35
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Dialect/machine metadata data contracts (DD-005 §4.2, as amended).
3
+ *
4
+ * Pure data types — no behavior. They live in toolpath-core (not
5
+ * @chestnutlabs/gcode-dialects) because every layer consumes them as values:
6
+ * the parser carries them beside the IR in ParseResult/protocol messages and
7
+ * the renderer applies MachineGeometry as a build volume — neither may depend
8
+ * on the dialects package (DD-002 §5 boundaries). The behavioral contracts
9
+ * (DialectAdapter, AnnotationSink, registry) live in gcode-dialects.
10
+ */
11
+ import type { Confidence } from './ir.js';
12
+ export interface Point2 {
13
+ x: number;
14
+ y: number;
15
+ }
16
+ /** A 2D region in printer coordinates (excluded areas etc.). */
17
+ export interface Region2 {
18
+ kind: 'rect' | 'polygon';
19
+ points: Point2[];
20
+ }
21
+ /**
22
+ * Machine/bed geometry discovered from a file (DD-005 §4.2, amendment 2).
23
+ * Never fabricated: absent means unknown, and consumers must not invent one.
24
+ */
25
+ export interface MachineGeometry {
26
+ bed: {
27
+ kind: 'rect';
28
+ min: Point2;
29
+ max: Point2;
30
+ } | {
31
+ kind: 'circular';
32
+ center: Point2;
33
+ diameter: number;
34
+ } | {
35
+ kind: 'polygon';
36
+ points: Point2[];
37
+ };
38
+ /** Printer-coordinate origin location — explicit, not a convention flag. */
39
+ origin: Point2;
40
+ /** Regions the toolhead must avoid (Klipper excludes, Bambu excluded areas). */
41
+ excludedRegions?: Region2[];
42
+ heightMm?: number;
43
+ printerName?: string;
44
+ /** 'known' from container/config data; 'inferred' from slicer comments. */
45
+ confidence: Confidence;
46
+ /** Provenance: which adapter concluded this, from what evidence. */
47
+ source: {
48
+ adapterId: string;
49
+ evidence: string;
50
+ srcByte?: number;
51
+ };
52
+ }
53
+ /** A dialect detection decision with its evidence (DD-005 §4.1). */
54
+ export interface DialectDetection {
55
+ dialectId: string;
56
+ /** 'slicer' and 'firmware' adapters compose (amendment 1). */
57
+ kind: 'slicer' | 'firmware' | 'generic';
58
+ confidence: Confidence;
59
+ evidence: string;
60
+ }
61
+ export interface FilamentInfo {
62
+ slot: number;
63
+ type?: string;
64
+ color?: string;
65
+ name?: string;
66
+ }
67
+ export interface ThumbnailData {
68
+ width: number;
69
+ height: number;
70
+ mime: string;
71
+ bytes: Uint8Array;
72
+ }
73
+ /**
74
+ * Optional result metadata riding beside the IR (DD-005 §4.2) — no IR schema
75
+ * bump; structured-cloneable across the worker boundary (thumbnail bytes
76
+ * transferable).
77
+ */
78
+ export interface DialectMetadata {
79
+ /** Every applied adapter (composition, amendment 1). */
80
+ dialects?: DialectDetection[];
81
+ machine?: MachineGeometry;
82
+ filaments?: FilamentInfo[];
83
+ thumbnails?: ThumbnailData[];
84
+ /** Whitelisted key/value settings only — bounded, never local paths. */
85
+ raw?: Record<string, string>;
86
+ }
87
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EACC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAC1C;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GACtD;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC1C,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,UAAU,EAAE,UAAU,CAAC;IACvB,oEAAoE;IACpE,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAED,oEAAoE;AACpE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Normalized live progress: observation contract + source-position mapper (DD-006).
3
+ *
4
+ * A host (e.g. AnyBridge) pushes `ProgressObservation`s built from its own telemetry;
5
+ * `createProgressMapper` turns each into a `MappedProgress` over an existing `ToolpathIR`
6
+ * using the fallback hierarchy of DD-006 §4.3: byte > line (reserved) > layer > percent.
7
+ * Everything here is plain serializable data and pure IR math — no telemetry transport,
8
+ * no worker round-trip, O(log n) per observation.
9
+ *
10
+ * Honesty rules are the point: confidence reuses the DD-001 vocabulary, approximate tiers
11
+ * carry an uncertainty band instead of pretending to be a point, and unusable observations
12
+ * degrade to `unavailable` — never a fabricated position.
13
+ */
14
+ import { type Confidence, type ToolpathIR } from './ir.js';
15
+ /** Contract version of `ProgressObservation`. Bump only on breaking shape changes. */
16
+ export declare const PROGRESS_OBSERVATION_VERSION = 1;
17
+ /** Default staleness threshold (DD-006 §4.4.3). */
18
+ export declare const DEFAULT_STALE_AFTER_MS = 10000;
19
+ /** What a percent observation measures — decides how it maps (DD-006 D4). */
20
+ export type ProgressPercentBasis = 'bytes' | 'job' | 'unknown';
21
+ export type ProgressJobState = 'printing' | 'paused' | 'complete' | 'cancelled' | 'unknown';
22
+ /** Identity evidence for the file the printer is executing (mismatch detection, DD-006 §4.4.1). */
23
+ export interface ProgressFileIdentity {
24
+ name?: string;
25
+ sizeBytes?: number;
26
+ sha256?: string;
27
+ }
28
+ export interface ProgressPosition {
29
+ /** Exact byte offset into the byte stream the parser consumed (DD-006 §4.4.1 byte domain). */
30
+ byte?: number;
31
+ /** 0-based source line. Reserved (D3): carried and serialized, not mapped in v1. */
32
+ line?: number;
33
+ /** Current layer as reported by the printer/host (numbering caveats, DD-006 §4.3). */
34
+ layer?: number;
35
+ totalLayers?: number;
36
+ /** Fraction 0..1. */
37
+ percent?: number;
38
+ percentBasis?: ProgressPercentBasis;
39
+ }
40
+ /** One consumer-supplied snapshot of where the printer is. All position facts optional. */
41
+ export interface ProgressObservation {
42
+ v: 1;
43
+ /** Consumer clock (ms) — drives staleness via `tick()`. */
44
+ timestampMs: number;
45
+ file?: ProgressFileIdentity;
46
+ position?: ProgressPosition;
47
+ state?: ProgressJobState;
48
+ }
49
+ /** Which observation fact won the fallback hierarchy. */
50
+ export type ProgressBasis = 'byte' | 'line' | 'layer' | 'percent' | 'none';
51
+ export interface ProgressNote {
52
+ code: string;
53
+ message?: string;
54
+ }
55
+ export interface MappedProgress {
56
+ /** Last segment at-or-before the observed position; null when unavailable (or before the first segment). */
57
+ segIndex: number | null;
58
+ basis: ProgressBasis;
59
+ /** DD-001 vocabulary (D2): byte→known, line/layer→inferred, percent→approximated, none→unavailable. */
60
+ confidence: Confidence;
61
+ /** Inclusive uncertainty band [loSeg, hiSeg]; a point (lo === hi) for the byte tier. */
62
+ band: [number, number] | null;
63
+ layerIndex: number | null;
64
+ /** True once `tick(now)` observes `now - timestampMs > staleAfterMs`. */
65
+ stale: boolean;
66
+ /** Structured degradation reasons (capped at {@link MAX_PROGRESS_NOTES}). */
67
+ notes: ProgressNote[];
68
+ }
69
+ export interface ProgressMapperOptions {
70
+ /** Staleness threshold in ms (default {@link DEFAULT_STALE_AFTER_MS}). */
71
+ staleAfterMs?: number;
72
+ /** Byte length of the parsed source; enables percent(bytes) promotion to the byte tier (D4). */
73
+ fileSizeBytes?: number;
74
+ }
75
+ export interface ProgressMapper {
76
+ /** Map one observation. Never throws on observation content (DD-006 §6). */
77
+ observe(obs: ProgressObservation): MappedProgress;
78
+ /** Recompute staleness against `nowMs` without a new observation. */
79
+ tick(nowMs: number): MappedProgress;
80
+ reset(): void;
81
+ }
82
+ /** Notes are bounded so a hostile/buggy host cannot grow memory (DD-006 §7). */
83
+ export declare const MAX_PROGRESS_NOTES = 8;
84
+ /**
85
+ * Build a `ProgressMapper` over a parsed IR. The mapper keeps only the last observation's
86
+ * timestamp/result (for `tick`); it never mutates the IR.
87
+ */
88
+ export declare function createProgressMapper(ir: ToolpathIR, opts?: ProgressMapperOptions): ProgressMapper;
89
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAG3D,sFAAsF;AACtF,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,mDAAmD;AACnD,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,6EAA6E;AAC7E,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,CAAC;AAE5F,mGAAmG;AACnG,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC,CAAC,EAAE,CAAC,CAAC;IACL,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE3E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,4GAA4G;IAC5G,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,uGAAuG;IACvG,UAAU,EAAE,UAAU,CAAC;IACvB,wFAAwF;IACxF,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,yEAAyE;IACzE,KAAK,EAAE,OAAO,CAAC;IACf,6EAA6E;IAC7E,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,cAAc,CAAC;IAClD,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,IAAI,CAAC;AA0EpC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAmQjG"}
@@ -0,0 +1,316 @@
1
+ import { segmentAtByte } from './source-index.js';
2
+ /** Contract version of `ProgressObservation`. Bump only on breaking shape changes. */
3
+ export const PROGRESS_OBSERVATION_VERSION = 1;
4
+ /** Default staleness threshold (DD-006 §4.4.3). */
5
+ export const DEFAULT_STALE_AFTER_MS = 10_000;
6
+ /** Notes are bounded so a hostile/buggy host cannot grow memory (DD-006 §7). */
7
+ export const MAX_PROGRESS_NOTES = 8;
8
+ /** Widening factor for percent(bytes) promotion: the source is still a fraction (§4.3 tier 4). */
9
+ const PERCENT_BYTES_BAND_FRACTION = 0.005;
10
+ /** Minimum half-width for ordinal percent interpolation (§4.3 tier 5). */
11
+ const PERCENT_ORDINAL_BAND_FRACTION = 0.02;
12
+ /** Relative file-size disagreement that demotes the byte domain (§4.4.1). */
13
+ const FILE_SIZE_MISMATCH_TOLERANCE = 0.001;
14
+ /** Reported-vs-IR layer-count disagreement beyond which the reported layer is a fraction (§4.3). */
15
+ const LAYER_COUNT_MISMATCH_TOLERANCE = 2;
16
+ /** Backward layer movement at or below this re-syncs silently (§4.4.2). */
17
+ const REGRESSION_LAYER_TOLERANCE = 2;
18
+ const UNAVAILABLE = Object.freeze({
19
+ segIndex: null,
20
+ basis: 'none',
21
+ confidence: 'unavailable',
22
+ band: null,
23
+ layerIndex: null,
24
+ stale: false,
25
+ notes: []
26
+ });
27
+ function finiteNonNegative(value) {
28
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
29
+ }
30
+ /** Extract the usable numeric facts, noting (not throwing on) malformed ones (DD-006 §6). */
31
+ function sanitizePosition(position, notes) {
32
+ const out = { percentBasis: 'unknown' };
33
+ if (position === undefined || position === null || typeof position !== 'object')
34
+ return out;
35
+ for (const field of ['byte', 'line', 'layer', 'totalLayers', 'percent']) {
36
+ const raw = position[field];
37
+ if (raw === undefined || raw === null)
38
+ continue;
39
+ const value = finiteNonNegative(raw);
40
+ if (value === undefined) {
41
+ pushNote(notes, { code: 'invalid-field', message: `position.${field} ignored` });
42
+ }
43
+ else {
44
+ out[field] = value;
45
+ }
46
+ }
47
+ if (out.percent !== undefined && out.percent > 1) {
48
+ pushNote(notes, { code: 'invalid-field', message: 'position.percent > 1 ignored' });
49
+ out.percent = undefined;
50
+ }
51
+ if (position.percentBasis === 'bytes' || position.percentBasis === 'job') {
52
+ out.percentBasis = position.percentBasis;
53
+ }
54
+ return out;
55
+ }
56
+ function pushNote(notes, note) {
57
+ if (notes.length < MAX_PROGRESS_NOTES)
58
+ notes.push(note);
59
+ }
60
+ function clampSeg(ir, seg) {
61
+ return Math.max(0, Math.min(ir.segments.count - 1, seg));
62
+ }
63
+ function layerOfSegment(ir, segIndex) {
64
+ if (segIndex === null || ir.segments.count === 0)
65
+ return null;
66
+ return ir.segments.layer[segIndex];
67
+ }
68
+ /**
69
+ * Build a `ProgressMapper` over a parsed IR. The mapper keeps only the last observation's
70
+ * timestamp/result (for `tick`); it never mutates the IR.
71
+ */
72
+ export function createProgressMapper(ir, opts) {
73
+ const staleAfterMs = opts?.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
74
+ const fileSizeBytes = finiteNonNegative(opts?.fileSizeBytes);
75
+ let lastTimestampMs = null;
76
+ let lastResult = UNAVAILABLE;
77
+ function mapByte(byte, confidence, bandHalfWidth, notes) {
78
+ const seg = segmentAtByte(ir.sourceIndex, byte);
79
+ if (seg === -1) {
80
+ // Position precedes the first segment: nothing completed yet — honest empty, not seg 0.
81
+ pushNote(notes, { code: 'before-first-segment' });
82
+ return { segIndex: null, basis: 'byte', confidence, band: null, layerIndex: null, stale: false, notes };
83
+ }
84
+ const lo = clampSeg(ir, seg - bandHalfWidth);
85
+ const hi = clampSeg(ir, seg + bandHalfWidth);
86
+ return {
87
+ segIndex: seg,
88
+ basis: 'byte',
89
+ confidence,
90
+ band: [lo, hi],
91
+ layerIndex: layerOfSegment(ir, seg),
92
+ stale: false,
93
+ notes
94
+ };
95
+ }
96
+ function mapLayer(reported, notes) {
97
+ const layerCount = ir.layers.length;
98
+ let layer = Math.floor(reported);
99
+ if (layer >= layerCount) {
100
+ pushNote(notes, { code: 'layer-out-of-range', message: `reported ${layer}, IR has ${layerCount}` });
101
+ layer = layerCount - 1;
102
+ }
103
+ const entry = ir.layers[layer];
104
+ // segEnd is inclusive; "somewhere in layer L" maps to its last segment with a whole-layer band.
105
+ return {
106
+ segIndex: entry.segEnd,
107
+ basis: 'layer',
108
+ confidence: 'inferred',
109
+ band: [entry.segStart, entry.segEnd],
110
+ layerIndex: layer,
111
+ stale: false,
112
+ notes
113
+ };
114
+ }
115
+ function mapPercentOrdinal(percent, notes) {
116
+ const count = ir.segments.count;
117
+ const seg = clampSeg(ir, Math.round(percent * (count - 1)));
118
+ const halfWidth = Math.ceil(count * PERCENT_ORDINAL_BAND_FRACTION);
119
+ const layer = ir.segments.layer[seg];
120
+ const entry = ir.layers[layer];
121
+ // Band: at least ±2% of segments, widened to cover the whole containing layer (§4.3 tier 5).
122
+ const lo = Math.min(clampSeg(ir, seg - halfWidth), entry?.segStart ?? seg);
123
+ const hi = Math.max(clampSeg(ir, seg + halfWidth), entry?.segEnd ?? seg);
124
+ return {
125
+ segIndex: seg,
126
+ basis: 'percent',
127
+ confidence: 'approximated',
128
+ band: [lo, hi],
129
+ layerIndex: layer,
130
+ stale: false,
131
+ notes
132
+ };
133
+ }
134
+ /**
135
+ * File-identity check (§4.4.1). A hash disagreement kills the mapping outright; a size
136
+ * disagreement demotes the byte domain (byte + percent-bytes promotion) to fraction mapping.
137
+ */
138
+ function checkIdentity(file, notes) {
139
+ if (file === undefined || file === null || typeof file !== 'object')
140
+ return 'ok';
141
+ const expectedSha = ir.header.source.sha256;
142
+ if (typeof file.sha256 === 'string' && expectedSha !== undefined && file.sha256 !== expectedSha) {
143
+ pushNote(notes, { code: 'file-mismatch', message: 'sha256 disagrees with the parsed source' });
144
+ return 'unavailable';
145
+ }
146
+ const obsSize = finiteNonNegative(file.sizeBytes);
147
+ const expectedSize = fileSizeBytes ?? finiteNonNegative(ir.header.source.byteLength);
148
+ if (obsSize !== undefined && expectedSize !== undefined && expectedSize > 0) {
149
+ if (Math.abs(obsSize - expectedSize) / expectedSize > FILE_SIZE_MISMATCH_TOLERANCE) {
150
+ pushNote(notes, {
151
+ code: 'file-mismatch',
152
+ message: `sizeBytes ${obsSize} vs parsed ${expectedSize}`
153
+ });
154
+ return 'demote';
155
+ }
156
+ }
157
+ return 'ok';
158
+ }
159
+ /**
160
+ * Cross-check the winning tier against a reported layer (§4.3): disagreement beyond one layer
161
+ * widens the band to cover both — precision claims stay evidence-backed, tiers never switch silently.
162
+ */
163
+ function applyLayerCrossCheck(result, reportedLayerRaw) {
164
+ if (result.layerIndex === null || result.band === null || ir.layers.length === 0)
165
+ return result;
166
+ const reported = Math.min(Math.max(Math.floor(reportedLayerRaw), 0), ir.layers.length - 1);
167
+ if (Math.abs(result.layerIndex - reported) <= 1)
168
+ return result;
169
+ const entry = ir.layers[reported];
170
+ const notes = [...result.notes];
171
+ pushNote(notes, {
172
+ code: 'cross-check-disagrees',
173
+ message: `mapped layer ${result.layerIndex}, reported ${reported}`
174
+ });
175
+ return {
176
+ ...result,
177
+ band: [Math.min(result.band[0], entry.segStart), Math.max(result.band[1], entry.segEnd)],
178
+ notes
179
+ };
180
+ }
181
+ function map(obs) {
182
+ if (ir.segments.count === 0)
183
+ return { ...UNAVAILABLE, notes: [{ code: 'empty-ir' }] };
184
+ if (obs.v !== PROGRESS_OBSERVATION_VERSION) {
185
+ return { ...UNAVAILABLE, notes: [{ code: 'version-unsupported' }] };
186
+ }
187
+ const notes = [];
188
+ const p = sanitizePosition(obs.position, notes);
189
+ const identity = checkIdentity(obs.file, notes);
190
+ if (identity === 'unavailable') {
191
+ // A marker on the wrong file is worse than no marker (§4.4.1).
192
+ return { ...UNAVAILABLE, notes };
193
+ }
194
+ // `complete` maps to the final segment regardless of position facts (§4.4.3).
195
+ if (obs.state === 'complete') {
196
+ const last = ir.segments.count - 1;
197
+ return {
198
+ segIndex: last,
199
+ basis: p.byte !== undefined ? 'byte' : 'none',
200
+ confidence: 'known',
201
+ band: [last, last],
202
+ layerIndex: layerOfSegment(ir, last),
203
+ stale: false,
204
+ notes
205
+ };
206
+ }
207
+ // Fallback hierarchy (§4.3): highest-precision usable fact wins.
208
+ if (p.byte !== undefined) {
209
+ if (identity === 'demote') {
210
+ // The printer's byte domain is not our parsed stream: map its byte as a fraction of
211
+ // ITS file (tier 5), keeping the basis honest about which fact was used.
212
+ const theirSize = finiteNonNegative(obs.file?.sizeBytes);
213
+ if (theirSize !== undefined && theirSize > 0) {
214
+ const demoted = mapPercentOrdinal(Math.min(1, p.byte / theirSize), notes);
215
+ const withBasis = { ...demoted, basis: 'byte' };
216
+ return p.layer !== undefined ? applyLayerCrossCheck(withBasis, p.layer) : withBasis;
217
+ }
218
+ return { ...UNAVAILABLE, notes };
219
+ }
220
+ const mapped = mapByte(p.byte, 'known', 0, notes);
221
+ return p.layer !== undefined ? applyLayerCrossCheck(mapped, p.layer) : mapped;
222
+ }
223
+ if (p.line !== undefined) {
224
+ // Reserved tier (D3): carried but unmapped in v1 — fall through, visibly.
225
+ pushNote(notes, { code: 'line-unmapped', message: 'no line index in v1; falling through' });
226
+ }
227
+ // Size usable for percent(bytes) promotion — untrusted after an identity demotion.
228
+ const promoSize = identity === 'demote'
229
+ ? undefined // their fraction is of a different byte stream — no promotion
230
+ : (fileSizeBytes ?? finiteNonNegative(obs.file?.sizeBytes) ?? finiteNonNegative(ir.header.source.byteLength));
231
+ /** Segment the percent fact points at on its own (for cross-checking a winning layer tier). */
232
+ function percentImpliedSegment(percent) {
233
+ if (p.percentBasis === 'bytes' && promoSize !== undefined && promoSize > 0) {
234
+ const seg = segmentAtByte(ir.sourceIndex, Math.round(percent * promoSize));
235
+ return seg === -1 ? 0 : seg;
236
+ }
237
+ return clampSeg(ir, Math.round(percent * (ir.segments.count - 1)));
238
+ }
239
+ if (p.layer !== undefined && ir.layers.length > 0) {
240
+ if (p.totalLayers !== undefined &&
241
+ p.totalLayers > 0 &&
242
+ Math.abs(p.totalLayers - ir.layers.length) > LAYER_COUNT_MISMATCH_TOLERANCE) {
243
+ // The reporter counts layers differently than the IR: its index is untrustworthy as an
244
+ // index, but still meaningful as a fraction (§4.3 layer caveats).
245
+ pushNote(notes, {
246
+ code: 'layer-count-mismatch',
247
+ message: `reported total ${p.totalLayers}, IR has ${ir.layers.length}`
248
+ });
249
+ const fraction = Math.min(1, p.layer / p.totalLayers);
250
+ return { ...mapPercentOrdinal(fraction, notes), basis: 'layer' };
251
+ }
252
+ const mapped = mapLayer(p.layer, notes);
253
+ // Winning layer tier is validated against the percent fact when both are present (§4.3).
254
+ return p.percent !== undefined
255
+ ? applyLayerCrossCheck(mapped, ir.segments.layer[percentImpliedSegment(p.percent)])
256
+ : mapped;
257
+ }
258
+ if (p.percent !== undefined) {
259
+ if (p.percentBasis === 'bytes' && promoSize !== undefined && promoSize > 0) {
260
+ // Promotion (D4): arithmetic is exact, the source is still a fraction → approximated + band.
261
+ const halfWidth = Math.ceil(ir.segments.count * PERCENT_BYTES_BAND_FRACTION);
262
+ const mapped = {
263
+ ...mapByte(Math.round(p.percent * promoSize), 'approximated', halfWidth, notes),
264
+ basis: 'percent'
265
+ };
266
+ return p.layer !== undefined ? applyLayerCrossCheck(mapped, p.layer) : mapped;
267
+ }
268
+ return mapPercentOrdinal(p.percent, notes);
269
+ }
270
+ if (notes.length === 0)
271
+ pushNote(notes, { code: 'no-position-facts' });
272
+ return { ...UNAVAILABLE, notes };
273
+ }
274
+ /** Append a note to a finished result (copy-on-write; respects the cap). */
275
+ function withNote(result, code, message) {
276
+ const notes = [...result.notes];
277
+ pushNote(notes, message === undefined ? { code } : { code, message });
278
+ return { ...result, notes };
279
+ }
280
+ return {
281
+ observe(obs) {
282
+ const prev = lastResult;
283
+ let result = map(obs);
284
+ // `cancelled`/`unknown` with no usable facts keep the last mapped position, flagged (§4.4.3).
285
+ const state = obs?.state;
286
+ if ((state === 'cancelled' || state === 'unknown') && result.basis === 'none' && prev.segIndex !== null) {
287
+ result = withNote({ ...prev, stale: false }, state === 'cancelled' ? 'job-cancelled' : 'state-unknown');
288
+ }
289
+ else if (state === 'cancelled') {
290
+ result = withNote(result, 'job-cancelled');
291
+ }
292
+ // Regression (§4.4.2): re-sync always; a jump back beyond tolerance is visible, not dropped.
293
+ if (prev.layerIndex !== null &&
294
+ result.layerIndex !== null &&
295
+ prev.layerIndex - result.layerIndex > REGRESSION_LAYER_TOLERANCE) {
296
+ result = withNote(result, 'position-regressed', `layer ${prev.layerIndex} -> ${result.layerIndex}`);
297
+ }
298
+ lastTimestampMs =
299
+ typeof obs?.timestampMs === 'number' && Number.isFinite(obs.timestampMs) ? obs.timestampMs : null;
300
+ lastResult = result;
301
+ return result;
302
+ },
303
+ tick(nowMs) {
304
+ if (lastTimestampMs === null)
305
+ return lastResult;
306
+ const stale = nowMs - lastTimestampMs > staleAfterMs;
307
+ if (stale !== lastResult.stale)
308
+ lastResult = { ...lastResult, stale };
309
+ return lastResult;
310
+ },
311
+ reset() {
312
+ lastTimestampMs = null;
313
+ lastResult = UNAVAILABLE;
314
+ }
315
+ };
316
+ }
@@ -0,0 +1,13 @@
1
+ import { type SourceIndex } from './ir.js';
2
+ /**
3
+ * Build the source-position index: sort segments by their source byte offset so a
4
+ * consumer (live progress, E5/DD-006) can map `byteOffset -> segmentIndex`.
5
+ */
6
+ export declare function buildSourceIndex(srcByte: Uint32Array, count: number): SourceIndex;
7
+ /**
8
+ * Return the segment index for the largest indexed byte offset `<= byteOffset`,
9
+ * or `-1` if `byteOffset` precedes the first segment. Exact where an offset matches;
10
+ * otherwise resolves to the segment in progress at that offset.
11
+ */
12
+ export declare function segmentAtByte(index: SourceIndex, byteOffset: number): number;
13
+ //# sourceMappingURL=source-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-index.d.ts","sourceRoot":"","sources":["../src/source-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAUjF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAe5E"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Build the source-position index: sort segments by their source byte offset so a
3
+ * consumer (live progress, E5/DD-006) can map `byteOffset -> segmentIndex`.
4
+ */
5
+ export function buildSourceIndex(srcByte, count) {
6
+ const order = Array.from({ length: count }, (_, i) => i);
7
+ order.sort((a, b) => srcByte[a] - srcByte[b]);
8
+ const byteOffsets = new Uint32Array(count);
9
+ const segmentIndices = new Uint32Array(count);
10
+ for (let k = 0; k < count; k++) {
11
+ byteOffsets[k] = srcByte[order[k]];
12
+ segmentIndices[k] = order[k];
13
+ }
14
+ return { byteOffsets, segmentIndices };
15
+ }
16
+ /**
17
+ * Return the segment index for the largest indexed byte offset `<= byteOffset`,
18
+ * or `-1` if `byteOffset` precedes the first segment. Exact where an offset matches;
19
+ * otherwise resolves to the segment in progress at that offset.
20
+ */
21
+ export function segmentAtByte(index, byteOffset) {
22
+ const offsets = index.byteOffsets;
23
+ let lo = 0;
24
+ let hi = offsets.length - 1;
25
+ let ans = -1;
26
+ while (lo <= hi) {
27
+ const mid = (lo + hi) >> 1;
28
+ if (offsets[mid] <= byteOffset) {
29
+ ans = mid;
30
+ lo = mid + 1;
31
+ }
32
+ else {
33
+ hi = mid - 1;
34
+ }
35
+ }
36
+ return ans === -1 ? -1 : index.segmentIndices[ans];
37
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@chestnutlabs/toolpath-core",
3
+ "version": "0.1.0",
4
+ "description": "Neutral ToolpathIR and capability model for the Chestnut Labs G-code Preview toolpath stack.",
5
+ "keywords": [
6
+ "gcode",
7
+ "3d-printing",
8
+ "toolpath",
9
+ "cnc",
10
+ "chestnutlabs",
11
+ "toolpath-ir",
12
+ "intermediate-representation",
13
+ "capability-model",
14
+ "progress"
15
+ ],
16
+ "author": "Chestnut Labs",
17
+ "homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
18
+ "bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
22
+ "directory": "packages/toolpath-core"
23
+ },
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "prepare": "npm run build",
47
+ "build": "tsc -p tsconfig.json",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "vitest run"
50
+ }
51
+ }