@chestnutlabs/gcode-preview-react 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 +60 -0
- package/dist/gcode-preview-component.d.ts +63 -0
- package/dist/gcode-preview-component.d.ts.map +1 -0
- package/dist/gcode-preview-component.js +114 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/use-gcode-preview.d.ts +27 -0
- package/dist/use-gcode-preview.d.ts.map +1 -0
- package/dist/use-gcode-preview.js +74 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @chestnutlabs/gcode-preview-react
|
|
2
|
+
|
|
3
|
+
Thin React integration for the Chestnut Labs G-code viewer — a reactivity bridge over
|
|
4
|
+
`@chestnutlabs/gcode-preview-core` with **capability parity** to the Vue and Svelte adapters
|
|
5
|
+
(same engine, same state model, same TypeScript contracts; DD-007 D1 amendment).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @chestnutlabs/gcode-preview-react three
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
(`three` is a peerDependency of the renderer, supported range `^0.178.0` — npm ≥ 7 installs
|
|
12
|
+
it automatically; pnpm/yarn users add it explicitly. See the
|
|
13
|
+
[support policy](../../docs/reference/support-policy.md).)
|
|
14
|
+
|
|
15
|
+
## Ready-to-use component
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { GcodePreview } from '@chestnutlabs/gcode-preview-react';
|
|
19
|
+
|
|
20
|
+
function Viewer({ file }: { file: File | null }) {
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ height: '70vh' }}>
|
|
23
|
+
<GcodePreview source={file} onReady={(s) => console.log(`${s.segments} segments`)} />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That is a complete viewer (StrictMode-safe). The full surface is optional props with sensible
|
|
30
|
+
defaults — `source`, `parseOptions`, `buildVolume` (consumer-wins bed precedence), `quality`,
|
|
31
|
+
`colorMode`, `layerRange`, `scrub`, `showTravel`, `progress` (DD-006 observation), `createWorker`
|
|
32
|
+
— with matching callbacks (`onReady`, `onParseError`, `onBuildComplete`, `onQualityFallback`,
|
|
33
|
+
`onMachineGeometryMismatch`/`Discovered`, `onProgressPresentationChanged`, `onDisclosure`,
|
|
34
|
+
`onError`). The full handle is reachable via `ref`.
|
|
35
|
+
|
|
36
|
+
## Headless hook (build your own controls)
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useGcodePreview } from '@chestnutlabs/gcode-preview-react';
|
|
40
|
+
|
|
41
|
+
function CustomViewer() {
|
|
42
|
+
const preview = useGcodePreview();
|
|
43
|
+
// <canvas ref={preview.canvasRef} /> — bind; unmount disposes automatically
|
|
44
|
+
// await preview.parse(bytes); preview.controls.setScrubPosition(n);
|
|
45
|
+
// preview.observeProgress(obs); preview.state (useSyncExternalStore-subscribed)
|
|
46
|
+
return <canvas ref={preview.canvasRef} style={{ width: '100%', height: '100%' }} />;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Workers
|
|
51
|
+
|
|
52
|
+
The batteries default (all dialect adapters + `.gcode.3mf`) needs zero setup under Vite; pass
|
|
53
|
+
`createWorker` for the slim build, custom adapters, other bundlers, or strict CSP. See the
|
|
54
|
+
`@chestnutlabs/gcode-preview-vue` README for the shared worker documentation — the options are
|
|
55
|
+
identical across adapters by design.
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
`tools/example-react` in the repository is a complete Vite + React app (corpus load, layer/scrub,
|
|
60
|
+
simulated live progress) running under `<StrictMode>`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BuildVolumeDef, ColorMode, QualityMode, TubeOptions } from '@chestnutlabs/gcode-renderer-three';
|
|
2
|
+
import type { MachineGeometry, ProgressObservation } from '@chestnutlabs/toolpath-core';
|
|
3
|
+
import type { WireParseOptions, WorkerLike } from '@chestnutlabs/gcode-parser';
|
|
4
|
+
import { type GcodePreviewHandle, type UseGcodePreviewOptions } from './use-gcode-preview.js';
|
|
5
|
+
export type GcodePreviewSource = Uint8Array | ArrayBuffer | File | null;
|
|
6
|
+
export interface GcodePreviewProps {
|
|
7
|
+
/** The one prop needed in practice: bytes/File to parse. Changing it re-parses. */
|
|
8
|
+
source?: GcodePreviewSource;
|
|
9
|
+
parseOptions?: WireParseOptions;
|
|
10
|
+
/** Consumer-configured volume — wins over file-discovered geometry (DD-005 precedence). */
|
|
11
|
+
buildVolume?: BuildVolumeDef | MachineGeometry;
|
|
12
|
+
quality?: QualityMode | 'auto';
|
|
13
|
+
colorMode?: ColorMode;
|
|
14
|
+
tube?: TubeOptions;
|
|
15
|
+
/** Inclusive [start, end]; null/undefined shows every layer. */
|
|
16
|
+
layerRange?: [number, number] | null;
|
|
17
|
+
/** Scrub cut (IR segment index); null shows everything up to the layer range. */
|
|
18
|
+
scrub?: number | null;
|
|
19
|
+
showTravel?: boolean;
|
|
20
|
+
/** DD-006 live progress observation; null hides the overlay. */
|
|
21
|
+
progress?: ProgressObservation | null;
|
|
22
|
+
/** Worker factory escape hatch (DD-005 slim/custom entries; D2). */
|
|
23
|
+
createWorker?: () => WorkerLike;
|
|
24
|
+
/** Advanced/test renderer injectables (pass-throughs of the renderer contract). */
|
|
25
|
+
rendererOptions?: Omit<NonNullable<UseGcodePreviewOptions['renderer']>, 'buildVolume' | 'quality' | 'colorMode' | 'tube'>;
|
|
26
|
+
onReady?: (summary: {
|
|
27
|
+
segments: number;
|
|
28
|
+
layers: number;
|
|
29
|
+
complete: boolean;
|
|
30
|
+
}) => void;
|
|
31
|
+
onParseError?: (e: {
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}) => void;
|
|
35
|
+
onParseCancelled?: () => void;
|
|
36
|
+
onParseProgress?: (p: {
|
|
37
|
+
bytesProcessed: number;
|
|
38
|
+
totalBytes: number;
|
|
39
|
+
}) => void;
|
|
40
|
+
onBuildComplete?: (e: {
|
|
41
|
+
segments: number;
|
|
42
|
+
quality: QualityMode;
|
|
43
|
+
}) => void;
|
|
44
|
+
onQualityFallback?: (e: {
|
|
45
|
+
from: QualityMode;
|
|
46
|
+
to: QualityMode;
|
|
47
|
+
reason: string;
|
|
48
|
+
}) => void;
|
|
49
|
+
onMachineGeometryMismatch?: (message: string) => void;
|
|
50
|
+
onMachineGeometryDiscovered?: (machine: MachineGeometry) => void;
|
|
51
|
+
onProgressPresentationChanged?: (e: {
|
|
52
|
+
mode: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
}) => void;
|
|
55
|
+
onDisclosure?: (text: string) => void;
|
|
56
|
+
onError?: (e: {
|
|
57
|
+
code: string;
|
|
58
|
+
message: string;
|
|
59
|
+
}) => void;
|
|
60
|
+
}
|
|
61
|
+
/** Ready-to-use viewer: `<GcodePreview source={file} />` works alone (D4). */
|
|
62
|
+
export declare const GcodePreview: import("react").ForwardRefExoticComponent<GcodePreviewProps & import("react").RefAttributes<GcodePreviewHandle>>;
|
|
63
|
+
//# 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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <GcodePreview> — the ready-to-use React component (DD-007 D1 amendment, phase 6).
|
|
3
|
+
*
|
|
4
|
+
* Strictly a SHELL over `useGcodePreview` (D1: never a separate implementation): every
|
|
5
|
+
* prop maps to a hook/controls call, every callback re-fires a controller event, and the
|
|
6
|
+
* full handle is reachable via `ref`. Advanced props are optional with sensible defaults,
|
|
7
|
+
* so `<GcodePreview source={file} />` alone is a working viewer (D4).
|
|
8
|
+
*
|
|
9
|
+
* createElement (no JSX) on purpose: the template is a single full-size <canvas>, and
|
|
10
|
+
* staying pure TypeScript keeps the package on the plain tsc build.
|
|
11
|
+
*/
|
|
12
|
+
import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
13
|
+
import { useGcodePreview } from './use-gcode-preview.js';
|
|
14
|
+
function GcodePreviewImpl(props, ref) {
|
|
15
|
+
const isMachine = props.buildVolume !== undefined && 'bed' in props.buildVolume;
|
|
16
|
+
const preview = useGcodePreview({
|
|
17
|
+
createWorker: props.createWorker,
|
|
18
|
+
renderer: {
|
|
19
|
+
buildVolume: isMachine ? undefined : props.buildVolume,
|
|
20
|
+
quality: props.quality ?? 'auto',
|
|
21
|
+
colorMode: props.colorMode,
|
|
22
|
+
tube: props.tube,
|
|
23
|
+
...props.rendererOptions
|
|
24
|
+
},
|
|
25
|
+
parseDefaults: props.parseOptions
|
|
26
|
+
});
|
|
27
|
+
useImperativeHandle(ref, () => preview, [preview]);
|
|
28
|
+
// Callbacks live in a ref so the event subscription is stable across renders.
|
|
29
|
+
const cbRef = useRef(props);
|
|
30
|
+
cbRef.current = props;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return preview.onEvent((e) => {
|
|
33
|
+
const p = cbRef.current;
|
|
34
|
+
switch (e.type) {
|
|
35
|
+
case 'parse-complete':
|
|
36
|
+
p.onReady?.({ segments: e.segments, layers: e.layers, complete: e.complete });
|
|
37
|
+
break;
|
|
38
|
+
case 'parse-error':
|
|
39
|
+
p.onParseError?.({ code: e.code, message: e.message });
|
|
40
|
+
break;
|
|
41
|
+
case 'parse-cancelled':
|
|
42
|
+
p.onParseCancelled?.();
|
|
43
|
+
break;
|
|
44
|
+
case 'parse-progress':
|
|
45
|
+
p.onParseProgress?.({ bytesProcessed: e.progress.bytesProcessed, totalBytes: e.progress.totalBytes });
|
|
46
|
+
break;
|
|
47
|
+
case 'buildComplete':
|
|
48
|
+
p.onBuildComplete?.({ segments: e.segments, quality: e.quality });
|
|
49
|
+
p.onDisclosure?.(preview.state.disclosure);
|
|
50
|
+
break;
|
|
51
|
+
case 'qualityFallback':
|
|
52
|
+
p.onQualityFallback?.({ from: e.from, to: e.to, reason: e.reason });
|
|
53
|
+
break;
|
|
54
|
+
case 'progress-presentation-changed':
|
|
55
|
+
p.onProgressPresentationChanged?.(e.reason === undefined ? { mode: e.mode } : { mode: e.mode, reason: e.reason });
|
|
56
|
+
break;
|
|
57
|
+
case 'machine-geometry-discovered':
|
|
58
|
+
p.onMachineGeometryDiscovered?.(e.machine);
|
|
59
|
+
break;
|
|
60
|
+
case 'error':
|
|
61
|
+
if (e.code === 'machine-geometry-mismatch')
|
|
62
|
+
p.onMachineGeometryMismatch?.(e.message);
|
|
63
|
+
else
|
|
64
|
+
p.onError?.({ code: e.code, message: e.message });
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}, []);
|
|
71
|
+
// ---- prop wiring: each prop is a thin call into the hook (D1 shell rule) ----
|
|
72
|
+
const { source, layerRange, scrub, showTravel, colorMode, quality, buildVolume, progress } = props;
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (source !== null && source !== undefined)
|
|
75
|
+
void preview.parse(source, cbRef.current.parseOptions);
|
|
76
|
+
}, [source]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (layerRange === null || layerRange === undefined)
|
|
79
|
+
preview.controls.setLayerRange(0, Number.POSITIVE_INFINITY);
|
|
80
|
+
else
|
|
81
|
+
preview.controls.setLayerRange(layerRange[0], layerRange[1]);
|
|
82
|
+
}, [layerRange]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
preview.controls.setScrubPosition(scrub ?? null);
|
|
85
|
+
}, [scrub]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
preview.controls.setKindVisible('travel', showTravel ?? true);
|
|
88
|
+
}, [showTravel]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (colorMode !== undefined)
|
|
91
|
+
preview.controls.setColorMode(colorMode);
|
|
92
|
+
}, [colorMode]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (quality !== undefined)
|
|
95
|
+
preview.controls.setQuality(quality);
|
|
96
|
+
}, [quality]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (buildVolume !== undefined && 'bed' in buildVolume)
|
|
99
|
+
preview.controls.setBuildVolume(buildVolume);
|
|
100
|
+
}, [buildVolume]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (progress === null || progress === undefined)
|
|
103
|
+
preview.clearProgress();
|
|
104
|
+
else
|
|
105
|
+
preview.observeProgress(progress);
|
|
106
|
+
}, [progress]);
|
|
107
|
+
return createElement('canvas', {
|
|
108
|
+
ref: preview.canvasRef,
|
|
109
|
+
style: { width: '100%', height: '100%', display: 'block', touchAction: 'none' },
|
|
110
|
+
'aria-label': '3D G-code toolpath preview'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Ready-to-use viewer: `<GcodePreview source={file} />` works alone (D4). */
|
|
114
|
+
export const GcodePreview = forwardRef(GcodePreviewImpl);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-preview-react — thin React integration (DD-007 D1 amendment).
|
|
3
|
+
*
|
|
4
|
+
* `useGcodePreview` hook + `<GcodePreview>` component, both reactivity bridges over
|
|
5
|
+
* @chestnutlabs/gcode-preview-core — never a separate viewer implementation. Shared
|
|
6
|
+
* contracts are re-exported from core.
|
|
7
|
+
*/
|
|
8
|
+
export * from './use-gcode-preview.js';
|
|
9
|
+
export { GcodePreview, type GcodePreviewProps, type GcodePreviewSource } from './gcode-preview-component.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-preview-react — thin React integration (DD-007 D1 amendment).
|
|
3
|
+
*
|
|
4
|
+
* `useGcodePreview` hook + `<GcodePreview>` component, both reactivity bridges over
|
|
5
|
+
* @chestnutlabs/gcode-preview-core — never a separate viewer implementation. Shared
|
|
6
|
+
* contracts are re-exported from core.
|
|
7
|
+
*/
|
|
8
|
+
export * from './use-gcode-preview.js';
|
|
9
|
+
export { GcodePreview } from './gcode-preview-component.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GcodeParseSession, WireParseOptions } from '@chestnutlabs/gcode-parser';
|
|
2
|
+
import type { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three';
|
|
3
|
+
import type { MappedProgress, ProgressObservation } from '@chestnutlabs/toolpath-core';
|
|
4
|
+
import { type GcodePreviewControls, type GcodePreviewState, type ParseOutcome, type PreviewControllerOptions, type PreviewEvent } from '@chestnutlabs/gcode-preview-core';
|
|
5
|
+
export type { GcodePreviewControls, GcodePreviewState, ParseOutcome, PreviewEvent } from '@chestnutlabs/gcode-preview-core';
|
|
6
|
+
/** React-facing options — identical to the controller's (the alias is the public name). */
|
|
7
|
+
export type UseGcodePreviewOptions = PreviewControllerOptions;
|
|
8
|
+
export interface GcodePreviewHandle {
|
|
9
|
+
/** Attach as `<canvas ref={preview.canvasRef} />`; null detaches safely. */
|
|
10
|
+
canvasRef: (el: HTMLCanvasElement | null) => void;
|
|
11
|
+
parse(input: Uint8Array | ArrayBuffer | File, opts?: WireParseOptions): Promise<ParseOutcome>;
|
|
12
|
+
cancel(): void;
|
|
13
|
+
observeProgress(obs: ProgressObservation): MappedProgress | null;
|
|
14
|
+
tickProgress(nowMs: number): MappedProgress | null;
|
|
15
|
+
clearProgress(): void;
|
|
16
|
+
/** Current state snapshot — render-subscribed via useSyncExternalStore. */
|
|
17
|
+
state: GcodePreviewState;
|
|
18
|
+
controls: GcodePreviewControls;
|
|
19
|
+
raw: {
|
|
20
|
+
session: GcodeParseSession;
|
|
21
|
+
renderer: () => ToolpathRenderer | null;
|
|
22
|
+
};
|
|
23
|
+
onEvent(cb: (e: PreviewEvent) => void): () => void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function useGcodePreview(options?: UseGcodePreviewOptions): GcodePreviewHandle;
|
|
27
|
+
//# 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":"AAYA,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,EAEjB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACb,MAAM,kCAAkC,CAAC;AAE1C,2FAA2F;AAC3F,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,SAAS,EAAE,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,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,eAAe,CAAC,GAAG,EAAE,mBAAmB,GAAG,cAAc,GAAG,IAAI,CAAC;IACjE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,IAAI,CAAC;IACtB,2EAA2E;IAC3E,KAAK,EAAE,iBAAiB,CAAC;IACzB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,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;AAUD,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,kBAAkB,CAiExF"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGcodePreview — the React reactivity bridge over the framework-neutral controller
|
|
3
|
+
* (DD-007 §4.6 D1 amendment, phase 6, issue #113). This file owns ONLY the React-isms:
|
|
4
|
+
* a `useSyncExternalStore` subscription to controller snapshots, a callback canvas ref,
|
|
5
|
+
* and effect-scoped disposal. Engine mechanics and contracts live in
|
|
6
|
+
* @chestnutlabs/gcode-preview-core and are RE-EXPORTED, never redeclared.
|
|
7
|
+
*
|
|
8
|
+
* StrictMode safety: React 18 dev mounts effects twice (setup → cleanup → setup). Every
|
|
9
|
+
* delegate goes through `ensure()`, which lazily (re)creates the controller after a
|
|
10
|
+
* cleanup disposed it — the remounted subscription lands on the fresh controller.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
13
|
+
import { createPreviewController } from '@chestnutlabs/gcode-preview-core';
|
|
14
|
+
export function useGcodePreview(options = {}) {
|
|
15
|
+
const optionsRef = useRef(options);
|
|
16
|
+
optionsRef.current = options;
|
|
17
|
+
const slot = useRef({ controller: null, disposed: false, canvas: null });
|
|
18
|
+
const ensure = useCallback(() => {
|
|
19
|
+
const s = slot.current;
|
|
20
|
+
if (s.controller === null || s.disposed) {
|
|
21
|
+
s.controller = createPreviewController(optionsRef.current);
|
|
22
|
+
s.disposed = false;
|
|
23
|
+
}
|
|
24
|
+
return s.controller;
|
|
25
|
+
}, []);
|
|
26
|
+
const subscribe = useCallback((cb) => ensure().onStateChange(() => cb()), [ensure]);
|
|
27
|
+
const getSnapshot = useCallback(() => ensure().getState(), [ensure]);
|
|
28
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const c = ensure(); // (re)create on mount — StrictMode's second setup lands here
|
|
31
|
+
// Refs attached BEFORE StrictMode's dispose/recreate cycle: rebind the canvas the
|
|
32
|
+
// ref delivered so the fresh controller actually renders (refs never re-fire).
|
|
33
|
+
if (slot.current.canvas !== null && c.raw.renderer() === null) {
|
|
34
|
+
c.bindCanvas(slot.current.canvas);
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
slot.current.controller?.dispose();
|
|
38
|
+
slot.current.disposed = true;
|
|
39
|
+
};
|
|
40
|
+
}, [ensure]);
|
|
41
|
+
// The handle (incl. canvasRef) must be IDENTITY-STABLE across state changes: a fresh
|
|
42
|
+
// canvasRef each render would make React detach/reattach the ref, rebuilding the
|
|
43
|
+
// renderer per snapshot — an infinite loop. `state` reads through a per-render ref.
|
|
44
|
+
const stateRef = useRef(state);
|
|
45
|
+
stateRef.current = state;
|
|
46
|
+
return useMemo(() => ({
|
|
47
|
+
canvasRef: (el) => {
|
|
48
|
+
slot.current.canvas = el;
|
|
49
|
+
if (el === null)
|
|
50
|
+
slot.current.controller?.bindCanvas(null);
|
|
51
|
+
else
|
|
52
|
+
ensure().bindCanvas(el);
|
|
53
|
+
},
|
|
54
|
+
parse: (input, opts) => ensure().parse(input, opts),
|
|
55
|
+
cancel: () => slot.current.controller?.cancel(),
|
|
56
|
+
observeProgress: (obs) => slot.current.controller?.observeProgress(obs) ?? null,
|
|
57
|
+
tickProgress: (nowMs) => slot.current.controller?.tickProgress(nowMs) ?? null,
|
|
58
|
+
clearProgress: () => slot.current.controller?.clearProgress(),
|
|
59
|
+
get state() {
|
|
60
|
+
return stateRef.current;
|
|
61
|
+
},
|
|
62
|
+
get controls() {
|
|
63
|
+
return ensure().controls;
|
|
64
|
+
},
|
|
65
|
+
get raw() {
|
|
66
|
+
return ensure().raw;
|
|
67
|
+
},
|
|
68
|
+
onEvent: (cb) => ensure().onEvent(cb),
|
|
69
|
+
dispose: () => {
|
|
70
|
+
slot.current.controller?.dispose();
|
|
71
|
+
slot.current.disposed = true;
|
|
72
|
+
}
|
|
73
|
+
}), [ensure]);
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chestnutlabs/gcode-preview-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin React integration for the Chestnut Labs G-code viewer (DD-007 D1 amendment): useGcodePreview hook + GcodePreview component as a reactivity bridge over @chestnutlabs/gcode-preview-core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"gcode",
|
|
7
|
+
"3d-printing",
|
|
8
|
+
"toolpath",
|
|
9
|
+
"cnc",
|
|
10
|
+
"chestnutlabs",
|
|
11
|
+
"react",
|
|
12
|
+
"hooks",
|
|
13
|
+
"component",
|
|
14
|
+
"viewer",
|
|
15
|
+
"preview"
|
|
16
|
+
],
|
|
17
|
+
"author": "Chestnut Labs",
|
|
18
|
+
"homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
|
|
19
|
+
"bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
|
|
23
|
+
"directory": "packages/gcode-preview-react"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@chestnutlabs/gcode-parser": "0.1.0",
|
|
53
|
+
"@chestnutlabs/gcode-preview-core": "0.1.0",
|
|
54
|
+
"@chestnutlabs/gcode-renderer-three": "0.1.0",
|
|
55
|
+
"@chestnutlabs/toolpath-core": "0.1.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/react": "^18.3.0",
|
|
62
|
+
"react": "^18.3.0",
|
|
63
|
+
"react-dom": "^18.3.0"
|
|
64
|
+
}
|
|
65
|
+
}
|