@chestnutlabs/gcode-preview-core 0.2.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 +35 -8
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +107 -36
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +5 -4
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,15 +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';
|
|
45
|
-
/** Camera projection (#150, DD-009 D3); default 'perspective'. */
|
|
52
|
+
/** Camera projection (#150, DD-009 D3); default 'perspective'. 3D only. */
|
|
46
53
|
cameraMode?: CameraMode;
|
|
47
54
|
colorMode?: ColorMode;
|
|
48
55
|
tube?: TubeOptions;
|
|
49
|
-
/** Bounded declarative theme (#153, DD-009 D4). */
|
|
56
|
+
/** Bounded declarative theme (#153, DD-009 D4). 3D only. */
|
|
50
57
|
theme?: Theme;
|
|
51
|
-
/**
|
|
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). */
|
|
52
63
|
createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
|
|
53
64
|
scheduleFrame?: (cb: () => void) => void;
|
|
54
65
|
chunksPerTick?: number;
|
|
@@ -90,6 +101,13 @@ export interface GcodePreviewState {
|
|
|
90
101
|
segmentCount: number;
|
|
91
102
|
/** User-presentable decimation/travel disclosure (empty when rendering everything). */
|
|
92
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;
|
|
93
111
|
error: {
|
|
94
112
|
code: string;
|
|
95
113
|
message: string;
|
|
@@ -98,7 +116,12 @@ export interface GcodePreviewState {
|
|
|
98
116
|
export interface GcodePreviewControls {
|
|
99
117
|
setLayerRange(startLayer: number, endLayer: number): void;
|
|
100
118
|
setScrubPosition(segIndex: number | null): void;
|
|
101
|
-
|
|
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;
|
|
102
125
|
/** Opt-in retraction/deretraction markers (DD-009 D1, #148). Off by default. */
|
|
103
126
|
setShowRetractions(visible: boolean): void;
|
|
104
127
|
setColorMode(mode: ColorMode): boolean;
|
|
@@ -126,10 +149,14 @@ export interface PreviewController {
|
|
|
126
149
|
/** Subscribe to snapshot replacements. Returns the unsubscriber. */
|
|
127
150
|
onStateChange(cb: (state: GcodePreviewState) => void): () => void;
|
|
128
151
|
controls: GcodePreviewControls;
|
|
129
|
-
/**
|
|
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
|
+
*/
|
|
130
157
|
raw: {
|
|
131
158
|
session: GcodeParseSession;
|
|
132
|
-
renderer: () =>
|
|
159
|
+
renderer: () => PreviewRenderer | null;
|
|
133
160
|
};
|
|
134
161
|
onEvent(cb: (e: PreviewEvent) => void): () => void;
|
|
135
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,28 +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
|
-
cameraMode: r.cameraMode,
|
|
107
|
-
colorMode: r.colorMode,
|
|
108
|
-
tube: r.tube,
|
|
109
|
-
theme: r.theme,
|
|
110
|
-
createRenderer: r.createRenderer,
|
|
111
|
-
scheduleFrame: r.scheduleFrame,
|
|
112
|
-
chunksPerTick: r.chunksPerTick
|
|
113
|
-
});
|
|
114
|
-
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);
|
|
115
111
|
// Sizing (§4.2): explicit initial fit; ResizeObserver where available; window-resize
|
|
116
112
|
// fallback because observers may never fire in embedded panes.
|
|
117
113
|
fitToCanvas(canvas);
|
|
@@ -125,9 +121,61 @@ export function createPreviewController(options = {}) {
|
|
|
125
121
|
window.addEventListener('resize', windowResize);
|
|
126
122
|
}
|
|
127
123
|
if (lastIR !== null) {
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
130
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
|
+
});
|
|
131
179
|
}
|
|
132
180
|
const offProgress = session.onProgress((p) => {
|
|
133
181
|
mutate({ parseProgress: p });
|
|
@@ -157,6 +205,12 @@ export function createPreviewController(options = {}) {
|
|
|
157
205
|
lastIR = result.ir;
|
|
158
206
|
lastMachine = result.metadata?.machine;
|
|
159
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';
|
|
160
214
|
const counts = renderer !== null
|
|
161
215
|
? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount }
|
|
162
216
|
: { layerCount: result.ir.layers.length, segmentCount: result.ir.segments.count };
|
|
@@ -173,6 +227,8 @@ export function createPreviewController(options = {}) {
|
|
|
173
227
|
},
|
|
174
228
|
metadata: result.metadata,
|
|
175
229
|
presentation: 'hidden',
|
|
230
|
+
totalTimeMs,
|
|
231
|
+
timeEstimateSource,
|
|
176
232
|
...(renderer !== null ? { layerCount: renderer.layerCount, segmentCount: renderer.segmentCount } : counts)
|
|
177
233
|
});
|
|
178
234
|
// DD-005 consumer-wins precedence: a consumer-configured volume blocks auto-apply.
|
|
@@ -208,19 +264,32 @@ export function createPreviewController(options = {}) {
|
|
|
208
264
|
}
|
|
209
265
|
}
|
|
210
266
|
const controls = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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)),
|
|
219
288
|
setBuildVolume: (def) => {
|
|
220
289
|
consumerVolumeSet = true;
|
|
221
|
-
|
|
290
|
+
withRenderer((r) => r.setBuildVolume(def));
|
|
222
291
|
},
|
|
223
|
-
frame: () =>
|
|
292
|
+
frame: () => withRenderer((r) => r.frame())
|
|
224
293
|
};
|
|
225
294
|
function dispose() {
|
|
226
295
|
if (disposed)
|
|
@@ -231,7 +300,9 @@ export function createPreviewController(options = {}) {
|
|
|
231
300
|
offPartial();
|
|
232
301
|
session.dispose();
|
|
233
302
|
disposeRenderer();
|
|
303
|
+
pendingOps.length = 0;
|
|
234
304
|
lastIR = null;
|
|
305
|
+
timeAxis = null;
|
|
235
306
|
mapper = null;
|
|
236
307
|
listeners.clear();
|
|
237
308
|
stateListeners.clear();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +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';
|
|
9
11
|
export type { Theme, MaterialPreset, ThemeColor } from '@chestnutlabs/gcode-renderer-three';
|
|
10
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
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chestnutlabs/gcode-preview-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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",
|
|
@@ -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
|
}
|