@chestnutlabs/gcode-preview-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @chestnutlabs/gcode-preview-vue
2
+
3
+ Thin Vue 3 integration for the Chestnut Labs G-code viewer: parse `.gcode` / `.gcode.3mf` off the
4
+ main thread, render an interactive Three.js toolpath (layers, scrub, per-file build plates, honest
5
+ live progress), and never block your UI. This package is glue only — the engine lives in
6
+ `@chestnutlabs/gcode-parser`, `@chestnutlabs/gcode-renderer-three`, and
7
+ `@chestnutlabs/toolpath-core` (design: DD-007).
8
+
9
+ ```sh
10
+ npm install @chestnutlabs/gcode-preview-vue three
11
+ ```
12
+
13
+ (`three` is a peerDependency of the renderer, supported range `^0.178.0` — npm ≥ 7 installs it
14
+ automatically; pnpm/yarn users add it explicitly. See the
15
+ [support policy](../../docs/reference/support-policy.md).)
16
+
17
+ Two adoption levels, one implementation (the component is a shell over the composable):
18
+
19
+ ## Ready-to-use component
20
+
21
+ ```vue
22
+ <script setup lang="ts">
23
+ import { GcodePreview } from '@chestnutlabs/gcode-preview-vue';
24
+ import { shallowRef } from 'vue';
25
+
26
+ const file = shallowRef<File | null>(null);
27
+ </script>
28
+
29
+ <template>
30
+ <input type="file" accept=".gcode,.3mf" @change="file = ($event.target as HTMLInputElement).files?.[0] ?? null" />
31
+ <div style="height: 70vh">
32
+ <GcodePreview :source="file" @ready="(s) => console.log(`${s.segments} segments`)" />
33
+ </div>
34
+ </template>
35
+ ```
36
+
37
+ That is a complete viewer. The full surface is optional props with sensible defaults:
38
+
39
+ | Prop | Purpose |
40
+ |---|---|
41
+ | `source` | `Uint8Array \| ArrayBuffer \| File` — changing it re-parses |
42
+ | `parse-options` | wire options (limits, dialects, containers, plate selection) |
43
+ | `build-volume` | consumer-configured plate — wins over file-discovered geometry; discovery is then emitted, not applied |
44
+ | `quality` | `'auto' \| 'lines' \| 'tubes'` |
45
+ | `color-mode` | single / by-tool / by-feature (feature coloring is capability-gated — listen for `error`) |
46
+ | `layer-range` / `scrub` / `show-travel` | clipping controls |
47
+ | `progress` | a DD-006 `ProgressObservation` — drives the honest live-progress overlay (marker for byte-exact, uncertainty band for approximate, gray when stale) |
48
+ | `create-worker` | worker factory escape hatch (see below) |
49
+
50
+ Emits: `ready`, `parse-error`, `parse-cancelled`, `parse-progress`, `build-complete`,
51
+ `quality-fallback`, `machine-geometry-mismatch`, `machine-geometry-discovered`,
52
+ `progress-presentation-changed`, `disclosure`, `error`. The underlying handle is exposed for
53
+ template refs (`ref.preview`).
54
+
55
+ ## Headless composable (build your own controls)
56
+
57
+ ```ts
58
+ import { useGcodePreview } from '@chestnutlabs/gcode-preview-vue';
59
+
60
+ const preview = useGcodePreview(); // in setup(); auto-disposes with the scope
61
+ // bind: <canvas :ref="(el) => (preview.canvasRef.value = el as HTMLCanvasElement)" />
62
+ await preview.parse(bytes); // worker parse + progressive preview
63
+ preview.controls.setLayerRange(0, 42); // scrub / colors / quality / bed / frame ...
64
+ preview.observeProgress(obs); // DD-006 live progress (tick ~1 Hz for staleness)
65
+ preview.state; // shallow read-only reactive summaries
66
+ preview.raw.session; preview.raw.renderer();// escape hatches (non-reactive)
67
+ ```
68
+
69
+ The composable owns the sharp edges: dispose-on-unmount/HMR, SSR-import safety, canvas resize
70
+ (ResizeObserver + fallback), and a strict reactivity boundary (toolpath buffers are never proxied).
71
+
72
+ ## Workers: default and custom (both first-class)
73
+
74
+ **Default (zero setup):** the batteries worker — all supported slicer/firmware dialect adapters
75
+ plus `.gcode.3mf` container support — is created automatically via the bundler-native
76
+ `new Worker(new URL(...))` pattern. Vite resolves this out of the box (this path is exercised by
77
+ the repo's Vite demo and consumer fixture).
78
+
79
+ **Custom (smaller builds / custom adapters / strict CSP / other bundlers):**
80
+
81
+ ```ts
82
+ useGcodePreview({
83
+ createWorker: () => new Worker(new URL('@chestnutlabs/gcode-parser/dist/worker-slim.js', import.meta.url), { type: 'module' })
84
+ });
85
+ // or your own worker built with createWorkerHandler(...) — see the parser package docs
86
+ ```
87
+
88
+ Linked-workspace development note: Vite consumers using `file:` links should add every
89
+ `@chestnutlabs/*` package the worker pulls in to `optimizeDeps.exclude` (installed tarballs/registry
90
+ packages need no configuration).
91
+
92
+ ## Capability honesty
93
+
94
+ Everything the engine reports as `inferred`/`approximated`/`unavailable` stays that way at this
95
+ layer: feature coloring refuses rather than fabricates, live progress shows a band instead of a
96
+ fake-precise marker, and decimation is disclosed via the `disclosure` emit — surface it.
97
+
98
+ ## Support
99
+
100
+ `.gcode` (Marlin/Klipper/RepRap-flavor; PrusaSlicer, Orca/Bambu, Cura annotations),
101
+ `.gcode.3mf` (sliced-plate export; multi-plate via `parse-options.plate`), per-file build plates,
102
+ multi-tool/AMS color, object exclusion, arcs. See the repository's
103
+ `docs/compatibility/dialects-and-containers.md` for the evidence-dated matrix and
104
+ `docs/reference/progress-signal-contract.md` for the live-progress contract.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * <GcodePreview> — the ready-to-use component (DD-007 §4.1 D1/D4, phase 2, issue #105).
3
+ *
4
+ * Strictly a SHELL over `useGcodePreview` (D1: never a separate implementation): every prop
5
+ * maps to a composable call, every emit re-fires a composable event, and the underlying handle
6
+ * is exposed for template-ref access. Advanced props are optional with sensible defaults, so
7
+ * `<GcodePreview :source="file" />` alone is a working viewer (D4).
8
+ *
9
+ * Render-function component on purpose (no SFC): the template is a single full-size <canvas>,
10
+ * and staying pure TypeScript keeps the package on the plain tsc build with typed props/emits.
11
+ */
12
+ import { type PropType } from 'vue';
13
+ import type { BuildVolumeDef, ColorMode, QualityMode, TubeOptions } from '@chestnutlabs/gcode-renderer-three';
14
+ import type { MachineGeometry, ProgressObservation } from '@chestnutlabs/toolpath-core';
15
+ import type { WireParseOptions, WorkerLike } from '@chestnutlabs/gcode-parser';
16
+ import { type UseGcodePreviewOptions } from './use-gcode-preview.js';
17
+ export type GcodePreviewSource = Uint8Array | ArrayBuffer | File | null;
18
+ export declare const GcodePreview: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
19
+ /** The one required-in-practice prop: bytes/File to parse. Changing it re-parses. */
20
+ source: {
21
+ type: PropType<GcodePreviewSource>;
22
+ default: null;
23
+ };
24
+ parseOptions: {
25
+ type: PropType<WireParseOptions>;
26
+ default: undefined;
27
+ };
28
+ /** Consumer-configured volume — wins over file-discovered geometry (DD-005 precedence). */
29
+ buildVolume: {
30
+ type: PropType<BuildVolumeDef | MachineGeometry>;
31
+ default: undefined;
32
+ };
33
+ quality: {
34
+ type: PropType<QualityMode | "auto">;
35
+ default: string;
36
+ };
37
+ colorMode: {
38
+ type: PropType<ColorMode>;
39
+ default: undefined;
40
+ };
41
+ tube: {
42
+ type: PropType<TubeOptions>;
43
+ default: undefined;
44
+ };
45
+ /** Inclusive [start, end]; null shows every layer. */
46
+ layerRange: {
47
+ type: PropType<[number, number] | null>;
48
+ default: null;
49
+ };
50
+ /** Scrub cut (IR segment index); null shows everything up to the layer range. */
51
+ scrub: {
52
+ type: PropType<number | null>;
53
+ default: null;
54
+ };
55
+ showTravel: {
56
+ type: BooleanConstructor;
57
+ default: boolean;
58
+ };
59
+ /** DD-006 live progress observation; null hides the overlay. */
60
+ progress: {
61
+ type: PropType<ProgressObservation | null>;
62
+ default: null;
63
+ };
64
+ /** Worker factory escape hatch (DD-005 slim/custom entries; D2). Factory form only —
65
+ * a worker INSTANCE cannot survive re-parse lifecycles the way a factory can. */
66
+ createWorker: {
67
+ type: PropType<() => WorkerLike>;
68
+ default: undefined;
69
+ };
70
+ /** Advanced/test renderer injectables (pass-throughs of the renderer contract). */
71
+ rendererOptions: {
72
+ type: PropType<Omit<NonNullable<UseGcodePreviewOptions["renderer"]>, "buildVolume" | "quality" | "colorMode" | "tube">>;
73
+ default: undefined;
74
+ };
75
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
76
+ [key: string]: any;
77
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
78
+ ready: (_summary: {
79
+ segments: number;
80
+ layers: number;
81
+ complete: boolean;
82
+ }) => true;
83
+ 'parse-error': (_e: {
84
+ code: string;
85
+ message: string;
86
+ }) => true;
87
+ 'parse-cancelled': () => true;
88
+ 'parse-progress': (_p: {
89
+ bytesProcessed: number;
90
+ totalBytes: number;
91
+ }) => true;
92
+ 'build-complete': (_e: {
93
+ segments: number;
94
+ quality: QualityMode;
95
+ }) => true;
96
+ 'quality-fallback': (_e: {
97
+ from: QualityMode;
98
+ to: QualityMode;
99
+ reason: string;
100
+ }) => true;
101
+ 'machine-geometry-mismatch': (_message: string) => true;
102
+ 'machine-geometry-discovered': (_machine: MachineGeometry) => true;
103
+ 'progress-presentation-changed': (_e: {
104
+ mode: string;
105
+ reason?: string;
106
+ }) => true;
107
+ disclosure: (_text: string) => true;
108
+ error: (_e: {
109
+ code: string;
110
+ message: string;
111
+ }) => true;
112
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
113
+ /** The one required-in-practice prop: bytes/File to parse. Changing it re-parses. */
114
+ source: {
115
+ type: PropType<GcodePreviewSource>;
116
+ default: null;
117
+ };
118
+ parseOptions: {
119
+ type: PropType<WireParseOptions>;
120
+ default: undefined;
121
+ };
122
+ /** Consumer-configured volume — wins over file-discovered geometry (DD-005 precedence). */
123
+ buildVolume: {
124
+ type: PropType<BuildVolumeDef | MachineGeometry>;
125
+ default: undefined;
126
+ };
127
+ quality: {
128
+ type: PropType<QualityMode | "auto">;
129
+ default: string;
130
+ };
131
+ colorMode: {
132
+ type: PropType<ColorMode>;
133
+ default: undefined;
134
+ };
135
+ tube: {
136
+ type: PropType<TubeOptions>;
137
+ default: undefined;
138
+ };
139
+ /** Inclusive [start, end]; null shows every layer. */
140
+ layerRange: {
141
+ type: PropType<[number, number] | null>;
142
+ default: null;
143
+ };
144
+ /** Scrub cut (IR segment index); null shows everything up to the layer range. */
145
+ scrub: {
146
+ type: PropType<number | null>;
147
+ default: null;
148
+ };
149
+ showTravel: {
150
+ type: BooleanConstructor;
151
+ default: boolean;
152
+ };
153
+ /** DD-006 live progress observation; null hides the overlay. */
154
+ progress: {
155
+ type: PropType<ProgressObservation | null>;
156
+ default: null;
157
+ };
158
+ /** Worker factory escape hatch (DD-005 slim/custom entries; D2). Factory form only —
159
+ * a worker INSTANCE cannot survive re-parse lifecycles the way a factory can. */
160
+ createWorker: {
161
+ type: PropType<() => WorkerLike>;
162
+ default: undefined;
163
+ };
164
+ /** Advanced/test renderer injectables (pass-throughs of the renderer contract). */
165
+ rendererOptions: {
166
+ type: PropType<Omit<NonNullable<UseGcodePreviewOptions["renderer"]>, "buildVolume" | "quality" | "colorMode" | "tube">>;
167
+ default: undefined;
168
+ };
169
+ }>> & Readonly<{
170
+ onDisclosure?: ((_text: string) => any) | undefined;
171
+ onError?: ((_e: {
172
+ code: string;
173
+ message: string;
174
+ }) => any) | undefined;
175
+ onReady?: ((_summary: {
176
+ segments: number;
177
+ layers: number;
178
+ complete: boolean;
179
+ }) => any) | undefined;
180
+ "onParse-error"?: ((_e: {
181
+ code: string;
182
+ message: string;
183
+ }) => any) | undefined;
184
+ "onParse-cancelled"?: (() => any) | undefined;
185
+ "onParse-progress"?: ((_p: {
186
+ bytesProcessed: number;
187
+ totalBytes: number;
188
+ }) => any) | undefined;
189
+ "onBuild-complete"?: ((_e: {
190
+ segments: number;
191
+ quality: QualityMode;
192
+ }) => any) | undefined;
193
+ "onQuality-fallback"?: ((_e: {
194
+ from: QualityMode;
195
+ to: QualityMode;
196
+ reason: string;
197
+ }) => any) | undefined;
198
+ "onMachine-geometry-mismatch"?: ((_message: string) => any) | undefined;
199
+ "onMachine-geometry-discovered"?: ((_machine: MachineGeometry) => any) | undefined;
200
+ "onProgress-presentation-changed"?: ((_e: {
201
+ mode: string;
202
+ reason?: string;
203
+ }) => any) | undefined;
204
+ }>, {
205
+ source: GcodePreviewSource;
206
+ buildVolume: BuildVolumeDef | MachineGeometry;
207
+ quality: QualityMode | "auto";
208
+ colorMode: ColorMode;
209
+ tube: TubeOptions;
210
+ parseOptions: WireParseOptions;
211
+ layerRange: [number, number] | null;
212
+ scrub: number | null;
213
+ showTravel: boolean;
214
+ progress: ProgressObservation | null;
215
+ createWorker: () => WorkerLike;
216
+ rendererOptions: Omit<{
217
+ buildVolume?: BuildVolumeDef;
218
+ quality?: QualityMode | "auto";
219
+ colorMode?: ColorMode;
220
+ tube?: TubeOptions;
221
+ createRenderer?: (canvas: import("@chestnutlabs/gcode-renderer-three").RenderTargetCanvas) => import("@chestnutlabs/gcode-renderer-three").GLRendererLike;
222
+ scheduleFrame?: (cb: () => void) => void;
223
+ chunksPerTick?: number;
224
+ }, "buildVolume" | "quality" | "colorMode" | "tube">;
225
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
226
+ //# sourceMappingURL=gcode-preview-component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcode-preview-component.d.ts","sourceRoot":"","sources":["../src/gcode-preview-component.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * <GcodePreview> — the ready-to-use component (DD-007 §4.1 D1/D4, phase 2, issue #105).
3
+ *
4
+ * Strictly a SHELL over `useGcodePreview` (D1: never a separate implementation): every prop
5
+ * maps to a composable call, every emit re-fires a composable event, and the underlying handle
6
+ * is exposed for template-ref access. Advanced props are optional with sensible defaults, so
7
+ * `<GcodePreview :source="file" />` alone is a working viewer (D4).
8
+ *
9
+ * Render-function component on purpose (no SFC): the template is a single full-size <canvas>,
10
+ * and staying pure TypeScript keeps the package on the plain tsc build with typed props/emits.
11
+ */
12
+ import { defineComponent, h, onMounted, ref, watch } from 'vue';
13
+ import { useGcodePreview } from './use-gcode-preview.js';
14
+ export const GcodePreview = defineComponent({
15
+ name: 'GcodePreview',
16
+ props: {
17
+ /** The one required-in-practice prop: bytes/File to parse. Changing it re-parses. */
18
+ source: { type: [Object, Uint8Array, ArrayBuffer, File], default: null },
19
+ parseOptions: { type: Object, default: undefined },
20
+ /** Consumer-configured volume — wins over file-discovered geometry (DD-005 precedence). */
21
+ buildVolume: { type: Object, default: undefined },
22
+ quality: { type: String, default: 'auto' },
23
+ colorMode: { type: Object, default: undefined },
24
+ tube: { type: Object, default: undefined },
25
+ /** Inclusive [start, end]; null shows every layer. */
26
+ layerRange: { type: Array, default: null },
27
+ /** Scrub cut (IR segment index); null shows everything up to the layer range. */
28
+ scrub: { type: Number, default: null },
29
+ showTravel: { type: Boolean, default: true },
30
+ /** DD-006 live progress observation; null hides the overlay. */
31
+ progress: { type: Object, default: null },
32
+ /** Worker factory escape hatch (DD-005 slim/custom entries; D2). Factory form only —
33
+ * a worker INSTANCE cannot survive re-parse lifecycles the way a factory can. */
34
+ createWorker: {
35
+ type: Function,
36
+ default: undefined
37
+ },
38
+ /** Advanced/test renderer injectables (pass-throughs of the renderer contract). */
39
+ rendererOptions: {
40
+ type: Object,
41
+ default: undefined
42
+ }
43
+ },
44
+ emits: {
45
+ /* eslint-disable @typescript-eslint/no-unused-vars -- emit validators document payloads */
46
+ ready: (_summary) => true,
47
+ 'parse-error': (_e) => true,
48
+ 'parse-cancelled': () => true,
49
+ 'parse-progress': (_p) => true,
50
+ 'build-complete': (_e) => true,
51
+ 'quality-fallback': (_e) => true,
52
+ 'machine-geometry-mismatch': (_message) => true,
53
+ 'machine-geometry-discovered': (_machine) => true,
54
+ 'progress-presentation-changed': (_e) => true,
55
+ disclosure: (_text) => true,
56
+ error: (_e) => true
57
+ /* eslint-enable @typescript-eslint/no-unused-vars */
58
+ },
59
+ setup(props, { emit, expose }) {
60
+ const preview = useGcodePreview({
61
+ createWorker: props.createWorker,
62
+ renderer: {
63
+ buildVolume: 'bed' in (props.buildVolume ?? {}) ? undefined : props.buildVolume,
64
+ quality: props.quality,
65
+ colorMode: props.colorMode,
66
+ tube: props.tube,
67
+ ...props.rendererOptions
68
+ },
69
+ parseDefaults: props.parseOptions
70
+ });
71
+ // A MachineGeometry buildVolume prop applies post-construction (same consumer-wins effect).
72
+ onMounted(() => {
73
+ if (props.buildVolume !== undefined && 'bed' in props.buildVolume) {
74
+ preview.controls.setBuildVolume(props.buildVolume);
75
+ }
76
+ });
77
+ preview.onEvent((e) => {
78
+ switch (e.type) {
79
+ case 'parse-complete':
80
+ emit('ready', { segments: e.segments, layers: e.layers, complete: e.complete });
81
+ break;
82
+ case 'parse-error':
83
+ emit('parse-error', { code: e.code, message: e.message });
84
+ break;
85
+ case 'parse-cancelled':
86
+ emit('parse-cancelled');
87
+ break;
88
+ case 'parse-progress':
89
+ emit('parse-progress', { bytesProcessed: e.progress.bytesProcessed, totalBytes: e.progress.totalBytes });
90
+ break;
91
+ case 'buildComplete':
92
+ emit('build-complete', { segments: e.segments, quality: e.quality });
93
+ emit('disclosure', preview.state.disclosure);
94
+ break;
95
+ case 'qualityFallback':
96
+ emit('quality-fallback', { from: e.from, to: e.to, reason: e.reason });
97
+ break;
98
+ case 'progress-presentation-changed':
99
+ emit('progress-presentation-changed', e.reason === undefined ? { mode: e.mode } : { mode: e.mode, reason: e.reason });
100
+ break;
101
+ case 'machine-geometry-discovered':
102
+ emit('machine-geometry-discovered', e.machine);
103
+ break;
104
+ case 'error':
105
+ if (e.code === 'machine-geometry-mismatch')
106
+ emit('machine-geometry-mismatch', e.message);
107
+ else
108
+ emit('error', { code: e.code, message: e.message });
109
+ break;
110
+ default:
111
+ break;
112
+ }
113
+ });
114
+ // ---- prop wiring: each prop is a thin call into the composable (D1 shell rule) ----
115
+ watch(() => props.source, (source) => {
116
+ if (source !== null)
117
+ void preview.parse(source, props.parseOptions);
118
+ }, { immediate: true });
119
+ watch(() => props.layerRange, (range) => {
120
+ if (range === null)
121
+ preview.controls.setLayerRange(0, Number.POSITIVE_INFINITY);
122
+ else
123
+ preview.controls.setLayerRange(range[0], range[1]);
124
+ });
125
+ watch(() => props.scrub, (scrub) => preview.controls.setScrubPosition(scrub));
126
+ watch(() => props.showTravel, (visible) => preview.controls.setKindVisible('travel', visible));
127
+ watch(() => props.colorMode, (mode) => {
128
+ if (mode !== undefined)
129
+ preview.controls.setColorMode(mode);
130
+ });
131
+ watch(() => props.quality, (quality) => preview.controls.setQuality(quality));
132
+ watch(() => props.buildVolume, (volume) => {
133
+ if (volume !== undefined)
134
+ preview.controls.setBuildVolume(volume);
135
+ });
136
+ watch(() => props.progress, (obs) => {
137
+ if (obs === null)
138
+ preview.clearProgress();
139
+ else
140
+ preview.observeProgress(obs);
141
+ });
142
+ // Template-ref access to the full handle (defineExpose per §4.1).
143
+ expose({ preview });
144
+ const canvasEl = ref(null);
145
+ watch(canvasEl, (el) => {
146
+ preview.canvasRef.value = el;
147
+ });
148
+ return () => h('canvas', {
149
+ ref: canvasEl,
150
+ style: { width: '100%', height: '100%', display: 'block', touchAction: 'none' },
151
+ 'aria-label': '3D G-code toolpath preview'
152
+ });
153
+ }
154
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @chestnutlabs/gcode-preview-vue — thin Vue 3 integration (DD-007).
3
+ *
4
+ * Phase 1 (issue #104): the headless `useGcodePreview` composable. Phase 2 adds the
5
+ * `<GcodePreview>` component, which is strictly a shell over this composable (D1).
6
+ * This package owns Vue-glue mechanics only — no parsing/rendering capability lives
7
+ * here, and no VueKit/Pinia/router/AnyBridge code may ever enter (DD-002 §4).
8
+ */
9
+ export * from './use-gcode-preview.js';
10
+ export { GcodePreview, type GcodePreviewSource } from './gcode-preview-component.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @chestnutlabs/gcode-preview-vue — thin Vue 3 integration (DD-007).
3
+ *
4
+ * Phase 1 (issue #104): the headless `useGcodePreview` composable. Phase 2 adds the
5
+ * `<GcodePreview>` component, which is strictly a shell over this composable (D1).
6
+ * This package owns Vue-glue mechanics only — no parsing/rendering capability lives
7
+ * here, and no VueKit/Pinia/router/AnyBridge code may ever enter (DD-002 §4).
8
+ */
9
+ export * from './use-gcode-preview.js';
10
+ export { GcodePreview } from './gcode-preview-component.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * useGcodePreview — the Vue reactivity bridge over the framework-neutral controller
3
+ * (DD-007 §4.6, refactored in phase 5/issue #112; original glue extracted to
4
+ * @chestnutlabs/gcode-preview-core). This file owns ONLY the Vue-isms: a canvas
5
+ * template ref, a shallow-reactive state mirror fed by controller snapshots, and
6
+ * scope-tied disposal. The engine mechanics, state model, and TypeScript contracts
7
+ * live in the core package and are RE-EXPORTED, never redeclared (the D1-amendment
8
+ * drift firewall). Public API is unchanged from phase 1.
9
+ */
10
+ import { type Ref } from 'vue';
11
+ import type { GcodeParseSession, WireParseOptions } from '@chestnutlabs/gcode-parser';
12
+ import type { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three';
13
+ import type { MappedProgress, ProgressObservation } from '@chestnutlabs/toolpath-core';
14
+ import { type GcodePreviewControls, type GcodePreviewState, type ParseOutcome, type PreviewControllerOptions, type PreviewEvent } from '@chestnutlabs/gcode-preview-core';
15
+ export type { GcodePreviewControls, GcodePreviewState, ParseOutcome, PreviewEvent } from '@chestnutlabs/gcode-preview-core';
16
+ /** Vue-facing options — identical to the controller's (the alias is the public name). */
17
+ export type UseGcodePreviewOptions = PreviewControllerOptions;
18
+ export interface GcodePreviewHandle {
19
+ /** Bind to a <canvas> template ref; rebinding disposes and rebuilds safely. */
20
+ canvasRef: Ref<HTMLCanvasElement | null>;
21
+ parse(input: Uint8Array | ArrayBuffer | File, opts?: WireParseOptions): Promise<ParseOutcome>;
22
+ cancel(): void;
23
+ /** DD-006: map one observation and drive the overlay. Null before a successful parse. */
24
+ observeProgress(obs: ProgressObservation): MappedProgress | null;
25
+ /** Recompute staleness (DD-006 §4.4.3) — call at ~1 Hz while telemetry may go quiet. */
26
+ tickProgress(nowMs: number): MappedProgress | null;
27
+ clearProgress(): void;
28
+ state: Readonly<GcodePreviewState>;
29
+ controls: GcodePreviewControls;
30
+ /** Escape hatches — the neutral objects themselves (advanced use; not reactive). */
31
+ raw: {
32
+ session: GcodeParseSession;
33
+ renderer: () => ToolpathRenderer | null;
34
+ };
35
+ onEvent(cb: (e: PreviewEvent) => void): () => void;
36
+ dispose(): void;
37
+ }
38
+ export declare function useGcodePreview(options?: UseGcodePreviewOptions): GcodePreviewHandle;
39
+ //# sourceMappingURL=use-gcode-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-gcode-preview.d.ts","sourceRoot":"","sources":["../src/use-gcode-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAA0E,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACvG,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvF,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACb,MAAM,kCAAkC,CAAC;AAE1C,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,SAAS,EAAE,GAAG,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACzC,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,KAAK,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IACnC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,oFAAoF;IACpF,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;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,kBAAkB,CAiCxF"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * useGcodePreview — the Vue reactivity bridge over the framework-neutral controller
3
+ * (DD-007 §4.6, refactored in phase 5/issue #112; original glue extracted to
4
+ * @chestnutlabs/gcode-preview-core). This file owns ONLY the Vue-isms: a canvas
5
+ * template ref, a shallow-reactive state mirror fed by controller snapshots, and
6
+ * scope-tied disposal. The engine mechanics, state model, and TypeScript contracts
7
+ * live in the core package and are RE-EXPORTED, never redeclared (the D1-amendment
8
+ * drift firewall). Public API is unchanged from phase 1.
9
+ */
10
+ import { getCurrentScope, onScopeDispose, readonly, ref, shallowReactive, watch } from 'vue';
11
+ import { createPreviewController } from '@chestnutlabs/gcode-preview-core';
12
+ export function useGcodePreview(options = {}) {
13
+ const controller = createPreviewController(options);
14
+ // Reactivity bridge: controller snapshots → a shallow-reactive mirror. Snapshots
15
+ // carry summaries only, so a shallow field copy is the whole bridge (§4.2 boundary).
16
+ const state = shallowReactive({ ...controller.getState() });
17
+ const offState = controller.onStateChange((snap) => {
18
+ Object.assign(state, snap);
19
+ });
20
+ const canvasRef = ref(null);
21
+ watch(canvasRef, (canvas) => controller.bindCanvas(canvas));
22
+ function dispose() {
23
+ offState();
24
+ controller.dispose();
25
+ }
26
+ // Unmount/HMR safety (§4.2): auto-dispose with the owning effect scope, when there is one.
27
+ if (getCurrentScope() !== undefined)
28
+ onScopeDispose(dispose);
29
+ return {
30
+ canvasRef,
31
+ parse: controller.parse,
32
+ cancel: controller.cancel,
33
+ observeProgress: controller.observeProgress,
34
+ tickProgress: controller.tickProgress,
35
+ clearProgress: controller.clearProgress,
36
+ state: readonly(state),
37
+ controls: controller.controls,
38
+ raw: controller.raw,
39
+ onEvent: controller.onEvent,
40
+ dispose
41
+ };
42
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@chestnutlabs/gcode-preview-vue",
3
+ "version": "0.1.0",
4
+ "description": "Thin Vue 3 integration for the Chestnut Labs G-code viewer (DD-007): useGcodePreview composable + GcodePreview component over the framework-neutral parser/renderer/progress packages.",
5
+ "keywords": [
6
+ "gcode",
7
+ "3d-printing",
8
+ "toolpath",
9
+ "cnc",
10
+ "chestnutlabs",
11
+ "vue",
12
+ "vue3",
13
+ "component",
14
+ "composable",
15
+ "viewer",
16
+ "preview"
17
+ ],
18
+ "author": "Chestnut Labs",
19
+ "homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
20
+ "bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
24
+ "directory": "packages/gcode-preview-vue"
25
+ },
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "type": "module",
31
+ "sideEffects": false,
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "test": "vitest run"
51
+ },
52
+ "dependencies": {
53
+ "@chestnutlabs/gcode-parser": "0.1.0",
54
+ "@chestnutlabs/gcode-renderer-three": "0.1.0",
55
+ "@chestnutlabs/toolpath-core": "0.1.0",
56
+ "@chestnutlabs/gcode-preview-core": "0.1.0"
57
+ },
58
+ "peerDependencies": {
59
+ "vue": "^3.4.0"
60
+ },
61
+ "devDependencies": {
62
+ "vue": "^3.4.0"
63
+ }
64
+ }