@chestnutlabs/gcode-preview-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller.d.ts +43 -6
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +107 -31
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/render-still.d.ts +5 -1
- package/dist/render-still.d.ts.map +1 -1
- package/dist/render-still.js +10 -3
- package/dist/renderer-2d-adapter.d.ts +60 -0
- package/dist/renderer-2d-adapter.d.ts.map +1 -0
- package/dist/renderer-2d-adapter.js +135 -0
- package/dist/renderer-interface.d.ts +60 -0
- package/dist/renderer-interface.d.ts.map +1 -0
- package/dist/renderer-interface.js +1 -0
- package/package.json +6 -5
package/dist/controller.d.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* only (numbers/strings): IR/typed arrays NEVER enter the state model.
|
|
11
11
|
*/
|
|
12
12
|
import { GcodeParseSession, type ParseProgress, type ParseResult, type SessionOptions, type WireParseOptions } from '@chestnutlabs/gcode-parser';
|
|
13
|
-
import {
|
|
13
|
+
import type { BuildVolumeDef, CameraMode, ColorMode, GLRendererLike, ProgressPresentationMode, QualityMode, RenderTargetCanvas, Theme, TubeOptions } from '@chestnutlabs/gcode-renderer-three';
|
|
14
|
+
import type { MoveKindToggle, PreviewRenderer, PreviewRendererEvent, RendererMode } from './renderer-interface.js';
|
|
14
15
|
import { type Confidence, type DialectMetadata, type MachineGeometry, type MappedProgress, type ProgressObservation } from '@chestnutlabs/toolpath-core';
|
|
15
16
|
/** Session/renderer events, re-emitted, plus the controller's own lifecycle events. */
|
|
16
|
-
export type PreviewEvent =
|
|
17
|
+
export type PreviewEvent = PreviewRendererEvent | {
|
|
17
18
|
type: 'parse-started';
|
|
18
19
|
bytes: number;
|
|
19
20
|
} | {
|
|
@@ -40,11 +41,25 @@ export interface PreviewControllerOptions {
|
|
|
40
41
|
/** Custom worker factory (DD-005 slim/custom entries). Default: the batteries worker. */
|
|
41
42
|
createWorker?: SessionOptions['worker'];
|
|
42
43
|
renderer?: {
|
|
44
|
+
/**
|
|
45
|
+
* Renderer implementation (DD-014 D5): `'3d'` (default) is the Three.js renderer; `'2d'` is the
|
|
46
|
+
* low-resource Canvas 2D layer view. The 3D renderer is loaded on demand, so a `'2d'` consumer's
|
|
47
|
+
* bundle never pulls Three.js. 3D-only options below are ignored (and disclosed) in `'2d'`.
|
|
48
|
+
*/
|
|
49
|
+
mode?: RendererMode;
|
|
43
50
|
buildVolume?: BuildVolumeDef;
|
|
44
51
|
quality?: QualityMode | 'auto';
|
|
52
|
+
/** Camera projection (#150, DD-009 D3); default 'perspective'. 3D only. */
|
|
53
|
+
cameraMode?: CameraMode;
|
|
45
54
|
colorMode?: ColorMode;
|
|
46
55
|
tube?: TubeOptions;
|
|
47
|
-
/**
|
|
56
|
+
/** Bounded declarative theme (#153, DD-009 D4). 3D only. */
|
|
57
|
+
theme?: Theme;
|
|
58
|
+
/** 2D only: preceding "ghost" layers drawn beneath the active one (default 1, floor 0). */
|
|
59
|
+
adjacentLayers?: number;
|
|
60
|
+
/** 2D only: ghost-layer opacity (default 0.25). */
|
|
61
|
+
ghostOpacity?: number;
|
|
62
|
+
/** Advanced/test injectables — pass-throughs of the renderer's own contract (3D only). */
|
|
48
63
|
createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
|
|
49
64
|
scheduleFrame?: (cb: () => void) => void;
|
|
50
65
|
chunksPerTick?: number;
|
|
@@ -86,6 +101,13 @@ export interface GcodePreviewState {
|
|
|
86
101
|
segmentCount: number;
|
|
87
102
|
/** User-presentable decimation/travel disclosure (empty when rendering everything). */
|
|
88
103
|
disclosure: string;
|
|
104
|
+
/**
|
|
105
|
+
* Estimated total print time in ms (#181), or null before a parse. `timeEstimateSource` says whether
|
|
106
|
+
* it's the slicer's own estimate (`'slicer'`, trustworthy) or a kinematic approximation
|
|
107
|
+
* (`'kinematic'`, constant-velocity, slightly low — disclose when shown).
|
|
108
|
+
*/
|
|
109
|
+
totalTimeMs: number | null;
|
|
110
|
+
timeEstimateSource: 'slicer' | 'kinematic' | null;
|
|
89
111
|
error: {
|
|
90
112
|
code: string;
|
|
91
113
|
message: string;
|
|
@@ -94,9 +116,20 @@ export interface GcodePreviewState {
|
|
|
94
116
|
export interface GcodePreviewControls {
|
|
95
117
|
setLayerRange(startLayer: number, endLayer: number): void;
|
|
96
118
|
setScrubPosition(segIndex: number | null): void;
|
|
97
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Time-based scrub (#181): cut the toolpath at print time `ms` (kinematic axis). null clears the
|
|
121
|
+
* cut. Resolves to a segment-index scrub; no-op before a successful parse.
|
|
122
|
+
*/
|
|
123
|
+
setScrubTime(ms: number | null): void;
|
|
124
|
+
setKindVisible(kind: MoveKindToggle, visible: boolean): void;
|
|
125
|
+
/** Opt-in retraction/deretraction markers (DD-009 D1, #148). Off by default. */
|
|
126
|
+
setShowRetractions(visible: boolean): void;
|
|
98
127
|
setColorMode(mode: ColorMode): boolean;
|
|
99
128
|
setQuality(quality: QualityMode | 'auto'): void;
|
|
129
|
+
/** Switch camera projection (#150, DD-009 D3). */
|
|
130
|
+
setCameraMode(mode: CameraMode): void;
|
|
131
|
+
/** Apply a bounded declarative theme (#153, DD-009 D4). */
|
|
132
|
+
setTheme(theme: Theme): void;
|
|
100
133
|
/** Marks the volume consumer-configured: file-discovered geometry stops auto-applying. */
|
|
101
134
|
setBuildVolume(def: BuildVolumeDef | MachineGeometry): void;
|
|
102
135
|
frame(): void;
|
|
@@ -116,10 +149,14 @@ export interface PreviewController {
|
|
|
116
149
|
/** Subscribe to snapshot replacements. Returns the unsubscriber. */
|
|
117
150
|
onStateChange(cb: (state: GcodePreviewState) => void): () => void;
|
|
118
151
|
controls: GcodePreviewControls;
|
|
119
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Escape hatches — the neutral objects themselves (advanced use). `renderer()` returns the active
|
|
154
|
+
* {@link PreviewRenderer} (the 3D `ToolpathRenderer` or the 2D `LayerView2DRenderer`), or null
|
|
155
|
+
* before bind / during the 3D renderer's on-demand load.
|
|
156
|
+
*/
|
|
120
157
|
raw: {
|
|
121
158
|
session: GcodeParseSession;
|
|
122
|
-
renderer: () =>
|
|
159
|
+
renderer: () => PreviewRenderer | null;
|
|
123
160
|
};
|
|
124
161
|
onEvent(cb: (e: PreviewEvent) => void): () => void;
|
|
125
162
|
dispose(): void;
|
package/dist/controller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,iBAAiB,EAGjB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|
package/dist/controller.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* only (numbers/strings): IR/typed arrays NEVER enter the state model.
|
|
11
11
|
*/
|
|
12
12
|
import { GcodeParseSession, CancelledError, ParseSessionError } from '@chestnutlabs/gcode-parser';
|
|
13
|
-
import {
|
|
14
|
-
import { createProgressMapper } from '@chestnutlabs/toolpath-core';
|
|
13
|
+
import { LayerView2DRenderer } from './renderer-2d-adapter.js';
|
|
14
|
+
import { createProgressMapper, computeToolpathTime, segmentsCompletedAtTime } from '@chestnutlabs/toolpath-core';
|
|
15
15
|
const INITIAL_STATE = {
|
|
16
16
|
parsing: false,
|
|
17
17
|
parseProgress: null,
|
|
@@ -22,6 +22,8 @@ const INITIAL_STATE = {
|
|
|
22
22
|
layerCount: 0,
|
|
23
23
|
segmentCount: 0,
|
|
24
24
|
disclosure: '',
|
|
25
|
+
totalTimeMs: null,
|
|
26
|
+
timeEstimateSource: null,
|
|
25
27
|
error: null
|
|
26
28
|
};
|
|
27
29
|
function toBytes(input) {
|
|
@@ -40,6 +42,7 @@ export function createPreviewController(options = {}) {
|
|
|
40
42
|
let renderer = null;
|
|
41
43
|
let unbindRendererEvents = null;
|
|
42
44
|
let lastIR = null;
|
|
45
|
+
let timeAxis = null;
|
|
43
46
|
let lastMachine;
|
|
44
47
|
let mapper = null;
|
|
45
48
|
let consumerVolumeSet = options.renderer?.buildVolume !== undefined;
|
|
@@ -47,6 +50,17 @@ export function createPreviewController(options = {}) {
|
|
|
47
50
|
let resizeObserver = null;
|
|
48
51
|
let windowResize = null;
|
|
49
52
|
let disposed = false;
|
|
53
|
+
// The 3D renderer is loaded on demand (DD-014: a 2D-only bundle never pulls Three.js), so binding
|
|
54
|
+
// can resolve asynchronously. `bindGen` invalidates a slow load superseded by rebind/dispose;
|
|
55
|
+
// `pendingOps` replays control calls made before the renderer is ready.
|
|
56
|
+
let bindGen = 0;
|
|
57
|
+
const pendingOps = [];
|
|
58
|
+
const withRenderer = (fn) => {
|
|
59
|
+
if (renderer !== null)
|
|
60
|
+
fn(renderer);
|
|
61
|
+
else
|
|
62
|
+
pendingOps.push(fn);
|
|
63
|
+
};
|
|
50
64
|
const listeners = new Set();
|
|
51
65
|
const emit = (e) => {
|
|
52
66
|
for (const cb of listeners)
|
|
@@ -90,26 +104,10 @@ export function createPreviewController(options = {}) {
|
|
|
90
104
|
const h = canvas.clientHeight || canvas.height || 1;
|
|
91
105
|
renderer?.resize(w, h);
|
|
92
106
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
107
|
+
/** Wire a freshly-constructed renderer: events, sizing, replay of IR + queued control ops. */
|
|
108
|
+
function applyRendererReady(canvas, r) {
|
|
109
|
+
renderer = r;
|
|
110
|
+
unbindRendererEvents = r.onEvent(onRendererEvent);
|
|
113
111
|
// Sizing (§4.2): explicit initial fit; ResizeObserver where available; window-resize
|
|
114
112
|
// fallback because observers may never fire in embedded panes.
|
|
115
113
|
fitToCanvas(canvas);
|
|
@@ -123,9 +121,61 @@ export function createPreviewController(options = {}) {
|
|
|
123
121
|
window.addEventListener('resize', windowResize);
|
|
124
122
|
}
|
|
125
123
|
if (lastIR !== null) {
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
r.setIR(lastIR);
|
|
125
|
+
// DD-005 consumer-wins: only auto-apply file geometry when the consumer set no volume.
|
|
126
|
+
if (lastMachine !== undefined && !consumerVolumeSet)
|
|
127
|
+
r.setBuildVolume(lastMachine);
|
|
128
|
+
}
|
|
129
|
+
// Replay control calls made during the (async) load, in order.
|
|
130
|
+
for (const op of pendingOps)
|
|
131
|
+
op(r);
|
|
132
|
+
pendingOps.length = 0;
|
|
133
|
+
if (lastIR !== null)
|
|
134
|
+
mutate({ layerCount: r.layerCount, segmentCount: r.segmentCount });
|
|
135
|
+
}
|
|
136
|
+
function bindCanvas(canvas) {
|
|
137
|
+
if (disposed)
|
|
138
|
+
return;
|
|
139
|
+
disposeRenderer();
|
|
140
|
+
if (canvas === null)
|
|
141
|
+
return;
|
|
142
|
+
const gen = ++bindGen;
|
|
143
|
+
const r = options.renderer ?? {};
|
|
144
|
+
if ((r.mode ?? '3d') === '2d') {
|
|
145
|
+
// The 2D renderer is a static (three-free) import — construct synchronously.
|
|
146
|
+
applyRendererReady(canvas, new LayerView2DRenderer(canvas, {
|
|
147
|
+
colorMode: r.colorMode,
|
|
148
|
+
adjacentLayers: r.adjacentLayers,
|
|
149
|
+
ghostOpacity: r.ghostOpacity
|
|
150
|
+
}));
|
|
151
|
+
return;
|
|
128
152
|
}
|
|
153
|
+
// 3D: load Three.js on demand so a 2D-only bundle never ships it (DD-014 D4).
|
|
154
|
+
import('@chestnutlabs/gcode-renderer-three')
|
|
155
|
+
.then((mod) => {
|
|
156
|
+
if (disposed || gen !== bindGen)
|
|
157
|
+
return; // superseded by a rebind/dispose during the load
|
|
158
|
+
const r3d = new mod.ToolpathRenderer({
|
|
159
|
+
canvas,
|
|
160
|
+
buildVolume: r.buildVolume,
|
|
161
|
+
quality: r.quality ?? 'auto',
|
|
162
|
+
cameraMode: r.cameraMode,
|
|
163
|
+
colorMode: r.colorMode,
|
|
164
|
+
tube: r.tube,
|
|
165
|
+
theme: r.theme,
|
|
166
|
+
createRenderer: r.createRenderer,
|
|
167
|
+
scheduleFrame: r.scheduleFrame,
|
|
168
|
+
chunksPerTick: r.chunksPerTick
|
|
169
|
+
});
|
|
170
|
+
applyRendererReady(canvas, r3d);
|
|
171
|
+
})
|
|
172
|
+
.catch((err) => {
|
|
173
|
+
if (disposed || gen !== bindGen)
|
|
174
|
+
return;
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
mutate({ error: { code: 'E_RENDERER_LOAD', message } });
|
|
177
|
+
emit({ type: 'error', code: 'E_RENDERER_LOAD', message });
|
|
178
|
+
});
|
|
129
179
|
}
|
|
130
180
|
const offProgress = session.onProgress((p) => {
|
|
131
181
|
mutate({ parseProgress: p });
|
|
@@ -155,6 +205,12 @@ export function createPreviewController(options = {}) {
|
|
|
155
205
|
lastIR = result.ir;
|
|
156
206
|
lastMachine = result.metadata?.machine;
|
|
157
207
|
mapper = createProgressMapper(result.ir, { fileSizeBytes: bytes });
|
|
208
|
+
// Time axis (#181): prefer the slicer's own estimate (#183) for the total; the kinematic axis
|
|
209
|
+
// always backs time-scrub. `hasUnknownFeedrate` means the kinematic total is a lower bound.
|
|
210
|
+
timeAxis = computeToolpathTime(result.ir);
|
|
211
|
+
const slicerSeconds = result.metadata?.printEstimate?.seconds;
|
|
212
|
+
const totalTimeMs = slicerSeconds !== undefined ? slicerSeconds * 1000 : timeAxis.totalMs;
|
|
213
|
+
const timeEstimateSource = slicerSeconds !== undefined ? 'slicer' : 'kinematic';
|
|
158
214
|
const counts = renderer !== null
|
|
159
215
|
? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount }
|
|
160
216
|
: { layerCount: result.ir.layers.length, segmentCount: result.ir.segments.count };
|
|
@@ -171,6 +227,8 @@ export function createPreviewController(options = {}) {
|
|
|
171
227
|
},
|
|
172
228
|
metadata: result.metadata,
|
|
173
229
|
presentation: 'hidden',
|
|
230
|
+
totalTimeMs,
|
|
231
|
+
timeEstimateSource,
|
|
174
232
|
...(renderer !== null ? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount } : counts)
|
|
175
233
|
});
|
|
176
234
|
// DD-005 consumer-wins precedence: a consumer-configured volume blocks auto-apply.
|
|
@@ -206,16 +264,32 @@ export function createPreviewController(options = {}) {
|
|
|
206
264
|
}
|
|
207
265
|
}
|
|
208
266
|
const controls = {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
267
|
+
// Controls queue when the (async) renderer isn't ready yet, then replay in order on bind.
|
|
268
|
+
setLayerRange: (a, b) => withRenderer((r) => r.setLayerRange(a, b)),
|
|
269
|
+
setScrubPosition: (s) => withRenderer((r) => r.setScrubPosition(s)),
|
|
270
|
+
setScrubTime: (ms) => {
|
|
271
|
+
// Resolve a print time to a segment-index scrub cut via the kinematic axis (#181).
|
|
272
|
+
const seg = ms === null || timeAxis === null ? null : segmentsCompletedAtTime(timeAxis.cumulativeMs, ms);
|
|
273
|
+
withRenderer((r) => r.setScrubPosition(seg));
|
|
274
|
+
},
|
|
275
|
+
setKindVisible: (k, v) => withRenderer((r) => r.setKindVisible(k, v)),
|
|
276
|
+
setShowRetractions: (v) => withRenderer((r) => r.setShowRetractions(v)),
|
|
277
|
+
// Availability is IR/renderer-dependent; before the renderer is ready we optimistically queue
|
|
278
|
+
// and report true (the mode applies once bound). Callers can re-check after `parse-complete`.
|
|
279
|
+
setColorMode: (m) => {
|
|
280
|
+
if (renderer !== null)
|
|
281
|
+
return renderer.setColorMode(m);
|
|
282
|
+
pendingOps.push((r) => r.setColorMode(m));
|
|
283
|
+
return true;
|
|
284
|
+
},
|
|
285
|
+
setQuality: (q) => withRenderer((r) => r.setQuality(q)),
|
|
286
|
+
setCameraMode: (m) => withRenderer((r) => r.setCameraMode(m)),
|
|
287
|
+
setTheme: (t) => withRenderer((r) => r.setTheme(t)),
|
|
214
288
|
setBuildVolume: (def) => {
|
|
215
289
|
consumerVolumeSet = true;
|
|
216
|
-
|
|
290
|
+
withRenderer((r) => r.setBuildVolume(def));
|
|
217
291
|
},
|
|
218
|
-
frame: () =>
|
|
292
|
+
frame: () => withRenderer((r) => r.frame())
|
|
219
293
|
};
|
|
220
294
|
function dispose() {
|
|
221
295
|
if (disposed)
|
|
@@ -226,7 +300,9 @@ export function createPreviewController(options = {}) {
|
|
|
226
300
|
offPartial();
|
|
227
301
|
session.dispose();
|
|
228
302
|
disposeRenderer();
|
|
303
|
+
pendingOps.length = 0;
|
|
229
304
|
lastIR = null;
|
|
305
|
+
timeAxis = null;
|
|
230
306
|
mapper = null;
|
|
231
307
|
listeners.clear();
|
|
232
308
|
stateListeners.clear();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,8 @@
|
|
|
5
5
|
* The portable behavioral suite ships via the `./testing` subpath.
|
|
6
6
|
*/
|
|
7
7
|
export * from './controller.js';
|
|
8
|
+
export type { PreviewRenderer, PreviewRendererEvent, RendererMode } from './renderer-interface.js';
|
|
9
|
+
export { LayerView2DRenderer, type LayerView2DRendererOptions } from './renderer-2d-adapter.js';
|
|
8
10
|
export { renderStill, type RenderStillOptions, type RenderStillResult, type StillCameraPose } from './render-still.js';
|
|
11
|
+
export type { Theme, MaterialPreset, ThemeColor } from '@chestnutlabs/gcode-renderer-three';
|
|
9
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,iBAAiB,CAAC;AAEhC,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAAE,KAAK,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEvH,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/render-still.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* identity is NOT promised (antialiasing varies); cache by job identity.
|
|
20
20
|
*/
|
|
21
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';
|
|
22
|
+
import { type BuildVolumeDef, type CameraMode, type ColorMode, type GLRendererLike, type QualityMode, type RenderTargetCanvas, type Theme, type TubeOptions } from '@chestnutlabs/gcode-renderer-three';
|
|
23
23
|
import { type MachineGeometry, type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
24
24
|
/** Explicit camera pose (scene coordinates), overriding the deterministic frame(). */
|
|
25
25
|
export interface StillCameraPose {
|
|
@@ -36,8 +36,12 @@ export interface RenderStillOptions {
|
|
|
36
36
|
height?: number;
|
|
37
37
|
/** Quality tier (default 'auto': tubes ≤ 1 M segments, else lines). */
|
|
38
38
|
quality?: QualityMode | 'auto';
|
|
39
|
+
/** Camera projection (#150, DD-009 D3); default 'perspective'. */
|
|
40
|
+
cameraMode?: CameraMode;
|
|
39
41
|
colorMode?: ColorMode;
|
|
40
42
|
tube?: TubeOptions;
|
|
43
|
+
/** Bounded declarative theme (#153, DD-009 D4); headless stills theme identically. */
|
|
44
|
+
theme?: Theme;
|
|
41
45
|
/** Bed geometry — a renderer volume or discovered MachineGeometry. */
|
|
42
46
|
buildVolume?: BuildVolumeDef | MachineGeometry;
|
|
43
47
|
/** Inclusive layer clip [start, end]. Omitted → whole model. */
|
|
@@ -1 +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,
|
|
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,UAAU,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACV,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,kEAAkE;IAClE,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,sFAAsF;IACtF,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,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,CA6F5B"}
|
package/dist/render-still.js
CHANGED
|
@@ -61,6 +61,8 @@ export async function renderStill(source, options) {
|
|
|
61
61
|
const renderer = new ToolpathRenderer({
|
|
62
62
|
canvas,
|
|
63
63
|
quality: options.quality ?? 'auto',
|
|
64
|
+
...(options.cameraMode ? { cameraMode: options.cameraMode } : {}),
|
|
65
|
+
...(options.theme ? { theme: options.theme } : {}),
|
|
64
66
|
...(options.colorMode ? { colorMode: options.colorMode } : {}),
|
|
65
67
|
...(options.tube ? { tube: options.tube } : {}),
|
|
66
68
|
...(options.buildVolume ? { buildVolume: toVolume(options.buildVolume) } : {}),
|
|
@@ -99,9 +101,14 @@ export async function renderStill(source, options) {
|
|
|
99
101
|
renderer.setScrubPosition(options.scrub);
|
|
100
102
|
const pose = options.camera;
|
|
101
103
|
if (pose) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
// `fov` only applies to the perspective projection; on an orthographic camera
|
|
105
|
+
// it is meaningless and absent from the union type. Ortho frustum sizing already
|
|
106
|
+
// happened in setIR()→frame() from the model bounds. Structural cast keeps this
|
|
107
|
+
// module free of a direct `three` import (three is the renderer's peer dep).
|
|
108
|
+
if (pose.fov !== undefined && (options.cameraMode ?? 'perspective') === 'perspective') {
|
|
109
|
+
const cam = renderer.camera;
|
|
110
|
+
cam.fov = pose.fov;
|
|
111
|
+
cam.updateProjectionMatrix();
|
|
105
112
|
}
|
|
106
113
|
renderer.camera.position.set(pose.position[0], pose.position[1], pose.position[2]);
|
|
107
114
|
renderer.camera.lookAt(pose.target[0], pose.target[1], pose.target[2]);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LayerView2DRenderer` — adapts the low-resource Canvas 2D renderer
|
|
3
|
+
* (`@chestnutlabs/gcode-renderer-2d`) to the controller's {@link PreviewRenderer} interface
|
|
4
|
+
* (DD-014 D5), so `renderer.mode: '2d'` slots in behind the same seam as the 3D renderer.
|
|
5
|
+
*
|
|
6
|
+
* This is a **static** import of the (tiny, three-free) 2D package. The 3D renderer is loaded
|
|
7
|
+
* on demand in the controller, so a 2D-only bundle never pulls Three.js.
|
|
8
|
+
*
|
|
9
|
+
* Honesty (DD-014 §6/§11): a flat per-layer view cannot represent 3D-only options. Genuine
|
|
10
|
+
* 3D-only requests (camera projection, quality modes) are disclosed via `renderer-unsupported`
|
|
11
|
+
* — never silently faked. Coarser-granularity requests (segment scrub, retraction point markers)
|
|
12
|
+
* are documented no-ops. Nothing is ever drawn that the IR does not contain.
|
|
13
|
+
*/
|
|
14
|
+
import { type ColorMode, type TravelStyle } from '@chestnutlabs/gcode-renderer-2d';
|
|
15
|
+
import type { BuildVolumeDef, CameraMode, QualityMode, Theme } from '@chestnutlabs/gcode-renderer-three';
|
|
16
|
+
import type { MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
17
|
+
import type { MoveKindToggle, PreviewRenderer, PreviewRendererEvent } from './renderer-interface.js';
|
|
18
|
+
export interface LayerView2DRendererOptions {
|
|
19
|
+
colorMode?: ColorMode;
|
|
20
|
+
/** Preceding "ghost" layers drawn beneath the active one (default 1, floor 0). */
|
|
21
|
+
adjacentLayers?: number;
|
|
22
|
+
/** Ghost-layer opacity (default 0.25). */
|
|
23
|
+
ghostOpacity?: number;
|
|
24
|
+
/** Extrusion stroke width in device px (default 1). */
|
|
25
|
+
lineWidth?: number;
|
|
26
|
+
/** Travel style, or false to hide travel (default hidden). */
|
|
27
|
+
travel?: TravelStyle | false;
|
|
28
|
+
}
|
|
29
|
+
export declare class LayerView2DRenderer implements PreviewRenderer {
|
|
30
|
+
private readonly canvas;
|
|
31
|
+
private readonly view;
|
|
32
|
+
private ir;
|
|
33
|
+
private readonly listeners;
|
|
34
|
+
constructor(canvas: HTMLCanvasElement, opts?: LayerView2DRendererOptions);
|
|
35
|
+
private emit;
|
|
36
|
+
private disclose;
|
|
37
|
+
onEvent(cb: (e: PreviewRendererEvent) => void): () => void;
|
|
38
|
+
resize(width: number, height: number): void;
|
|
39
|
+
setIR(ir: ToolpathIR): void;
|
|
40
|
+
/** 3D-only progressive preview; the 2D view draws on the final IR (§3 non-goal). */
|
|
41
|
+
appendPartial(): void;
|
|
42
|
+
get layerCount(): number;
|
|
43
|
+
get segmentCount(): number;
|
|
44
|
+
setBuildVolume(_def: BuildVolumeDef | MachineGeometry): void;
|
|
45
|
+
setLayerRange(_startLayer: number, endLayer: number): void;
|
|
46
|
+
setScrubPosition(_segIndex: number | null): void;
|
|
47
|
+
setKindVisible(kind: MoveKindToggle, visible: boolean): void;
|
|
48
|
+
setShowRetractions(visible: boolean): void;
|
|
49
|
+
setColorMode(mode: ColorMode): boolean;
|
|
50
|
+
/** Capability gate mirroring the 3D renderer (DD-001 honesty): unknown channel → unavailable. */
|
|
51
|
+
private isColorModeAvailable;
|
|
52
|
+
setQuality(_quality: QualityMode | 'auto'): void;
|
|
53
|
+
setCameraMode(_mode: CameraMode): void;
|
|
54
|
+
setTheme(_theme: Theme): void;
|
|
55
|
+
setProgress(p: MappedProgress | null): void;
|
|
56
|
+
pickSegment(): number | null;
|
|
57
|
+
frame(): void;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=renderer-2d-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer-2d-adapter.d.ts","sourceRoot":"","sources":["../src/renderer-2d-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,WAAW,EACjB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC/F,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAErG,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;CAC9B;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAMvD,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,EAAE,CAA2B;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgD;gBAGvD,MAAM,EAAE,iBAAiB,EAC1C,IAAI,GAAE,0BAA+B;IAWvC,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI;IAK1D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAM3C,KAAK,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI;IAS3B,oFAAoF;IACpF,aAAa,IAAI,IAAI;IAIrB,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,IAAI;IAI5D,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM1D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIhD,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQ5D,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI1C,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO;IAOtC,iGAAiG;IACjG,OAAO,CAAC,oBAAoB;IAS5B,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI;IAIhD,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAItC,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,IAAI;IAI7B,WAAW,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI;IAM3C,WAAW,IAAI,MAAM,GAAG,IAAI;IAK5B,KAAK,IAAI,IAAI;IAKb,OAAO,IAAI,IAAI;CAKhB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LayerView2DRenderer` — adapts the low-resource Canvas 2D renderer
|
|
3
|
+
* (`@chestnutlabs/gcode-renderer-2d`) to the controller's {@link PreviewRenderer} interface
|
|
4
|
+
* (DD-014 D5), so `renderer.mode: '2d'` slots in behind the same seam as the 3D renderer.
|
|
5
|
+
*
|
|
6
|
+
* This is a **static** import of the (tiny, three-free) 2D package. The 3D renderer is loaded
|
|
7
|
+
* on demand in the controller, so a 2D-only bundle never pulls Three.js.
|
|
8
|
+
*
|
|
9
|
+
* Honesty (DD-014 §6/§11): a flat per-layer view cannot represent 3D-only options. Genuine
|
|
10
|
+
* 3D-only requests (camera projection, quality modes) are disclosed via `renderer-unsupported`
|
|
11
|
+
* — never silently faked. Coarser-granularity requests (segment scrub, retraction point markers)
|
|
12
|
+
* are documented no-ops. Nothing is ever drawn that the IR does not contain.
|
|
13
|
+
*/
|
|
14
|
+
import { LayerView2D, describe2DDisclosures } from '@chestnutlabs/gcode-renderer-2d';
|
|
15
|
+
export class LayerView2DRenderer {
|
|
16
|
+
canvas;
|
|
17
|
+
view;
|
|
18
|
+
ir = null;
|
|
19
|
+
listeners = new Set();
|
|
20
|
+
constructor(canvas, opts = {}) {
|
|
21
|
+
this.canvas = canvas;
|
|
22
|
+
this.view = new LayerView2D(canvas, {
|
|
23
|
+
colorMode: opts.colorMode,
|
|
24
|
+
adjacentLayers: opts.adjacentLayers,
|
|
25
|
+
ghostOpacity: opts.ghostOpacity,
|
|
26
|
+
lineWidth: opts.lineWidth,
|
|
27
|
+
travel: opts.travel
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
emit(e) {
|
|
31
|
+
for (const cb of this.listeners)
|
|
32
|
+
cb(e);
|
|
33
|
+
}
|
|
34
|
+
disclose(feature, message) {
|
|
35
|
+
this.emit({ type: 'renderer-unsupported', feature, message });
|
|
36
|
+
}
|
|
37
|
+
onEvent(cb) {
|
|
38
|
+
this.listeners.add(cb);
|
|
39
|
+
return () => this.listeners.delete(cb);
|
|
40
|
+
}
|
|
41
|
+
resize(width, height) {
|
|
42
|
+
this.canvas.width = Math.max(1, Math.round(width));
|
|
43
|
+
this.canvas.height = Math.max(1, Math.round(height));
|
|
44
|
+
this.view.render();
|
|
45
|
+
}
|
|
46
|
+
setIR(ir) {
|
|
47
|
+
this.ir = ir;
|
|
48
|
+
this.view.setToolpath(ir);
|
|
49
|
+
this.view.render();
|
|
50
|
+
// Disclose what a flat 2D view cannot faithfully represent (non-planar/CNC), never silently
|
|
51
|
+
// (DD-014 §6/§11) — an honest capability signal on the same channel as 3D-only-option notices.
|
|
52
|
+
for (const message of describe2DDisclosures(ir))
|
|
53
|
+
this.disclose('layers', message);
|
|
54
|
+
}
|
|
55
|
+
/** 3D-only progressive preview; the 2D view draws on the final IR (§3 non-goal). */
|
|
56
|
+
appendPartial() {
|
|
57
|
+
// Intentional no-op — 2D renders once on setIR; no fabricated partial geometry.
|
|
58
|
+
}
|
|
59
|
+
get layerCount() {
|
|
60
|
+
return this.ir?.layers.length ?? 0;
|
|
61
|
+
}
|
|
62
|
+
get segmentCount() {
|
|
63
|
+
return this.ir?.segments.count ?? 0;
|
|
64
|
+
}
|
|
65
|
+
setBuildVolume(_def) {
|
|
66
|
+
// No bed chrome in the 2D layer view (phase 1–3); not an error, simply not drawn.
|
|
67
|
+
}
|
|
68
|
+
setLayerRange(_startLayer, endLayer) {
|
|
69
|
+
// A single-layer 2D view shows the TOP of the requested range (the current layer).
|
|
70
|
+
this.view.setLayer(endLayer);
|
|
71
|
+
this.view.render();
|
|
72
|
+
}
|
|
73
|
+
setScrubPosition(_segIndex) {
|
|
74
|
+
// Segment-granular scrub is a 3D-clip feature; the 2D view is layer-granular. Documented no-op.
|
|
75
|
+
}
|
|
76
|
+
setKindVisible(kind, visible) {
|
|
77
|
+
// extrusion is the point of the 2D view (never hidden); wipe is a 3D-line concern with no
|
|
78
|
+
// distinct 2D representation, so only the travel toggle is meaningful here.
|
|
79
|
+
if (kind !== 'travel')
|
|
80
|
+
return;
|
|
81
|
+
this.view.setTravel(visible ? { color: 'rgba(120,120,120,0.9)', lineWidth: 0.5 } : false);
|
|
82
|
+
this.view.render();
|
|
83
|
+
}
|
|
84
|
+
setShowRetractions(visible) {
|
|
85
|
+
if (visible)
|
|
86
|
+
this.disclose('retractions', 'Retraction markers are only shown in the 3D renderer.');
|
|
87
|
+
}
|
|
88
|
+
setColorMode(mode) {
|
|
89
|
+
if (!this.isColorModeAvailable(mode.mode))
|
|
90
|
+
return false;
|
|
91
|
+
this.view.setColorMode(mode);
|
|
92
|
+
this.view.render();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** Capability gate mirroring the 3D renderer (DD-001 honesty): unknown channel → unavailable. */
|
|
96
|
+
isColorModeAvailable(mode) {
|
|
97
|
+
const caps = this.ir?.header.capabilities;
|
|
98
|
+
if (mode === 'feature')
|
|
99
|
+
return caps?.['featureRoles'] !== undefined && caps['featureRoles'] !== 'unavailable';
|
|
100
|
+
if (mode === 'colorChange')
|
|
101
|
+
return caps?.['colorChanges'] !== undefined && caps['colorChanges'] !== 'unavailable';
|
|
102
|
+
if (mode === 'object')
|
|
103
|
+
return caps?.['objects'] !== undefined && caps['objects'] !== 'unavailable';
|
|
104
|
+
if (mode === 'feedrate')
|
|
105
|
+
return caps?.['feedrate'] !== 'unavailable';
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
setQuality(_quality) {
|
|
109
|
+
this.disclose('quality', 'Quality/geometry modes apply only to the 3D renderer; the 2D view has none.');
|
|
110
|
+
}
|
|
111
|
+
setCameraMode(_mode) {
|
|
112
|
+
this.disclose('camera', 'Camera projection applies only to the 3D renderer; the 2D view is flat top-down.');
|
|
113
|
+
}
|
|
114
|
+
setTheme(_theme) {
|
|
115
|
+
// The 2D view uses per-segment colors from the color mode; scene theming is a 3D concern.
|
|
116
|
+
}
|
|
117
|
+
setProgress(p) {
|
|
118
|
+
const projected = p === null ? null : { segIndex: p.segIndex, layerIndex: p.layerIndex };
|
|
119
|
+
this.view.setProgress(projected);
|
|
120
|
+
this.view.render();
|
|
121
|
+
}
|
|
122
|
+
pickSegment() {
|
|
123
|
+
// Segment picking is a 3D raycast feature; the 2D layer view has none yet (#184).
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
frame() {
|
|
127
|
+
// The 2D view always auto-fits the whole-model frame; framing is just a redraw.
|
|
128
|
+
this.view.render();
|
|
129
|
+
}
|
|
130
|
+
dispose() {
|
|
131
|
+
this.listeners.clear();
|
|
132
|
+
this.view.setToolpath(null);
|
|
133
|
+
this.ir = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer-agnostic seam the controller talks to (DD-014 D5). Both the Three.js
|
|
3
|
+
* renderer (`@chestnutlabs/gcode-renderer-three`, loaded on demand) and the low-resource
|
|
4
|
+
* 2D renderer (`@chestnutlabs/gcode-renderer-2d`) satisfy {@link PreviewRenderer}, so the
|
|
5
|
+
* controller picks one behind this interface from a single `renderer.mode` option.
|
|
6
|
+
*
|
|
7
|
+
* The `gcode-renderer-three` imports here are **type-only** (erased at build): they must not
|
|
8
|
+
* pull Three.js into a 2D-only bundle. The 3D renderer's *values* are reached only through a
|
|
9
|
+
* dynamic `import()` in the controller, so a bundler drops Three.js when `mode` is `'2d'`.
|
|
10
|
+
*/
|
|
11
|
+
import type { BuildVolumeDef, CameraMode, ColorMode, QualityMode, RendererEvent, Theme } from '@chestnutlabs/gcode-renderer-three';
|
|
12
|
+
import type { MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
13
|
+
/** Which renderer implementation backs the preview. `'3d'` is the default (Three.js). */
|
|
14
|
+
export type RendererMode = '2d' | '3d';
|
|
15
|
+
/**
|
|
16
|
+
* Move classes whose visibility can be toggled independently (DD-009 travel toggle + DD-016 wipe).
|
|
17
|
+
* `'wipe'` is honored by the 3D renderer as its own chunk; the flat 2D view treats it as a no-op.
|
|
18
|
+
*/
|
|
19
|
+
export type MoveKindToggle = 'extrude' | 'travel' | 'wipe';
|
|
20
|
+
/**
|
|
21
|
+
* Renderer events seen by the controller. The 3D renderer emits {@link RendererEvent}; either
|
|
22
|
+
* renderer may emit `renderer-unsupported` to disclose a requested option it cannot honor (e.g.
|
|
23
|
+
* a camera mode on the flat 2D view) — an honest capability signal, never a hard error.
|
|
24
|
+
*/
|
|
25
|
+
export type PreviewRendererEvent = RendererEvent | {
|
|
26
|
+
type: 'renderer-unsupported';
|
|
27
|
+
feature: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The minimal renderer contract the controller drives. 3D-only members are honored by the 2D
|
|
32
|
+
* renderer as documented no-ops or `renderer-unsupported` disclosures — never fabricated output.
|
|
33
|
+
*/
|
|
34
|
+
export interface PreviewRenderer {
|
|
35
|
+
onEvent(cb: (e: PreviewRendererEvent) => void): () => void;
|
|
36
|
+
resize(width: number, height: number): void;
|
|
37
|
+
setIR(ir: ToolpathIR): void;
|
|
38
|
+
/** Progressive partial IR (3D only; the 2D renderer draws on the final IR). */
|
|
39
|
+
appendPartial(slice: ToolpathIR): void;
|
|
40
|
+
readonly layerCount: number;
|
|
41
|
+
readonly segmentCount: number;
|
|
42
|
+
setBuildVolume(def: BuildVolumeDef | MachineGeometry): void;
|
|
43
|
+
setLayerRange(startLayer: number, endLayer: number): void;
|
|
44
|
+
setScrubPosition(segIndex: number | null): void;
|
|
45
|
+
setKindVisible(kind: MoveKindToggle, visible: boolean): void;
|
|
46
|
+
setShowRetractions(visible: boolean): void;
|
|
47
|
+
setColorMode(mode: ColorMode): boolean;
|
|
48
|
+
setQuality(quality: QualityMode | 'auto'): void;
|
|
49
|
+
setCameraMode(mode: CameraMode): void;
|
|
50
|
+
setTheme(theme: Theme): void;
|
|
51
|
+
setProgress(p: MappedProgress | null): void;
|
|
52
|
+
/**
|
|
53
|
+
* Pick the IR segment under a pointer (normalized device coords in [-1, 1]), or null (#184). The 3D
|
|
54
|
+
* renderer raycasts the toolpath; the 2D renderer returns null (no picking yet).
|
|
55
|
+
*/
|
|
56
|
+
pickSegment(ndcX: number, ndcY: number, threshold?: number): number | null;
|
|
57
|
+
frame(): void;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=renderer-interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer-interface.d.ts","sourceRoot":"","sources":["../src/renderer-interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,KAAK,EACN,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE/F,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5B,+EAA+E;IAC/E,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,eAAe,GAAG,IAAI,CAAC;IAC5D,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAChD,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7D,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IACvC,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;IAC5C;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3E,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,IAAI,IAAI,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chestnutlabs/gcode-preview-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Framework-neutral preview controller for the Chestnut Labs G-code viewer (DD-007
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Framework-neutral preview controller for the Chestnut Labs G-code viewer (DD-007 §4.6): the shared engine glue, state model, and TypeScript contracts beneath the Vue/React/Svelte adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gcode",
|
|
7
7
|
"3d-printing",
|
|
@@ -52,8 +52,9 @@
|
|
|
52
52
|
"test": "vitest run"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@chestnutlabs/gcode-parser": "0.
|
|
56
|
-
"@chestnutlabs/gcode-renderer-
|
|
57
|
-
"@chestnutlabs/
|
|
55
|
+
"@chestnutlabs/gcode-parser": "0.3.0",
|
|
56
|
+
"@chestnutlabs/gcode-renderer-2d": "0.3.0",
|
|
57
|
+
"@chestnutlabs/gcode-renderer-three": "0.3.0",
|
|
58
|
+
"@chestnutlabs/toolpath-core": "0.3.0"
|
|
58
59
|
}
|
|
59
60
|
}
|