@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.
@@ -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 { ToolpathRenderer, type BuildVolumeDef, type CameraMode, type ColorMode, type GLRendererLike, type ProgressPresentationMode, type QualityMode, type RenderTargetCanvas, type RendererEvent, type Theme, type TubeOptions } from '@chestnutlabs/gcode-renderer-three';
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 = RendererEvent | {
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
- /** Advanced/test injectables — pass-throughs of the renderer's own contract. */
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
- setKindVisible(kind: 'extrude' | 'travel', visible: boolean): void;
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
- /** Escape hatches — the neutral objects themselves (advanced use). */
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: () => ToolpathRenderer | null;
159
+ renderer: () => PreviewRenderer | null;
133
160
  };
134
161
  onEvent(cb: (e: PreviewEvent) => void): () => void;
135
162
  dispose(): void;
@@ -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,EACL,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EAEzB,MAAM,6BAA6B,CAAC;AAErC,uFAAuF;AACvF,MAAM,MAAM,YAAY,GACpB,aAAa,GACb;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC/E;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IACE;uEACmE;IACnE,IAAI,EAAE,6BAA6B,CAAC;IACpC,OAAO,EAAE,eAAe,CAAC;CAC1B,CAAC;AAEN,MAAM,WAAW,wBAAwB;IACvC,yFAAyF;IACzF,YAAY,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,cAAc,CAAC;QAC7B,OAAO,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;QAC/B,kEAAkE;QAClE,UAAU,CAAC,EAAE,UAAU,CAAC;QACxB,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,mDAAmD;QACnD,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,gFAAgF;QAChF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAAC;QAChE,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;QACzC,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,yDAAyD;IACzD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,6FAA6F;AAC7F,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,wFAAwF;IACxF,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACzC,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,IAAI,CAAC;IACT,QAAQ,EAAE,eAAe,GAAG,SAAS,CAAC;IACtC,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,YAAY,EAAE,wBAAwB,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,oBAAoB;IACnC,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,SAAS,GAAG,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACnE,gFAAgF;IAChF,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,kDAAkD;IAClD,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACtC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,0FAA0F;IAC1F,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,eAAe,GAAG,IAAI,CAAC;IAC5D,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9F,MAAM,IAAI,IAAI,CAAC;IACf,yFAAyF;IACzF,eAAe,CAAC,GAAG,EAAE,mBAAmB,GAAG,cAAc,GAAG,IAAI,CAAC;IACjE,wFAAwF;IACxF,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,IAAI,CAAC;IACtB,iFAAiF;IACjF,QAAQ,IAAI,iBAAiB,CAAC;IAC9B,oEAAoE;IACpE,aAAa,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAClE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,sEAAsE;IACtE,GAAG,EAAE;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAA;KAAE,CAAC;IAC7E,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAmBD,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAmPjG"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
@@ -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 { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three';
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
- 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
- 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
- renderer.setIR(lastIR);
129
- mutate({ layerCount: renderer.layerCount, segmentCount: renderer.segmentCount });
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
- setLayerRange: (a, b) => renderer?.setLayerRange(a, b),
212
- setScrubPosition: (s) => renderer?.setScrubPosition(s),
213
- setKindVisible: (k, v) => renderer?.setKindVisible(k, v),
214
- setShowRetractions: (v) => renderer?.setShowRetractions(v),
215
- setColorMode: (m) => renderer?.setColorMode(m) ?? false,
216
- setQuality: (q) => renderer?.setQuality(q),
217
- setCameraMode: (m) => renderer?.setCameraMode(m),
218
- setTheme: (t) => renderer?.setTheme(t),
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
- renderer?.setBuildVolume(def);
290
+ withRenderer((r) => r.setBuildVolume(def));
222
291
  },
223
- frame: () => renderer?.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
@@ -1 +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;AAEvH,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oCAAoC,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
@@ -5,4 +5,5 @@
5
5
  * The portable behavioral suite ships via the `./testing` subpath.
6
6
  */
7
7
  export * from './controller.js';
8
+ export { LayerView2DRenderer } from './renderer-2d-adapter.js';
8
9
  export { renderStill } from './render-still.js';
@@ -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.2.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.2.0",
56
- "@chestnutlabs/gcode-renderer-three": "0.2.0",
57
- "@chestnutlabs/toolpath-core": "0.2.0"
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
  }