@chestnutlabs/gcode-preview-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 +26 -0
- package/dist/controller.d.ts +128 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +270 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/render-still.d.ts +75 -0
- package/dist/render-still.d.ts.map +1 -0
- package/dist/render-still.js +129 -0
- package/dist/testing.d.ts +67 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +147 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @chestnutlabs/gcode-preview-core
|
|
2
|
+
|
|
3
|
+
The **framework-neutral preview controller** for the Chestnut Labs G-code Preview stack
|
|
4
|
+
(design: DD-007). This is the single engine under the Vue, React, and Svelte adapters — parse
|
|
5
|
+
session + renderer + progress mapper glued into one lifecycle with an **immutable-snapshot state
|
|
6
|
+
model** (`getState()` returns a new identity per mutation; `onStateChange` notifies), which every
|
|
7
|
+
framework's reactivity can bridge cheaply.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createPreviewController } from '@chestnutlabs/gcode-preview-core';
|
|
11
|
+
|
|
12
|
+
const preview = createPreviewController();
|
|
13
|
+
preview.bindCanvas(canvas); // resize-observed
|
|
14
|
+
await preview.parse(bytes); // worker parse + progressive build
|
|
15
|
+
preview.controls.setLayerRange(0, 42); // scrub / quality / colors / bed / frame ...
|
|
16
|
+
preview.observeProgress(obs); // DD-006 live progress
|
|
17
|
+
preview.onStateChange(cb); // snapshots: summaries only, never toolpath buffers
|
|
18
|
+
preview.dispose();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Building an adapter for another framework? The **portable behavioral suite** is exported from
|
|
22
|
+
`@chestnutlabs/gcode-preview-core/testing` — `runBehavioralSuite(...)` is the same parity
|
|
23
|
+
contract the Vue/React/Svelte adapters pass in CI, and it is the anti-drift firewall for any new
|
|
24
|
+
integration.
|
|
25
|
+
|
|
26
|
+
Part of [Chestnut Labs G-code Preview](../../README.md) · MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPreviewController — the framework-neutral engine glue (DD-007 §4.6, phase 5,
|
|
3
|
+
* issue #112): GcodeParseSession (worker parse) + ToolpathRenderer (scene) +
|
|
4
|
+
* createProgressMapper (DD-006), extracted from the Vue composable so every framework
|
|
5
|
+
* adapter is a pure reactivity bridge over ONE implementation.
|
|
6
|
+
*
|
|
7
|
+
* State model: immutable snapshots. Every change replaces `getState()`'s return value
|
|
8
|
+
* and notifies subscribers — exactly what React's useSyncExternalStore, Svelte stores,
|
|
9
|
+
* and Vue reactive mirrors all consume without adaptation. Snapshots carry summaries
|
|
10
|
+
* only (numbers/strings): IR/typed arrays NEVER enter the state model.
|
|
11
|
+
*/
|
|
12
|
+
import { GcodeParseSession, type ParseProgress, type ParseResult, type SessionOptions, type WireParseOptions } from '@chestnutlabs/gcode-parser';
|
|
13
|
+
import { ToolpathRenderer, type BuildVolumeDef, type ColorMode, type GLRendererLike, type ProgressPresentationMode, type QualityMode, type RenderTargetCanvas, type RendererEvent, type TubeOptions } from '@chestnutlabs/gcode-renderer-three';
|
|
14
|
+
import { type Confidence, type DialectMetadata, type MachineGeometry, type MappedProgress, type ProgressObservation } from '@chestnutlabs/toolpath-core';
|
|
15
|
+
/** Session/renderer events, re-emitted, plus the controller's own lifecycle events. */
|
|
16
|
+
export type PreviewEvent = RendererEvent | {
|
|
17
|
+
type: 'parse-started';
|
|
18
|
+
bytes: number;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'parse-progress';
|
|
21
|
+
progress: ParseProgress;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'parse-complete';
|
|
24
|
+
segments: number;
|
|
25
|
+
layers: number;
|
|
26
|
+
complete: boolean;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'parse-cancelled';
|
|
29
|
+
} | {
|
|
30
|
+
type: 'parse-error';
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
} | {
|
|
34
|
+
/** File-discovered geometry NOT auto-applied because the consumer configured a
|
|
35
|
+
* volume (DD-005 consumer-wins precedence) — the host decides. */
|
|
36
|
+
type: 'machine-geometry-discovered';
|
|
37
|
+
machine: MachineGeometry;
|
|
38
|
+
};
|
|
39
|
+
export interface PreviewControllerOptions {
|
|
40
|
+
/** Custom worker factory (DD-005 slim/custom entries). Default: the batteries worker. */
|
|
41
|
+
createWorker?: SessionOptions['worker'];
|
|
42
|
+
renderer?: {
|
|
43
|
+
buildVolume?: BuildVolumeDef;
|
|
44
|
+
quality?: QualityMode | 'auto';
|
|
45
|
+
colorMode?: ColorMode;
|
|
46
|
+
tube?: TubeOptions;
|
|
47
|
+
/** Advanced/test injectables — pass-throughs of the renderer's own contract. */
|
|
48
|
+
createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
|
|
49
|
+
scheduleFrame?: (cb: () => void) => void;
|
|
50
|
+
chunksPerTick?: number;
|
|
51
|
+
};
|
|
52
|
+
/** Applied to every parse unless overridden per-call. */
|
|
53
|
+
parseDefaults?: WireParseOptions;
|
|
54
|
+
}
|
|
55
|
+
/** `parse()` never rejects from prop/event-driven flows (DD-007 §6): inspect the outcome. */
|
|
56
|
+
export type ParseOutcome = {
|
|
57
|
+
ok: true;
|
|
58
|
+
result: ParseResult;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
cancelled: true;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
cancelled?: false;
|
|
65
|
+
error: {
|
|
66
|
+
code: string;
|
|
67
|
+
message: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export interface GcodePreviewState {
|
|
71
|
+
parsing: boolean;
|
|
72
|
+
parseProgress: ParseProgress | null;
|
|
73
|
+
/** Summary of the last successful parse — plain numbers/strings, never typed arrays. */
|
|
74
|
+
summary: {
|
|
75
|
+
segments: number;
|
|
76
|
+
layers: number;
|
|
77
|
+
complete: boolean;
|
|
78
|
+
stopReason: string | undefined;
|
|
79
|
+
capabilities: Record<string, Confidence>;
|
|
80
|
+
warnings: number;
|
|
81
|
+
} | null;
|
|
82
|
+
metadata: DialectMetadata | undefined;
|
|
83
|
+
activeQuality: QualityMode | null;
|
|
84
|
+
presentation: ProgressPresentationMode;
|
|
85
|
+
layerCount: number;
|
|
86
|
+
segmentCount: number;
|
|
87
|
+
/** User-presentable decimation/travel disclosure (empty when rendering everything). */
|
|
88
|
+
disclosure: string;
|
|
89
|
+
error: {
|
|
90
|
+
code: string;
|
|
91
|
+
message: string;
|
|
92
|
+
} | null;
|
|
93
|
+
}
|
|
94
|
+
export interface GcodePreviewControls {
|
|
95
|
+
setLayerRange(startLayer: number, endLayer: number): void;
|
|
96
|
+
setScrubPosition(segIndex: number | null): void;
|
|
97
|
+
setKindVisible(kind: 'extrude' | 'travel', visible: boolean): void;
|
|
98
|
+
setColorMode(mode: ColorMode): boolean;
|
|
99
|
+
setQuality(quality: QualityMode | 'auto'): void;
|
|
100
|
+
/** Marks the volume consumer-configured: file-discovered geometry stops auto-applying. */
|
|
101
|
+
setBuildVolume(def: BuildVolumeDef | MachineGeometry): void;
|
|
102
|
+
frame(): void;
|
|
103
|
+
}
|
|
104
|
+
export interface PreviewController {
|
|
105
|
+
/** Bind/rebind the renderer to a canvas (null unbinds). Rebinding disposes safely. */
|
|
106
|
+
bindCanvas(canvas: HTMLCanvasElement | null): void;
|
|
107
|
+
parse(input: Uint8Array | ArrayBuffer | File, opts?: WireParseOptions): Promise<ParseOutcome>;
|
|
108
|
+
cancel(): void;
|
|
109
|
+
/** DD-006: map one observation and drive the overlay. Null before a successful parse. */
|
|
110
|
+
observeProgress(obs: ProgressObservation): MappedProgress | null;
|
|
111
|
+
/** Recompute staleness (DD-006 §4.4.3) — call at ~1 Hz while telemetry may go quiet. */
|
|
112
|
+
tickProgress(nowMs: number): MappedProgress | null;
|
|
113
|
+
clearProgress(): void;
|
|
114
|
+
/** Current immutable state snapshot (identity changes on every state change). */
|
|
115
|
+
getState(): GcodePreviewState;
|
|
116
|
+
/** Subscribe to snapshot replacements. Returns the unsubscriber. */
|
|
117
|
+
onStateChange(cb: (state: GcodePreviewState) => void): () => void;
|
|
118
|
+
controls: GcodePreviewControls;
|
|
119
|
+
/** Escape hatches — the neutral objects themselves (advanced use). */
|
|
120
|
+
raw: {
|
|
121
|
+
session: GcodeParseSession;
|
|
122
|
+
renderer: () => ToolpathRenderer | null;
|
|
123
|
+
};
|
|
124
|
+
onEvent(cb: (e: PreviewEvent) => void): () => void;
|
|
125
|
+
dispose(): void;
|
|
126
|
+
}
|
|
127
|
+
export declare function createPreviewController(options?: PreviewControllerOptions): PreviewController;
|
|
128
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPreviewController — the framework-neutral engine glue (DD-007 §4.6, phase 5,
|
|
3
|
+
* issue #112): GcodeParseSession (worker parse) + ToolpathRenderer (scene) +
|
|
4
|
+
* createProgressMapper (DD-006), extracted from the Vue composable so every framework
|
|
5
|
+
* adapter is a pure reactivity bridge over ONE implementation.
|
|
6
|
+
*
|
|
7
|
+
* State model: immutable snapshots. Every change replaces `getState()`'s return value
|
|
8
|
+
* and notifies subscribers — exactly what React's useSyncExternalStore, Svelte stores,
|
|
9
|
+
* and Vue reactive mirrors all consume without adaptation. Snapshots carry summaries
|
|
10
|
+
* only (numbers/strings): IR/typed arrays NEVER enter the state model.
|
|
11
|
+
*/
|
|
12
|
+
import { GcodeParseSession, CancelledError, ParseSessionError } from '@chestnutlabs/gcode-parser';
|
|
13
|
+
import { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three';
|
|
14
|
+
import { createProgressMapper } from '@chestnutlabs/toolpath-core';
|
|
15
|
+
const INITIAL_STATE = {
|
|
16
|
+
parsing: false,
|
|
17
|
+
parseProgress: null,
|
|
18
|
+
summary: null,
|
|
19
|
+
metadata: undefined,
|
|
20
|
+
activeQuality: null,
|
|
21
|
+
presentation: 'hidden',
|
|
22
|
+
layerCount: 0,
|
|
23
|
+
segmentCount: 0,
|
|
24
|
+
disclosure: '',
|
|
25
|
+
error: null
|
|
26
|
+
};
|
|
27
|
+
function toBytes(input) {
|
|
28
|
+
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
29
|
+
}
|
|
30
|
+
export function createPreviewController(options = {}) {
|
|
31
|
+
let snapshot = { ...INITIAL_STATE };
|
|
32
|
+
const stateListeners = new Set();
|
|
33
|
+
const mutate = (patch) => {
|
|
34
|
+
snapshot = { ...snapshot, ...patch };
|
|
35
|
+
for (const cb of stateListeners)
|
|
36
|
+
cb(snapshot);
|
|
37
|
+
};
|
|
38
|
+
const session = new GcodeParseSession(options.createWorker === undefined ? {} : { worker: options.createWorker });
|
|
39
|
+
// Non-reactive engine state: plain lets — snapshots carry summaries only.
|
|
40
|
+
let renderer = null;
|
|
41
|
+
let unbindRendererEvents = null;
|
|
42
|
+
let lastIR = null;
|
|
43
|
+
let lastMachine;
|
|
44
|
+
let mapper = null;
|
|
45
|
+
let consumerVolumeSet = options.renderer?.buildVolume !== undefined;
|
|
46
|
+
let activeParse = null;
|
|
47
|
+
let resizeObserver = null;
|
|
48
|
+
let windowResize = null;
|
|
49
|
+
let disposed = false;
|
|
50
|
+
const listeners = new Set();
|
|
51
|
+
const emit = (e) => {
|
|
52
|
+
for (const cb of listeners)
|
|
53
|
+
cb(e);
|
|
54
|
+
};
|
|
55
|
+
function onRendererEvent(e) {
|
|
56
|
+
if (e.type === 'buildComplete') {
|
|
57
|
+
mutate({
|
|
58
|
+
activeQuality: e.quality,
|
|
59
|
+
disclosure: e.decimationApplied > 1
|
|
60
|
+
? `Showing every ${e.decimationApplied}th extrusion segment (layer boundaries kept); ` +
|
|
61
|
+
`travel hidden. ${e.segments.toLocaleString()} segments drawn.`
|
|
62
|
+
: ''
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else if (e.type === 'qualityFallback') {
|
|
66
|
+
mutate({ activeQuality: e.to });
|
|
67
|
+
}
|
|
68
|
+
else if (e.type === 'progress-presentation-changed') {
|
|
69
|
+
mutate({ presentation: e.mode });
|
|
70
|
+
}
|
|
71
|
+
else if (e.type === 'error') {
|
|
72
|
+
mutate({ error: { code: e.code, message: e.message } });
|
|
73
|
+
}
|
|
74
|
+
emit(e);
|
|
75
|
+
}
|
|
76
|
+
function disposeRenderer() {
|
|
77
|
+
unbindRendererEvents?.();
|
|
78
|
+
unbindRendererEvents = null;
|
|
79
|
+
resizeObserver?.disconnect();
|
|
80
|
+
resizeObserver = null;
|
|
81
|
+
if (windowResize !== null && typeof window !== 'undefined') {
|
|
82
|
+
window.removeEventListener('resize', windowResize);
|
|
83
|
+
windowResize = null;
|
|
84
|
+
}
|
|
85
|
+
renderer?.dispose();
|
|
86
|
+
renderer = null;
|
|
87
|
+
}
|
|
88
|
+
function fitToCanvas(canvas) {
|
|
89
|
+
const w = canvas.clientWidth || canvas.width || 1;
|
|
90
|
+
const h = canvas.clientHeight || canvas.height || 1;
|
|
91
|
+
renderer?.resize(w, h);
|
|
92
|
+
}
|
|
93
|
+
function bindCanvas(canvas) {
|
|
94
|
+
if (disposed)
|
|
95
|
+
return;
|
|
96
|
+
if (canvas === null) {
|
|
97
|
+
disposeRenderer();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
disposeRenderer();
|
|
101
|
+
const r = options.renderer ?? {};
|
|
102
|
+
renderer = new ToolpathRenderer({
|
|
103
|
+
canvas,
|
|
104
|
+
buildVolume: r.buildVolume,
|
|
105
|
+
quality: r.quality ?? 'auto',
|
|
106
|
+
colorMode: r.colorMode,
|
|
107
|
+
tube: r.tube,
|
|
108
|
+
createRenderer: r.createRenderer,
|
|
109
|
+
scheduleFrame: r.scheduleFrame,
|
|
110
|
+
chunksPerTick: r.chunksPerTick
|
|
111
|
+
});
|
|
112
|
+
unbindRendererEvents = renderer.onEvent(onRendererEvent);
|
|
113
|
+
// Sizing (§4.2): explicit initial fit; ResizeObserver where available; window-resize
|
|
114
|
+
// fallback because observers may never fire in embedded panes.
|
|
115
|
+
fitToCanvas(canvas);
|
|
116
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
117
|
+
const ro = new ResizeObserver(() => fitToCanvas(canvas));
|
|
118
|
+
ro.observe(canvas);
|
|
119
|
+
resizeObserver = ro;
|
|
120
|
+
}
|
|
121
|
+
if (typeof window !== 'undefined') {
|
|
122
|
+
windowResize = () => fitToCanvas(canvas);
|
|
123
|
+
window.addEventListener('resize', windowResize);
|
|
124
|
+
}
|
|
125
|
+
if (lastIR !== null) {
|
|
126
|
+
renderer.setIR(lastIR);
|
|
127
|
+
mutate({ layerCount: renderer.layerCount, segmentCount: renderer.segmentCount });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const offProgress = session.onProgress((p) => {
|
|
131
|
+
mutate({ parseProgress: p });
|
|
132
|
+
emit({ type: 'parse-progress', progress: p });
|
|
133
|
+
});
|
|
134
|
+
const offPartial = session.onPartial((slice) => {
|
|
135
|
+
renderer?.appendPartial(slice);
|
|
136
|
+
});
|
|
137
|
+
async function parse(input, opts) {
|
|
138
|
+
if (disposed)
|
|
139
|
+
return { ok: false, error: { code: 'E_DISPOSED', message: 'preview disposed' } };
|
|
140
|
+
if (snapshot.parsing) {
|
|
141
|
+
// §5: parse-during-parse cancels then restarts (session semantics preserved).
|
|
142
|
+
session.cancel();
|
|
143
|
+
await activeParse?.catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
mutate({ parsing: true, parseProgress: null, error: null });
|
|
146
|
+
const bytes = typeof File !== 'undefined' && input instanceof File
|
|
147
|
+
? input.size
|
|
148
|
+
: input.byteLength;
|
|
149
|
+
emit({ type: 'parse-started', bytes });
|
|
150
|
+
const wire = { ...options.parseDefaults, ...opts };
|
|
151
|
+
const run = session.parse(input instanceof ArrayBuffer ? toBytes(input) : input, wire);
|
|
152
|
+
activeParse = run;
|
|
153
|
+
try {
|
|
154
|
+
const result = await run;
|
|
155
|
+
lastIR = result.ir;
|
|
156
|
+
lastMachine = result.metadata?.machine;
|
|
157
|
+
mapper = createProgressMapper(result.ir, { fileSizeBytes: bytes });
|
|
158
|
+
const counts = renderer !== null
|
|
159
|
+
? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount }
|
|
160
|
+
: { layerCount: result.ir.layers.length, segmentCount: result.ir.segments.count };
|
|
161
|
+
if (renderer !== null)
|
|
162
|
+
renderer.setIR(result.ir);
|
|
163
|
+
mutate({
|
|
164
|
+
summary: {
|
|
165
|
+
segments: result.ir.segments.count,
|
|
166
|
+
layers: result.ir.layers.length,
|
|
167
|
+
complete: result.ir.header.complete,
|
|
168
|
+
stopReason: result.stats.stopReason?.code,
|
|
169
|
+
capabilities: { ...result.ir.header.capabilities },
|
|
170
|
+
warnings: result.ir.header.warnings.length
|
|
171
|
+
},
|
|
172
|
+
metadata: result.metadata,
|
|
173
|
+
presentation: 'hidden',
|
|
174
|
+
...(renderer !== null ? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount } : counts)
|
|
175
|
+
});
|
|
176
|
+
// DD-005 consumer-wins precedence: a consumer-configured volume blocks auto-apply.
|
|
177
|
+
if (lastMachine !== undefined) {
|
|
178
|
+
if (consumerVolumeSet)
|
|
179
|
+
emit({ type: 'machine-geometry-discovered', machine: lastMachine });
|
|
180
|
+
else
|
|
181
|
+
renderer?.setBuildVolume(lastMachine);
|
|
182
|
+
}
|
|
183
|
+
emit({
|
|
184
|
+
type: 'parse-complete',
|
|
185
|
+
segments: result.ir.segments.count,
|
|
186
|
+
layers: result.ir.layers.length,
|
|
187
|
+
complete: result.ir.header.complete
|
|
188
|
+
});
|
|
189
|
+
return { ok: true, result };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err instanceof CancelledError) {
|
|
193
|
+
emit({ type: 'parse-cancelled' });
|
|
194
|
+
return { ok: false, cancelled: true };
|
|
195
|
+
}
|
|
196
|
+
const code = err instanceof ParseSessionError ? err.code : 'E_UNKNOWN';
|
|
197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
198
|
+
mutate({ error: { code, message } });
|
|
199
|
+
emit({ type: 'parse-error', code, message });
|
|
200
|
+
return { ok: false, error: { code, message } };
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
mutate({ parsing: false, parseProgress: null });
|
|
204
|
+
if (activeParse === run)
|
|
205
|
+
activeParse = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const controls = {
|
|
209
|
+
setLayerRange: (a, b) => renderer?.setLayerRange(a, b),
|
|
210
|
+
setScrubPosition: (s) => renderer?.setScrubPosition(s),
|
|
211
|
+
setKindVisible: (k, v) => renderer?.setKindVisible(k, v),
|
|
212
|
+
setColorMode: (m) => renderer?.setColorMode(m) ?? false,
|
|
213
|
+
setQuality: (q) => renderer?.setQuality(q),
|
|
214
|
+
setBuildVolume: (def) => {
|
|
215
|
+
consumerVolumeSet = true;
|
|
216
|
+
renderer?.setBuildVolume(def);
|
|
217
|
+
},
|
|
218
|
+
frame: () => renderer?.frame()
|
|
219
|
+
};
|
|
220
|
+
function dispose() {
|
|
221
|
+
if (disposed)
|
|
222
|
+
return;
|
|
223
|
+
disposed = true;
|
|
224
|
+
session.cancel();
|
|
225
|
+
offProgress();
|
|
226
|
+
offPartial();
|
|
227
|
+
session.dispose();
|
|
228
|
+
disposeRenderer();
|
|
229
|
+
lastIR = null;
|
|
230
|
+
mapper = null;
|
|
231
|
+
listeners.clear();
|
|
232
|
+
stateListeners.clear();
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
bindCanvas,
|
|
236
|
+
parse,
|
|
237
|
+
cancel: () => session.cancel(),
|
|
238
|
+
observeProgress: (obs) => {
|
|
239
|
+
if (mapper === null)
|
|
240
|
+
return null;
|
|
241
|
+
const mapped = mapper.observe(obs);
|
|
242
|
+
renderer?.setProgress(mapped);
|
|
243
|
+
return mapped;
|
|
244
|
+
},
|
|
245
|
+
tickProgress: (nowMs) => {
|
|
246
|
+
if (mapper === null)
|
|
247
|
+
return null;
|
|
248
|
+
const mapped = mapper.tick(nowMs);
|
|
249
|
+
renderer?.setProgress(mapped);
|
|
250
|
+
return mapped;
|
|
251
|
+
},
|
|
252
|
+
clearProgress: () => {
|
|
253
|
+
mapper?.reset();
|
|
254
|
+
renderer?.setProgress(null);
|
|
255
|
+
mutate({ presentation: 'hidden' });
|
|
256
|
+
},
|
|
257
|
+
getState: () => snapshot,
|
|
258
|
+
onStateChange: (cb) => {
|
|
259
|
+
stateListeners.add(cb);
|
|
260
|
+
return () => stateListeners.delete(cb);
|
|
261
|
+
},
|
|
262
|
+
controls,
|
|
263
|
+
raw: { session, renderer: () => renderer },
|
|
264
|
+
onEvent: (cb) => {
|
|
265
|
+
listeners.add(cb);
|
|
266
|
+
return () => listeners.delete(cb);
|
|
267
|
+
},
|
|
268
|
+
dispose
|
|
269
|
+
};
|
|
270
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-preview-core — the framework-neutral preview controller and the
|
|
3
|
+
* SHARED TypeScript contracts every framework adapter re-exports (DD-007 §4.6):
|
|
4
|
+
* adapters are reactivity bridges over this one implementation, never parallel viewers.
|
|
5
|
+
* The portable behavioral suite ships via the `./testing` subpath.
|
|
6
|
+
*/
|
|
7
|
+
export * from './controller.js';
|
|
8
|
+
export { renderStill, type RenderStillOptions, type RenderStillResult, type StillCameraPose } from './render-still.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-preview-core — the framework-neutral preview controller and the
|
|
3
|
+
* SHARED TypeScript contracts every framework adapter re-exports (DD-007 §4.6):
|
|
4
|
+
* adapters are reactivity bridges over this one implementation, never parallel viewers.
|
|
5
|
+
* The portable behavioral suite ships via the `./testing` subpath.
|
|
6
|
+
*/
|
|
7
|
+
export * from './controller.js';
|
|
8
|
+
export { renderStill } from './render-still.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderStill — headless, non-interactive still image of a toolpath (DD-008 §4.8,
|
|
3
|
+
* E7 phase 5, issue #132; consumer: AnyBridge #791 ThumbnailWorker).
|
|
4
|
+
*
|
|
5
|
+
* A bounded adapter over the SAME engine the interactive controller uses: parse
|
|
6
|
+
* (worker, for bytes) or accept a pre-parsed `ToolpathIR`, build the geometry to
|
|
7
|
+
* completion, frame the camera deterministically (or apply an explicit pose),
|
|
8
|
+
* render exactly once, and hand back the canvas + build stats. The caller
|
|
9
|
+
* extracts pixels in its own environment (`OffscreenCanvas.convertToBlob`,
|
|
10
|
+
* `canvas.toDataURL`, or a WebGL `readPixels`) — this module never touches the
|
|
11
|
+
* filesystem, a DOM, or a worker it wasn't given.
|
|
12
|
+
*
|
|
13
|
+
* Supported environments (see docs/reference/still-render.md): any Chromium-class
|
|
14
|
+
* WebGL2 context — an Electron hidden window, a Worker `OffscreenCanvas`, or
|
|
15
|
+
* headless Chromium. Pure-Node GPU-less rendering is out of scope (deferred
|
|
16
|
+
* E8-class roadmap item).
|
|
17
|
+
*
|
|
18
|
+
* Determinism: same environment ⇒ identical output. Cross-GPU/driver pixel
|
|
19
|
+
* identity is NOT promised (antialiasing varies); cache by job identity.
|
|
20
|
+
*/
|
|
21
|
+
import { type SessionOptions, type WireParseOptions } from '@chestnutlabs/gcode-parser';
|
|
22
|
+
import { type BuildVolumeDef, type ColorMode, type GLRendererLike, type QualityMode, type RenderTargetCanvas, type TubeOptions } from '@chestnutlabs/gcode-renderer-three';
|
|
23
|
+
import { type MachineGeometry, type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
24
|
+
/** Explicit camera pose (scene coordinates), overriding the deterministic frame(). */
|
|
25
|
+
export interface StillCameraPose {
|
|
26
|
+
position: [number, number, number];
|
|
27
|
+
target: [number, number, number];
|
|
28
|
+
/** Vertical field of view in degrees (default: the renderer's 50°). */
|
|
29
|
+
fov?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface RenderStillOptions {
|
|
32
|
+
/** Target surface: an OffscreenCanvas (worker/headless) or a DOM canvas. Required. */
|
|
33
|
+
canvas: RenderTargetCanvas;
|
|
34
|
+
/** Output size. Defaults to the canvas's current width/height. */
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
/** Quality tier (default 'auto': tubes ≤ 1 M segments, else lines). */
|
|
38
|
+
quality?: QualityMode | 'auto';
|
|
39
|
+
colorMode?: ColorMode;
|
|
40
|
+
tube?: TubeOptions;
|
|
41
|
+
/** Bed geometry — a renderer volume or discovered MachineGeometry. */
|
|
42
|
+
buildVolume?: BuildVolumeDef | MachineGeometry;
|
|
43
|
+
/** Inclusive layer clip [start, end]. Omitted → whole model. */
|
|
44
|
+
layerRange?: [number, number];
|
|
45
|
+
/** Draw only up to this segment (progress-style clip). null/omitted → whole model. */
|
|
46
|
+
scrub?: number | null;
|
|
47
|
+
/** Include travel moves (default false). */
|
|
48
|
+
showTravel?: boolean;
|
|
49
|
+
/** Camera: an explicit pose, or omitted for deterministic bounds framing. */
|
|
50
|
+
camera?: StillCameraPose;
|
|
51
|
+
/** Worker factory for the bytes path (batteries default when omitted). */
|
|
52
|
+
createWorker?: SessionOptions['worker'];
|
|
53
|
+
/** Wire parse options for the bytes path (dialects/containers/plate/limits). */
|
|
54
|
+
parseOptions?: WireParseOptions;
|
|
55
|
+
/** GL backend injection (tests / exotic hosts). Default: WebGLRenderer. */
|
|
56
|
+
createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
|
|
57
|
+
}
|
|
58
|
+
export interface RenderStillResult {
|
|
59
|
+
/** The same canvas passed in, now holding the rendered frame. */
|
|
60
|
+
canvas: RenderTargetCanvas;
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
layerCount: number;
|
|
64
|
+
segmentCount: number;
|
|
65
|
+
/** The quality tier actually rendered (resolves 'auto'). */
|
|
66
|
+
quality: QualityMode;
|
|
67
|
+
/** True when the input was G-code bytes (a worker parse ran). */
|
|
68
|
+
parsed: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Render one still image. Resolves after the geometry is fully built and a single
|
|
72
|
+
* frame has been drawn to `options.canvas`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderStill(source: Uint8Array | ArrayBuffer | ToolpathIR, options: RenderStillOptions): Promise<RenderStillResult>;
|
|
75
|
+
//# sourceMappingURL=render-still.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-still.d.ts","sourceRoot":"","sources":["../src/render-still.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAqB,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC3G,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EACjB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEpF,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,MAAM,EAAE,kBAAkB,CAAC;IAC3B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC/B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,cAAc,GAAG,eAAe,CAAC;IAC/C,gEAAgE;IAChE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACxC,gFAAgF;IAChF,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,2EAA2E;IAC3E,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAAC;CACjE;AAED,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,WAAW,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;CACjB;AAkBD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,UAAU,EAC7C,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAsF5B"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderStill — headless, non-interactive still image of a toolpath (DD-008 §4.8,
|
|
3
|
+
* E7 phase 5, issue #132; consumer: AnyBridge #791 ThumbnailWorker).
|
|
4
|
+
*
|
|
5
|
+
* A bounded adapter over the SAME engine the interactive controller uses: parse
|
|
6
|
+
* (worker, for bytes) or accept a pre-parsed `ToolpathIR`, build the geometry to
|
|
7
|
+
* completion, frame the camera deterministically (or apply an explicit pose),
|
|
8
|
+
* render exactly once, and hand back the canvas + build stats. The caller
|
|
9
|
+
* extracts pixels in its own environment (`OffscreenCanvas.convertToBlob`,
|
|
10
|
+
* `canvas.toDataURL`, or a WebGL `readPixels`) — this module never touches the
|
|
11
|
+
* filesystem, a DOM, or a worker it wasn't given.
|
|
12
|
+
*
|
|
13
|
+
* Supported environments (see docs/reference/still-render.md): any Chromium-class
|
|
14
|
+
* WebGL2 context — an Electron hidden window, a Worker `OffscreenCanvas`, or
|
|
15
|
+
* headless Chromium. Pure-Node GPU-less rendering is out of scope (deferred
|
|
16
|
+
* E8-class roadmap item).
|
|
17
|
+
*
|
|
18
|
+
* Determinism: same environment ⇒ identical output. Cross-GPU/driver pixel
|
|
19
|
+
* identity is NOT promised (antialiasing varies); cache by job identity.
|
|
20
|
+
*/
|
|
21
|
+
import { GcodeParseSession } from '@chestnutlabs/gcode-parser';
|
|
22
|
+
import { ToolpathRenderer, machineToVolume } from '@chestnutlabs/gcode-renderer-three';
|
|
23
|
+
function isToolpathIR(v) {
|
|
24
|
+
return (typeof v === 'object' &&
|
|
25
|
+
v !== null &&
|
|
26
|
+
'segments' in v &&
|
|
27
|
+
'layers' in v &&
|
|
28
|
+
'header' in v &&
|
|
29
|
+
!(v instanceof Uint8Array) &&
|
|
30
|
+
!(v instanceof ArrayBuffer));
|
|
31
|
+
}
|
|
32
|
+
function toVolume(def) {
|
|
33
|
+
return 'bed' in def ? machineToVolume(def) : def;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Render one still image. Resolves after the geometry is fully built and a single
|
|
37
|
+
* frame has been drawn to `options.canvas`.
|
|
38
|
+
*/
|
|
39
|
+
export async function renderStill(source, options) {
|
|
40
|
+
const { canvas } = options;
|
|
41
|
+
const width = options.width ?? canvas.width;
|
|
42
|
+
const height = options.height ?? canvas.height;
|
|
43
|
+
let ir;
|
|
44
|
+
let parsed = false;
|
|
45
|
+
if (isToolpathIR(source)) {
|
|
46
|
+
ir = source;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
|
|
50
|
+
const session = new GcodeParseSession(options.createWorker === undefined ? {} : { worker: options.createWorker });
|
|
51
|
+
try {
|
|
52
|
+
const result = await session.parse(bytes, options.parseOptions ?? {});
|
|
53
|
+
ir = result.ir;
|
|
54
|
+
parsed = true;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
session.dispose();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Build to completion off the event loop (no rAF): microtask-scheduled ticks.
|
|
61
|
+
const renderer = new ToolpathRenderer({
|
|
62
|
+
canvas,
|
|
63
|
+
quality: options.quality ?? 'auto',
|
|
64
|
+
...(options.colorMode ? { colorMode: options.colorMode } : {}),
|
|
65
|
+
...(options.tube ? { tube: options.tube } : {}),
|
|
66
|
+
...(options.buildVolume ? { buildVolume: toVolume(options.buildVolume) } : {}),
|
|
67
|
+
preserveDrawingBuffer: true,
|
|
68
|
+
scheduleFrame: (cb) => queueMicrotask(cb),
|
|
69
|
+
...(options.createRenderer ? { createRenderer: options.createRenderer } : {})
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
renderer.resize(width, height);
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
let settled = false;
|
|
75
|
+
const finish = () => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
off();
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
const off = renderer.onEvent((e) => {
|
|
83
|
+
if (e.type === 'buildComplete')
|
|
84
|
+
finish();
|
|
85
|
+
});
|
|
86
|
+
renderer.setIR(ir);
|
|
87
|
+
// An empty IR builds no chunks and may not emit buildComplete — guard on
|
|
88
|
+
// the IR's own segment count (deterministic, not renderer state).
|
|
89
|
+
if (ir.segments.count === 0)
|
|
90
|
+
queueMicrotask(finish);
|
|
91
|
+
});
|
|
92
|
+
if (options.showTravel === true)
|
|
93
|
+
renderer.setKindVisible('travel', true);
|
|
94
|
+
if (options.colorMode)
|
|
95
|
+
renderer.setColorMode(options.colorMode);
|
|
96
|
+
if (options.layerRange)
|
|
97
|
+
renderer.setLayerRange(options.layerRange[0], options.layerRange[1]);
|
|
98
|
+
if (options.scrub !== undefined)
|
|
99
|
+
renderer.setScrubPosition(options.scrub);
|
|
100
|
+
const pose = options.camera;
|
|
101
|
+
if (pose) {
|
|
102
|
+
if (pose.fov !== undefined) {
|
|
103
|
+
renderer.camera.fov = pose.fov;
|
|
104
|
+
renderer.camera.updateProjectionMatrix();
|
|
105
|
+
}
|
|
106
|
+
renderer.camera.position.set(pose.position[0], pose.position[1], pose.position[2]);
|
|
107
|
+
renderer.camera.lookAt(pose.target[0], pose.target[1], pose.target[2]);
|
|
108
|
+
renderer.render();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
renderer.frame(); // deterministic bounds framing + render
|
|
112
|
+
}
|
|
113
|
+
// frame()/setColorMode already render; one explicit render guarantees the
|
|
114
|
+
// final state is on the (preserved) drawing buffer for read-back.
|
|
115
|
+
renderer.render();
|
|
116
|
+
return {
|
|
117
|
+
canvas,
|
|
118
|
+
width,
|
|
119
|
+
height,
|
|
120
|
+
layerCount: renderer.layerCount,
|
|
121
|
+
segmentCount: renderer.segmentCount,
|
|
122
|
+
quality: renderer.activeQuality,
|
|
123
|
+
parsed
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
renderer.dispose();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-portable behavioral suite (DD-007 §4.6, phase 5): the SAME parse → state →
|
|
3
|
+
* controls → progress → dispose contract, runnable through every adapter — the parity
|
|
4
|
+
* lock that keeps Vue/React/Svelte from drifting into different feature sets.
|
|
5
|
+
*
|
|
6
|
+
* Adapters run it by implementing {@link AdapterHarness} over their public surface and
|
|
7
|
+
* calling {@link runBehavioralSuite} with their test framework's describe/it/expect
|
|
8
|
+
* (vitest-compatible shapes). Exported via `@chestnutlabs/gcode-preview-core/testing`.
|
|
9
|
+
*/
|
|
10
|
+
import { type MachineGeometry, type MappedProgress, type ProgressObservation, type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
11
|
+
import type { GLRendererLike } from '@chestnutlabs/gcode-renderer-three';
|
|
12
|
+
import type { GcodePreviewControls, GcodePreviewState, PreviewEvent } from './controller.js';
|
|
13
|
+
/** 2 layers × 6 extrude segments; srcByte = i*10; source byteLength 1000. */
|
|
14
|
+
export declare function makeTestIR(): ToolpathIR;
|
|
15
|
+
export declare const TEST_MACHINE: MachineGeometry;
|
|
16
|
+
/** Protocol-v1-shaped stub worker: parse → done, cancel → cancelled. Counts lifecycles. */
|
|
17
|
+
export declare class SuiteStubWorker {
|
|
18
|
+
private readonly machine?;
|
|
19
|
+
static created: number;
|
|
20
|
+
static terminated: number;
|
|
21
|
+
onmessage: ((ev: {
|
|
22
|
+
data: unknown;
|
|
23
|
+
}) => void) | null;
|
|
24
|
+
onerror: ((err: unknown) => void) | null;
|
|
25
|
+
constructor(machine?: MachineGeometry | undefined);
|
|
26
|
+
postMessage(msg: {
|
|
27
|
+
type: string;
|
|
28
|
+
id: number;
|
|
29
|
+
}): void;
|
|
30
|
+
terminate(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function makeSuiteStubGL(canvas: HTMLCanvasElement): GLRendererLike;
|
|
33
|
+
/** What every adapter must expose to the suite — its PUBLIC surface, uniformly shaped. */
|
|
34
|
+
export interface AdapterHarness {
|
|
35
|
+
/** Construct one preview bound to a fresh canvas, wired to a SuiteStubWorker + stub GL. */
|
|
36
|
+
create(opts?: {
|
|
37
|
+
machine?: MachineGeometry;
|
|
38
|
+
consumerVolume?: boolean;
|
|
39
|
+
}): Promise<AdapterInstance> | AdapterInstance;
|
|
40
|
+
}
|
|
41
|
+
export interface AdapterInstance {
|
|
42
|
+
parse(bytes: Uint8Array): Promise<unknown>;
|
|
43
|
+
getState(): GcodePreviewState;
|
|
44
|
+
controls: GcodePreviewControls;
|
|
45
|
+
observeProgress(obs: ProgressObservation): MappedProgress | null;
|
|
46
|
+
clearProgress(): void;
|
|
47
|
+
onEvent(cb: (e: PreviewEvent) => void): () => void;
|
|
48
|
+
/** Chunk draw-range probe (renderer-level effect check). */
|
|
49
|
+
firstChunkDrawCount(): number;
|
|
50
|
+
dispose(): Promise<void> | void;
|
|
51
|
+
/** Let microtask round-trips + framework effects settle. */
|
|
52
|
+
settle(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
interface TestApi {
|
|
55
|
+
describe: (name: string, fn: () => void) => void;
|
|
56
|
+
it: (name: string, fn: () => Promise<void> | void) => void;
|
|
57
|
+
expect: (actual: unknown) => Record<string, (...args: never[]) => unknown> & {
|
|
58
|
+
toBe(v: unknown): void;
|
|
59
|
+
toBeNull(): void;
|
|
60
|
+
toMatchObject(v: object): void;
|
|
61
|
+
toBeGreaterThan(v: number): void;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The parity contract every adapter must pass (DD-007 §4.6 amendment). */
|
|
65
|
+
export declare function runBehavioralSuite(name: string, api: TestApi, harness: AdapterHarness): void;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EAChB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE7F,6EAA6E;AAC7E,wBAAgB,UAAU,IAAI,UAAU,CAyBvC;AAED,eAAO,MAAM,YAAY,EAAE,eAM1B,CAAC;AAEF,2FAA2F;AAC3F,qBAAa,eAAe;IAKd,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAJrC,MAAM,CAAC,OAAO,SAAK;IACnB,MAAM,CAAC,UAAU,SAAK;IACtB,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IAC3D,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;gBACnB,OAAO,CAAC,EAAE,eAAe,YAAA;IAGtD,WAAW,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAkBpD,SAAS,IAAI,IAAI;CAGlB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAEzE;AAED,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,eAAe,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,eAAe,CAAC;CACpH;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,QAAQ,IAAI,iBAAiB,CAAC;IAC9B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,eAAe,CAAC,GAAG,EAAE,mBAAmB,GAAG,cAAc,GAAG,IAAI,CAAC;IACjE,aAAa,IAAI,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnD,4DAA4D;IAC5D,mBAAmB,IAAI,MAAM,CAAC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,4DAA4D;IAC5D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,UAAU,OAAO;IACf,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACjD,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3D,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAAG;QAC3E,IAAI,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QACvB,QAAQ,IAAI,IAAI,CAAC;QACjB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAClC,CAAC;CACH;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAqE5F"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-portable behavioral suite (DD-007 §4.6, phase 5): the SAME parse → state →
|
|
3
|
+
* controls → progress → dispose contract, runnable through every adapter — the parity
|
|
4
|
+
* lock that keeps Vue/React/Svelte from drifting into different feature sets.
|
|
5
|
+
*
|
|
6
|
+
* Adapters run it by implementing {@link AdapterHarness} over their public surface and
|
|
7
|
+
* calling {@link runBehavioralSuite} with their test framework's describe/it/expect
|
|
8
|
+
* (vitest-compatible shapes). Exported via `@chestnutlabs/gcode-preview-core/testing`.
|
|
9
|
+
*/
|
|
10
|
+
import { MoveKind, ToolpathIRBuilder } from '@chestnutlabs/toolpath-core';
|
|
11
|
+
/** 2 layers × 6 extrude segments; srcByte = i*10; source byteLength 1000. */
|
|
12
|
+
export function makeTestIR() {
|
|
13
|
+
const b = new ToolpathIRBuilder({
|
|
14
|
+
parserVersion: 'behavioral-suite',
|
|
15
|
+
units: 'mm',
|
|
16
|
+
unitsSource: 'known',
|
|
17
|
+
source: { byteLength: 1_000 }
|
|
18
|
+
});
|
|
19
|
+
let src = 0;
|
|
20
|
+
for (let l = 0; l < 2; l++) {
|
|
21
|
+
for (let s = 0; s < 6; s++) {
|
|
22
|
+
b.addSegment({
|
|
23
|
+
x0: 100 + s,
|
|
24
|
+
y0: 100,
|
|
25
|
+
z0: 0.2 * (l + 1),
|
|
26
|
+
x1: 101 + s,
|
|
27
|
+
y1: 100,
|
|
28
|
+
z1: 0.2 * (l + 1),
|
|
29
|
+
e: 1,
|
|
30
|
+
kind: MoveKind.Extrude,
|
|
31
|
+
layer: l,
|
|
32
|
+
srcByte: src++ * 10
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return b.finalize();
|
|
37
|
+
}
|
|
38
|
+
export const TEST_MACHINE = {
|
|
39
|
+
bed: { kind: 'rect', min: { x: 0, y: 0 }, max: { x: 256, y: 256 } },
|
|
40
|
+
heightMm: 256,
|
|
41
|
+
origin: { x: 0, y: 0 },
|
|
42
|
+
confidence: 'known',
|
|
43
|
+
source: { adapterId: 'behavioral-suite', evidence: 'stub' }
|
|
44
|
+
};
|
|
45
|
+
/** Protocol-v1-shaped stub worker: parse → done, cancel → cancelled. Counts lifecycles. */
|
|
46
|
+
export class SuiteStubWorker {
|
|
47
|
+
machine;
|
|
48
|
+
static created = 0;
|
|
49
|
+
static terminated = 0;
|
|
50
|
+
onmessage = null;
|
|
51
|
+
onerror = null;
|
|
52
|
+
constructor(machine) {
|
|
53
|
+
this.machine = machine;
|
|
54
|
+
SuiteStubWorker.created++;
|
|
55
|
+
}
|
|
56
|
+
postMessage(msg) {
|
|
57
|
+
if (msg.type === 'parse') {
|
|
58
|
+
queueMicrotask(() => {
|
|
59
|
+
this.onmessage?.({
|
|
60
|
+
data: {
|
|
61
|
+
v: 1,
|
|
62
|
+
type: 'done',
|
|
63
|
+
id: msg.id,
|
|
64
|
+
ir: makeTestIR(),
|
|
65
|
+
stats: { bytes: 1_000, wallMs: 1 },
|
|
66
|
+
metadata: this.machine === undefined ? undefined : { machine: this.machine }
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (msg.type === 'cancel') {
|
|
72
|
+
queueMicrotask(() => this.onmessage?.({ data: { v: 1, type: 'cancelled', id: msg.id } }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
terminate() {
|
|
76
|
+
SuiteStubWorker.terminated++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function makeSuiteStubGL(canvas) {
|
|
80
|
+
return { render: () => undefined, setSize: () => undefined, dispose: () => undefined, domElement: canvas };
|
|
81
|
+
}
|
|
82
|
+
/** The parity contract every adapter must pass (DD-007 §4.6 amendment). */
|
|
83
|
+
export function runBehavioralSuite(name, api, harness) {
|
|
84
|
+
const { describe, it, expect } = api;
|
|
85
|
+
describe(`behavioral suite — ${name}`, () => {
|
|
86
|
+
it('parses and reports the standard state summary', async () => {
|
|
87
|
+
const a = await harness.create();
|
|
88
|
+
await a.parse(new Uint8Array(1_000));
|
|
89
|
+
await a.settle();
|
|
90
|
+
expect(a.getState().summary).toMatchObject({ segments: 12, layers: 2, complete: true });
|
|
91
|
+
expect(a.getState().segmentCount).toBe(12);
|
|
92
|
+
await a.dispose();
|
|
93
|
+
});
|
|
94
|
+
it('controls reach the renderer: scrub cuts and restores draw ranges', async () => {
|
|
95
|
+
const a = await harness.create();
|
|
96
|
+
await a.parse(new Uint8Array(1_000));
|
|
97
|
+
await a.settle();
|
|
98
|
+
expect(a.firstChunkDrawCount()).toBe(24);
|
|
99
|
+
a.controls.setScrubPosition(5);
|
|
100
|
+
await a.settle();
|
|
101
|
+
expect(a.firstChunkDrawCount()).toBe(12);
|
|
102
|
+
a.controls.setScrubPosition(null);
|
|
103
|
+
await a.settle();
|
|
104
|
+
expect(a.firstChunkDrawCount()).toBe(24);
|
|
105
|
+
await a.dispose();
|
|
106
|
+
});
|
|
107
|
+
it('drives the DD-006 progress chain honestly (exact → cleared)', async () => {
|
|
108
|
+
const a = await harness.create();
|
|
109
|
+
await a.parse(new Uint8Array(1_000));
|
|
110
|
+
await a.settle();
|
|
111
|
+
const mapped = a.observeProgress({ v: 1, timestampMs: 1_000, position: { byte: 55 } });
|
|
112
|
+
await a.settle();
|
|
113
|
+
expect(mapped).toMatchObject({ basis: 'byte', confidence: 'known', segIndex: 5 });
|
|
114
|
+
expect(a.getState().presentation).toBe('exact');
|
|
115
|
+
a.clearProgress();
|
|
116
|
+
await a.settle();
|
|
117
|
+
expect(a.getState().presentation).toBe('hidden');
|
|
118
|
+
await a.dispose();
|
|
119
|
+
});
|
|
120
|
+
it('progress is unavailable before a parse (no fabricated mapping)', async () => {
|
|
121
|
+
const a = await harness.create();
|
|
122
|
+
expect(a.observeProgress({ v: 1, timestampMs: 1, position: { percent: 0.5 } })).toBeNull();
|
|
123
|
+
await a.dispose();
|
|
124
|
+
});
|
|
125
|
+
it('honors DD-005 bed precedence: consumer volume blocks auto-apply, discovery is evented', async () => {
|
|
126
|
+
const a = await harness.create({ machine: TEST_MACHINE, consumerVolume: true });
|
|
127
|
+
const events = [];
|
|
128
|
+
a.onEvent((e) => events.push(e));
|
|
129
|
+
await a.parse(new Uint8Array(1_000));
|
|
130
|
+
await a.settle();
|
|
131
|
+
expect(events.filter((e) => e.type === 'machine-geometry-discovered').length).toBe(1);
|
|
132
|
+
await a.dispose();
|
|
133
|
+
});
|
|
134
|
+
it('disposes cleanly: 10 lifecycles leak no workers', async () => {
|
|
135
|
+
const created0 = SuiteStubWorker.created;
|
|
136
|
+
const terminated0 = SuiteStubWorker.terminated;
|
|
137
|
+
for (let i = 0; i < 10; i++) {
|
|
138
|
+
const a = await harness.create();
|
|
139
|
+
await a.parse(new Uint8Array(8));
|
|
140
|
+
await a.settle();
|
|
141
|
+
await a.dispose();
|
|
142
|
+
}
|
|
143
|
+
expect(SuiteStubWorker.created - created0).toBe(10);
|
|
144
|
+
expect(SuiteStubWorker.terminated - terminated0).toBe(10);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chestnutlabs/gcode-preview-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-neutral preview controller for the Chestnut Labs G-code viewer (DD-007 \u00a74.6): the shared engine glue, state model, and TypeScript contracts beneath the Vue/React/Svelte adapters.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"gcode",
|
|
7
|
+
"3d-printing",
|
|
8
|
+
"toolpath",
|
|
9
|
+
"cnc",
|
|
10
|
+
"chestnutlabs",
|
|
11
|
+
"viewer",
|
|
12
|
+
"preview",
|
|
13
|
+
"controller",
|
|
14
|
+
"framework-neutral"
|
|
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/gcode-preview-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
|
+
"./testing": {
|
|
39
|
+
"types": "./dist/testing.d.ts",
|
|
40
|
+
"import": "./dist/testing.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.json",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@chestnutlabs/gcode-parser": "0.1.0",
|
|
56
|
+
"@chestnutlabs/gcode-renderer-three": "0.1.0",
|
|
57
|
+
"@chestnutlabs/toolpath-core": "0.1.0"
|
|
58
|
+
}
|
|
59
|
+
}
|