quilt-three 0.10.0 → 0.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # quilt-three
2
2
 
3
- Shared-scene Three.js viewports for Quilt, with optional React Three Fiber bindings.
3
+ Shared-scene Three.js viewports for Quilt, with React Three Fiber bindings available separately in `quilt-three-react`.
4
4
  The main workspace shares one canvas and renderer. Each same-origin companion document
5
5
  has its own renderer. Geometry, authored materials, and camera poses remain
6
6
  application-owned; moving a pane does not delete scene data.
@@ -8,20 +8,21 @@ application-owned; moving a pane does not delete scene data.
8
8
  ## Install
9
9
 
10
10
  ```sh
11
- npm install quilt-three three
12
- npm install --save-dev @types/three
11
+ npm install quilt-three
13
12
  ```
14
13
 
15
- The compatibility target is Three.js r186. For React:
14
+ With modern npm's default peer resolution, this installs the Quilt renderer,
15
+ Three.js r186, and its TypeScript declarations automatically. React and R3F are
16
+ not installed. For React applications instead use:
16
17
 
17
18
  ```sh
18
- npm install quilt-react react@^19 react-dom@^19 @react-three/fiber@^9.8.1
19
+ npm install quilt-three-react
19
20
  ```
20
21
 
21
- `quilt-three` does not import or install React or R3F. The `/react` entry requires
22
- React 19 and R3F 9.8.1 or newer within major 9. The optional React peer range also
23
- allows React 18 applications to use the **imperative base entry** without adopting
24
- R3F. Existing `quilt-react` React 18 support is unchanged.
22
+ That package installs the shared integration, Quilt React bindings, React 19,
23
+ React DOM 19, R3F 9, and the required declarations. Compatible existing peers
24
+ are reused; incompatible versions must be upgraded. Existing `quilt-react`
25
+ React 18 support is unchanged.
25
26
 
26
27
  ## Vanilla
27
28
 
@@ -141,7 +142,7 @@ Disposal is idempotent and releases only helper-owned graphics. Session disposal
141
142
  disposes attached helpers, without disposing application scene assets.
142
143
 
143
144
  In React, place `<CameraHelpers visible={showCameras} ray="pointer" />` from
144
- `quilt-three/react` inside `ThreeSessionProvider`, alongside `SharedScene` and the workspace.
145
+ `quilt-three-react` inside `ThreeSessionProvider`, alongside `SharedScene` and the workspace.
145
146
  `visible` updates reactively; other helper options are mount-only. Unmounting cleans up
146
147
  the helper instance, including React Strict Mode effect replays.
147
148
 
@@ -150,7 +151,7 @@ the helper instance, including React Strict Mode effect replays.
150
151
  ```tsx
151
152
  import { Workspace } from 'quilt-react';
152
153
  import type { PaneProps } from 'quilt-react';
153
- import { SharedScene, ThreeSessionProvider, ThreeView } from 'quilt-three/react';
154
+ import { SharedScene, ThreeSessionProvider, ThreeView } from 'quilt-three-react';
154
155
 
155
156
  // `camera` and `initialLayout` are application-owned, as in the vanilla example.
156
157
  function ScenePane(props: PaneProps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quilt-three",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Shared-scene Three.js viewports for Quilt",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,10 +15,6 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js"
17
17
  },
18
- "./react": {
19
- "types": "./dist/react.d.ts",
20
- "import": "./dist/react.js"
21
- },
22
18
  "./webgpu": {
23
19
  "types": "./dist/webgpu.d.ts",
24
20
  "import": "./dist/webgpu.js"
@@ -29,28 +25,11 @@
29
25
  "prepack": "node ../../scripts/build-package.mjs three --dependencies"
30
26
  },
31
27
  "dependencies": {
32
- "quilt-vanilla": "0.10.0"
28
+ "quilt-vanilla": "0.11.0",
29
+ "@types/three": "^0.186.0"
33
30
  },
34
31
  "peerDependencies": {
35
- "three": "^0.186.0",
36
- "@react-three/fiber": "^9.8.1",
37
- "react": "^18.3.0 || ^19.0.0",
38
- "react-dom": "^18.3.0 || ^19.0.0",
39
- "quilt-react": "0.10.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "@react-three/fiber": {
43
- "optional": true
44
- },
45
- "react": {
46
- "optional": true
47
- },
48
- "react-dom": {
49
- "optional": true
50
- },
51
- "quilt-react": {
52
- "optional": true
53
- }
32
+ "three": "^0.186.0"
54
33
  },
55
34
  "sideEffects": false,
56
35
  "publishConfig": {
package/dist/react.d.ts DELETED
@@ -1,28 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { PaneProps } from 'quilt-react';
3
- import { ThreeSession } from './index.js';
4
- import type { CameraHelpersOptions, SessionOptions, ViewOptions } from './index.js';
5
- export declare function useThreeSession(): ThreeSession;
6
- export interface ThreeSessionProviderProps {
7
- children: ReactNode;
8
- options?: Omit<SessionOptions, 'scene' | 'window' | 'requestFrame'>;
9
- onReady?: (session: ThreeSession) => void;
10
- }
11
- /** Owns one persistent R3F scene tree; place outside the Quilt Workspace. Options are mount-only. */
12
- export declare function ThreeSessionProvider({ children, options, onReady }: ThreeSessionProviderProps): import("react").JSX.Element | null;
13
- /** Declare scene content exactly once, outside individual pane components. */
14
- export declare function SharedScene({ children }: {
15
- children: ReactNode;
16
- }): null;
17
- export interface ThreeViewProps extends ViewOptions {
18
- paneContext: PaneProps;
19
- children?: ReactNode;
20
- }
21
- /** The children here are HTML overlays. Shared Three.js JSX belongs in SharedScene. */
22
- export declare function ThreeView({ paneContext, children, ...options }: ThreeViewProps): import("react").JSX.Element;
23
- export type { ThreeElements, ThreeEvent } from '@react-three/fiber';
24
- /** Mount inside ThreeSessionProvider, alongside SharedScene (not inside its R3F tree).
25
- * Visibility is reactive; other options are mount-only.
26
- */
27
- export declare function CameraHelpers({ visible, ...options }: CameraHelpersOptions): null;
28
- //# sourceMappingURL=react.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,YAAY,EAGb,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkBpF,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAsBD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC,CAAC;IACpE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AACD,qGAAqG;AACrG,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,yBAAyB,sCA8J7F;AACD,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,QAOhE;AACD,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,WAAW,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AACD,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,EAAE,cAAc,+BAiC9E;AAGD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEpE;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAc,EAAE,GAAG,OAAO,EAAE,EAAE,oBAAoB,QAgBjF"}
package/dist/react.js DELETED
@@ -1,247 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Component, createContext, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
3
- import { createRoot, events, extend, useFrame } from '@react-three/fiber';
4
- import * as THREE from 'three';
5
- import { ThreeSession, isWebGPURenderer, CameraHelpers as CameraHelpersController, } from './index.js';
6
- const Context = createContext(null);
7
- function useBridge() {
8
- const bridge = useContext(Context);
9
- if (!bridge)
10
- throw new Error('Three components require ThreeSessionProvider');
11
- return bridge;
12
- }
13
- export function useThreeSession() {
14
- return useBridge().session;
15
- }
16
- class SceneBoundary extends Component {
17
- state = { failed: false };
18
- static getDerivedStateFromError() {
19
- return { failed: true };
20
- }
21
- componentDidCatch(error) {
22
- this.props.fail(error);
23
- }
24
- render() {
25
- return this.state.failed ? null : this.props.children;
26
- }
27
- }
28
- function FrameOwner({ ready, children }) {
29
- // Positive priority delegates drawing to the shared session, while useFrame still runs once.
30
- useFrame(() => { }, 1);
31
- useLayoutEffect(ready, [ready]);
32
- return children;
33
- }
34
- /** Owns one persistent R3F scene tree; place outside the Quilt Workspace. Options are mount-only. */
35
- export function ThreeSessionProvider({ children, options, onReady }) {
36
- const [bridge, setBridge] = useState(null);
37
- const initial = useRef({ options, onReady });
38
- useEffect(() => {
39
- extend(Object.fromEntries(Object.entries(THREE).filter(([, value]) => typeof value === 'function')));
40
- const win = window;
41
- let root;
42
- let store;
43
- let content = null;
44
- let frameWindow = win;
45
- let animation = 0, disposed = false, elapsed = 0, previous;
46
- let resolve, reject;
47
- const ready = new Promise((yes, no) => {
48
- resolve = yes;
49
- reject = no;
50
- });
51
- void ready.catch(() => { });
52
- const committed = () => {
53
- resolve();
54
- session.invalidate();
55
- };
56
- const failed = (error) => {
57
- reject(error);
58
- initial.current.options?.onError?.(error);
59
- };
60
- const sceneTree = () => (_jsx(SceneBoundary, { fail: failed, children: _jsx(FrameOwner, { ready: committed, children: content }) }));
61
- const drawScene = () => {
62
- root?.render(sceneTree());
63
- };
64
- const session = new ThreeSession({
65
- ...initial.current.options,
66
- scene: new THREE.Scene(),
67
- window: win,
68
- requestFrame(owner) {
69
- if (disposed)
70
- return;
71
- if (animation && frameWindow !== owner) {
72
- frameWindow.cancelAnimationFrame(animation);
73
- animation = 0;
74
- }
75
- if (animation)
76
- return;
77
- frameWindow = owner;
78
- animation = owner.requestAnimationFrame(() => {
79
- const time = win.performance.now();
80
- animation = 0;
81
- if (previous !== undefined)
82
- elapsed += Math.min((time - previous) / 1000, 0.1);
83
- previous = time;
84
- try {
85
- store?.getState().advance(elapsed, true);
86
- session.render(time);
87
- }
88
- catch (error) {
89
- failed(error);
90
- }
91
- });
92
- },
93
- createRenderer(canvas) {
94
- const created = initial.current.options?.createRenderer?.(canvas) ??
95
- new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
96
- const connect = (renderer) => {
97
- if (!disposed && canvas.ownerDocument === win.document) {
98
- root = createRoot(canvas);
99
- const currentRoot = root;
100
- const initialized = isWebGPURenderer(renderer) ? renderer.init() : Promise.resolve();
101
- void initialized
102
- .then(() => {
103
- if (disposed)
104
- return;
105
- return currentRoot.configure({
106
- gl: renderer,
107
- scene: session.scene,
108
- frameloop: 'never',
109
- events,
110
- size: { width: 1, height: 1, top: 0, left: 0 },
111
- onCreated(state) {
112
- state.set({ invalidate: session.invalidate });
113
- },
114
- });
115
- })
116
- .then(() => {
117
- if (disposed)
118
- return;
119
- store = root.render(sceneTree());
120
- // Events are forwarded from pane elements, including companion documents.
121
- store.getState().events.disconnect?.();
122
- store.getState().setEvents({
123
- enabled: true,
124
- compute(event, state) {
125
- const element = event.currentTarget;
126
- const rect = element.getBoundingClientRect();
127
- state.pointer.set(((event.clientX - rect.left) / rect.width) * 2 - 1, (-(event.clientY - rect.top) / rect.height) * 2 + 1);
128
- state.raycaster.layers.mask = state.camera.layers.mask;
129
- state.raycaster.setFromCamera(state.pointer, state.camera);
130
- },
131
- });
132
- })
133
- .catch(failed);
134
- }
135
- return renderer;
136
- };
137
- return 'then' in created ? Promise.resolve(created).then(connect) : connect(created);
138
- },
139
- });
140
- const names = {
141
- click: 'onClick',
142
- dblclick: 'onDoubleClick',
143
- contextmenu: 'onContextMenu',
144
- wheel: 'onWheel',
145
- pointerdown: 'onPointerDown',
146
- pointerup: 'onPointerUp',
147
- pointermove: 'onPointerMove',
148
- pointerleave: 'onPointerLeave',
149
- pointercancel: 'onPointerCancel',
150
- lostpointercapture: 'onLostPointerCapture',
151
- };
152
- const value = {
153
- session,
154
- ready,
155
- setScene(next) {
156
- content = next;
157
- drawScene();
158
- session.invalidate();
159
- },
160
- pointer(_element, camera, event) {
161
- if (!store)
162
- return;
163
- const state = store.getState();
164
- const original = state.camera;
165
- state.set({ camera: camera });
166
- try {
167
- const name = names[event.type];
168
- if (name)
169
- state.events.handlers?.[name]?.(event);
170
- }
171
- finally {
172
- state.set({ camera: original });
173
- }
174
- },
175
- };
176
- setBridge(value);
177
- initial.current.onReady?.(session);
178
- return () => {
179
- disposed = true;
180
- if (animation)
181
- frameWindow.cancelAnimationFrame(animation);
182
- reject(new Error('Three session disposed before scene readiness'));
183
- root?.unmount();
184
- session.dispose();
185
- };
186
- }, []);
187
- return bridge ? _jsx(Context.Provider, { value: bridge, children: children }) : null;
188
- }
189
- /** Declare scene content exactly once, outside individual pane components. */
190
- export function SharedScene({ children }) {
191
- const bridge = useBridge();
192
- useLayoutEffect(() => {
193
- bridge.setScene(children);
194
- }, [bridge, children]);
195
- useLayoutEffect(() => () => bridge.setScene(null), [bridge]);
196
- return null;
197
- }
198
- /** The children here are HTML overlays. Shared Three.js JSX belongs in SharedScene. */
199
- export function ThreeView({ paneContext, children, ...options }) {
200
- const bridge = useBridge();
201
- const element = useRef(null);
202
- const current = useRef(options);
203
- current.current = options;
204
- useLayoutEffect(() => {
205
- if (!element.current)
206
- return;
207
- const host = element.current;
208
- const view = bridge.session.register({ ...paneContext, element: host }, {
209
- ...current.current,
210
- onPointer(event, hits, camera) {
211
- bridge.pointer(host, camera, event);
212
- current.current.onPointer?.(event, hits, camera);
213
- },
214
- });
215
- paneContext.deferReady(bridge.ready);
216
- return () => view.dispose();
217
- }, [
218
- bridge,
219
- paneContext.pane.id,
220
- paneContext.document,
221
- options.camera,
222
- options.controls,
223
- options.pipeline,
224
- ]);
225
- return (_jsx("div", { ref: element, style: { width: '100%', height: '100%', position: 'relative' }, children: children }));
226
- }
227
- /** Mount inside ThreeSessionProvider, alongside SharedScene (not inside its R3F tree).
228
- * Visibility is reactive; other options are mount-only.
229
- */
230
- export function CameraHelpers({ visible = true, ...options }) {
231
- const session = useThreeSession();
232
- const initial = useRef(options);
233
- const instance = useRef(null);
234
- useEffect(() => {
235
- const helpers = new CameraHelpersController(session, { ...initial.current, visible });
236
- instance.current = helpers;
237
- return () => {
238
- helpers.dispose();
239
- instance.current = null;
240
- };
241
- }, [session]);
242
- useEffect(() => {
243
- instance.current?.setVisible(visible);
244
- }, [visible]);
245
- return null;
246
- }
247
- //# sourceMappingURL=react.js.map
package/dist/react.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport type { ReactNode } from 'react';\nimport { createRoot, events, extend, useFrame } from '@react-three/fiber';\nimport type { RootStore, ReconcilerRoot, Events } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport type { PaneProps } from 'quilt-react';\nimport {\n ThreeSession,\n isWebGPURenderer,\n CameraHelpers as CameraHelpersController,\n} from './index.js';\nimport type { CameraHelpersOptions, SessionOptions, ViewOptions } from './index.js';\n\ninterface Bridge {\n session: ThreeSession;\n ready: Promise<void>;\n setScene(children: ReactNode): void;\n pointer(\n element: HTMLElement,\n camera: THREE.Camera,\n event: PointerEvent | MouseEvent | WheelEvent,\n ): void;\n}\nconst Context = createContext<Bridge | null>(null);\nfunction useBridge() {\n const bridge = useContext(Context);\n if (!bridge) throw new Error('Three components require ThreeSessionProvider');\n return bridge;\n}\nexport function useThreeSession(): ThreeSession {\n return useBridge().session;\n}\nclass SceneBoundary extends Component<\n { children: ReactNode; fail(error: unknown): void },\n { failed: boolean }\n> {\n override state = { failed: false };\n static getDerivedStateFromError() {\n return { failed: true };\n }\n override componentDidCatch(error: unknown) {\n this.props.fail(error);\n }\n override render() {\n return this.state.failed ? null : this.props.children;\n }\n}\nfunction FrameOwner({ ready, children }: { ready(): void; children: ReactNode }) {\n // Positive priority delegates drawing to the shared session, while useFrame still runs once.\n useFrame(() => {}, 1);\n useLayoutEffect(ready, [ready]);\n return children;\n}\nexport interface ThreeSessionProviderProps {\n children: ReactNode;\n options?: Omit<SessionOptions, 'scene' | 'window' | 'requestFrame'>;\n onReady?: (session: ThreeSession) => void;\n}\n/** Owns one persistent R3F scene tree; place outside the Quilt Workspace. Options are mount-only. */\nexport function ThreeSessionProvider({ children, options, onReady }: ThreeSessionProviderProps) {\n const [bridge, setBridge] = useState<Bridge | null>(null);\n const initial = useRef({ options, onReady });\n useEffect(() => {\n extend(\n Object.fromEntries(\n Object.entries(THREE).filter(([, value]) => typeof value === 'function'),\n ) as Parameters<typeof extend>[0],\n );\n const win = window;\n let root: ReconcilerRoot<HTMLCanvasElement> | undefined;\n let store: RootStore | undefined;\n let content: ReactNode = null;\n let frameWindow: Window = win;\n let animation = 0,\n disposed = false,\n elapsed = 0,\n previous: number | undefined;\n let resolve!: () => void, reject!: (error: unknown) => void;\n const ready = new Promise<void>((yes, no) => {\n resolve = yes;\n reject = no;\n });\n void ready.catch(() => {});\n const committed = () => {\n resolve();\n session.invalidate();\n };\n const failed = (error: unknown) => {\n reject(error);\n initial.current.options?.onError?.(error);\n };\n const sceneTree = () => (\n <SceneBoundary fail={failed}>\n <FrameOwner ready={committed}>{content}</FrameOwner>\n </SceneBoundary>\n );\n const drawScene = () => {\n root?.render(sceneTree());\n };\n const session = new ThreeSession({\n ...initial.current.options,\n scene: new THREE.Scene(),\n window: win,\n requestFrame(owner) {\n if (disposed) return;\n if (animation && frameWindow !== owner) {\n frameWindow.cancelAnimationFrame(animation);\n animation = 0;\n }\n if (animation) return;\n frameWindow = owner;\n animation = owner.requestAnimationFrame(() => {\n const time = win.performance.now();\n animation = 0;\n if (previous !== undefined) elapsed += Math.min((time - previous) / 1000, 0.1);\n previous = time;\n try {\n store?.getState().advance(elapsed, true);\n session.render(time);\n } catch (error) {\n failed(error);\n }\n });\n },\n createRenderer(canvas) {\n const created =\n initial.current.options?.createRenderer?.(canvas) ??\n new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });\n const connect = (renderer: import('./index.js').ThreeRenderer) => {\n if (!disposed && canvas.ownerDocument === win.document) {\n root = createRoot(canvas);\n const currentRoot = root;\n const initialized = isWebGPURenderer(renderer) ? renderer.init() : Promise.resolve();\n void initialized\n .then(() => {\n if (disposed) return;\n return currentRoot.configure({\n gl: renderer,\n scene: session.scene,\n frameloop: 'never',\n events,\n size: { width: 1, height: 1, top: 0, left: 0 },\n onCreated(state) {\n state.set({ invalidate: session.invalidate });\n },\n });\n })\n .then(() => {\n if (disposed) return;\n store = root!.render(sceneTree());\n // Events are forwarded from pane elements, including companion documents.\n store.getState().events.disconnect?.();\n store.getState().setEvents({\n enabled: true,\n compute(event, state) {\n const element = event.currentTarget as HTMLElement;\n const rect = element.getBoundingClientRect();\n state.pointer.set(\n ((event.clientX - rect.left) / rect.width) * 2 - 1,\n (-(event.clientY - rect.top) / rect.height) * 2 + 1,\n );\n state.raycaster.layers.mask = state.camera.layers.mask;\n state.raycaster.setFromCamera(state.pointer, state.camera);\n },\n });\n })\n .catch(failed);\n }\n return renderer;\n };\n return 'then' in created ? Promise.resolve(created).then(connect) : connect(created);\n },\n });\n const names: Record<string, keyof Events> = {\n click: 'onClick',\n dblclick: 'onDoubleClick',\n contextmenu: 'onContextMenu',\n wheel: 'onWheel',\n pointerdown: 'onPointerDown',\n pointerup: 'onPointerUp',\n pointermove: 'onPointerMove',\n pointerleave: 'onPointerLeave',\n pointercancel: 'onPointerCancel',\n lostpointercapture: 'onLostPointerCapture',\n };\n const value: Bridge = {\n session,\n ready,\n setScene(next) {\n content = next;\n drawScene();\n session.invalidate();\n },\n pointer(_element, camera, event) {\n if (!store) return;\n const state = store.getState();\n const original = state.camera;\n state.set({ camera: camera as THREE.PerspectiveCamera });\n try {\n const name = names[event.type];\n if (name) state.events.handlers?.[name]?.(event);\n } finally {\n state.set({ camera: original });\n }\n },\n };\n setBridge(value);\n initial.current.onReady?.(session);\n return () => {\n disposed = true;\n if (animation) frameWindow.cancelAnimationFrame(animation);\n reject(new Error('Three session disposed before scene readiness'));\n root?.unmount();\n session.dispose();\n };\n }, []);\n return bridge ? <Context.Provider value={bridge}>{children}</Context.Provider> : null;\n}\n/** Declare scene content exactly once, outside individual pane components. */\nexport function SharedScene({ children }: { children: ReactNode }) {\n const bridge = useBridge();\n useLayoutEffect(() => {\n bridge.setScene(children);\n }, [bridge, children]);\n useLayoutEffect(() => () => bridge.setScene(null), [bridge]);\n return null;\n}\nexport interface ThreeViewProps extends ViewOptions {\n paneContext: PaneProps;\n children?: ReactNode;\n}\n/** The children here are HTML overlays. Shared Three.js JSX belongs in SharedScene. */\nexport function ThreeView({ paneContext, children, ...options }: ThreeViewProps) {\n const bridge = useBridge();\n const element = useRef<HTMLDivElement>(null);\n const current = useRef(options);\n current.current = options;\n useLayoutEffect(() => {\n if (!element.current) return;\n const host = element.current;\n const view = bridge.session.register(\n { ...paneContext, element: host },\n {\n ...current.current,\n onPointer(event, hits, camera) {\n bridge.pointer(host, camera, event);\n current.current.onPointer?.(event, hits, camera);\n },\n },\n );\n paneContext.deferReady(bridge.ready);\n return () => view.dispose();\n }, [\n bridge,\n paneContext.pane.id,\n paneContext.document,\n options.camera,\n options.controls,\n options.pipeline,\n ]);\n return (\n <div ref={element} style={{ width: '100%', height: '100%', position: 'relative' }}>\n {children}\n </div>\n );\n}\n\n// Preserve R3F JSX augmentation for consumers importing only quilt-three/react.\nexport type { ThreeElements, ThreeEvent } from '@react-three/fiber';\n\n/** Mount inside ThreeSessionProvider, alongside SharedScene (not inside its R3F tree).\n * Visibility is reactive; other options are mount-only.\n */\nexport function CameraHelpers({ visible = true, ...options }: CameraHelpersOptions) {\n const session = useThreeSession();\n const initial = useRef(options);\n const instance = useRef<CameraHelpersController | null>(null);\n useEffect(() => {\n const helpers = new CameraHelpersController(session, { ...initial.current, visible });\n instance.current = helpers;\n return () => {\n helpers.dispose();\n instance.current = null;\n };\n }, [session]);\n useEffect(() => {\n instance.current?.setVisible(visible);\n }, [visible]);\n return null;\n}\n"]}
package/src/react.tsx DELETED
@@ -1,298 +0,0 @@
1
- import {
2
- Component,
3
- createContext,
4
- useContext,
5
- useEffect,
6
- useLayoutEffect,
7
- useRef,
8
- useState,
9
- } from 'react';
10
- import type { ReactNode } from 'react';
11
- import { createRoot, events, extend, useFrame } from '@react-three/fiber';
12
- import type { RootStore, ReconcilerRoot, Events } from '@react-three/fiber';
13
- import * as THREE from 'three';
14
- import type { PaneProps } from 'quilt-react';
15
- import {
16
- ThreeSession,
17
- isWebGPURenderer,
18
- CameraHelpers as CameraHelpersController,
19
- } from './index.js';
20
- import type { CameraHelpersOptions, SessionOptions, ViewOptions } from './index.js';
21
-
22
- interface Bridge {
23
- session: ThreeSession;
24
- ready: Promise<void>;
25
- setScene(children: ReactNode): void;
26
- pointer(
27
- element: HTMLElement,
28
- camera: THREE.Camera,
29
- event: PointerEvent | MouseEvent | WheelEvent,
30
- ): void;
31
- }
32
- const Context = createContext<Bridge | null>(null);
33
- function useBridge() {
34
- const bridge = useContext(Context);
35
- if (!bridge) throw new Error('Three components require ThreeSessionProvider');
36
- return bridge;
37
- }
38
- export function useThreeSession(): ThreeSession {
39
- return useBridge().session;
40
- }
41
- class SceneBoundary extends Component<
42
- { children: ReactNode; fail(error: unknown): void },
43
- { failed: boolean }
44
- > {
45
- override state = { failed: false };
46
- static getDerivedStateFromError() {
47
- return { failed: true };
48
- }
49
- override componentDidCatch(error: unknown) {
50
- this.props.fail(error);
51
- }
52
- override render() {
53
- return this.state.failed ? null : this.props.children;
54
- }
55
- }
56
- function FrameOwner({ ready, children }: { ready(): void; children: ReactNode }) {
57
- // Positive priority delegates drawing to the shared session, while useFrame still runs once.
58
- useFrame(() => {}, 1);
59
- useLayoutEffect(ready, [ready]);
60
- return children;
61
- }
62
- export interface ThreeSessionProviderProps {
63
- children: ReactNode;
64
- options?: Omit<SessionOptions, 'scene' | 'window' | 'requestFrame'>;
65
- onReady?: (session: ThreeSession) => void;
66
- }
67
- /** Owns one persistent R3F scene tree; place outside the Quilt Workspace. Options are mount-only. */
68
- export function ThreeSessionProvider({ children, options, onReady }: ThreeSessionProviderProps) {
69
- const [bridge, setBridge] = useState<Bridge | null>(null);
70
- const initial = useRef({ options, onReady });
71
- useEffect(() => {
72
- extend(
73
- Object.fromEntries(
74
- Object.entries(THREE).filter(([, value]) => typeof value === 'function'),
75
- ) as Parameters<typeof extend>[0],
76
- );
77
- const win = window;
78
- let root: ReconcilerRoot<HTMLCanvasElement> | undefined;
79
- let store: RootStore | undefined;
80
- let content: ReactNode = null;
81
- let frameWindow: Window = win;
82
- let animation = 0,
83
- disposed = false,
84
- elapsed = 0,
85
- previous: number | undefined;
86
- let resolve!: () => void, reject!: (error: unknown) => void;
87
- const ready = new Promise<void>((yes, no) => {
88
- resolve = yes;
89
- reject = no;
90
- });
91
- void ready.catch(() => {});
92
- const committed = () => {
93
- resolve();
94
- session.invalidate();
95
- };
96
- const failed = (error: unknown) => {
97
- reject(error);
98
- initial.current.options?.onError?.(error);
99
- };
100
- const sceneTree = () => (
101
- <SceneBoundary fail={failed}>
102
- <FrameOwner ready={committed}>{content}</FrameOwner>
103
- </SceneBoundary>
104
- );
105
- const drawScene = () => {
106
- root?.render(sceneTree());
107
- };
108
- const session = new ThreeSession({
109
- ...initial.current.options,
110
- scene: new THREE.Scene(),
111
- window: win,
112
- requestFrame(owner) {
113
- if (disposed) return;
114
- if (animation && frameWindow !== owner) {
115
- frameWindow.cancelAnimationFrame(animation);
116
- animation = 0;
117
- }
118
- if (animation) return;
119
- frameWindow = owner;
120
- animation = owner.requestAnimationFrame(() => {
121
- const time = win.performance.now();
122
- animation = 0;
123
- if (previous !== undefined) elapsed += Math.min((time - previous) / 1000, 0.1);
124
- previous = time;
125
- try {
126
- store?.getState().advance(elapsed, true);
127
- session.render(time);
128
- } catch (error) {
129
- failed(error);
130
- }
131
- });
132
- },
133
- createRenderer(canvas) {
134
- const created =
135
- initial.current.options?.createRenderer?.(canvas) ??
136
- new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
137
- const connect = (renderer: import('./index.js').ThreeRenderer) => {
138
- if (!disposed && canvas.ownerDocument === win.document) {
139
- root = createRoot(canvas);
140
- const currentRoot = root;
141
- const initialized = isWebGPURenderer(renderer) ? renderer.init() : Promise.resolve();
142
- void initialized
143
- .then(() => {
144
- if (disposed) return;
145
- return currentRoot.configure({
146
- gl: renderer,
147
- scene: session.scene,
148
- frameloop: 'never',
149
- events,
150
- size: { width: 1, height: 1, top: 0, left: 0 },
151
- onCreated(state) {
152
- state.set({ invalidate: session.invalidate });
153
- },
154
- });
155
- })
156
- .then(() => {
157
- if (disposed) return;
158
- store = root!.render(sceneTree());
159
- // Events are forwarded from pane elements, including companion documents.
160
- store.getState().events.disconnect?.();
161
- store.getState().setEvents({
162
- enabled: true,
163
- compute(event, state) {
164
- const element = event.currentTarget as HTMLElement;
165
- const rect = element.getBoundingClientRect();
166
- state.pointer.set(
167
- ((event.clientX - rect.left) / rect.width) * 2 - 1,
168
- (-(event.clientY - rect.top) / rect.height) * 2 + 1,
169
- );
170
- state.raycaster.layers.mask = state.camera.layers.mask;
171
- state.raycaster.setFromCamera(state.pointer, state.camera);
172
- },
173
- });
174
- })
175
- .catch(failed);
176
- }
177
- return renderer;
178
- };
179
- return 'then' in created ? Promise.resolve(created).then(connect) : connect(created);
180
- },
181
- });
182
- const names: Record<string, keyof Events> = {
183
- click: 'onClick',
184
- dblclick: 'onDoubleClick',
185
- contextmenu: 'onContextMenu',
186
- wheel: 'onWheel',
187
- pointerdown: 'onPointerDown',
188
- pointerup: 'onPointerUp',
189
- pointermove: 'onPointerMove',
190
- pointerleave: 'onPointerLeave',
191
- pointercancel: 'onPointerCancel',
192
- lostpointercapture: 'onLostPointerCapture',
193
- };
194
- const value: Bridge = {
195
- session,
196
- ready,
197
- setScene(next) {
198
- content = next;
199
- drawScene();
200
- session.invalidate();
201
- },
202
- pointer(_element, camera, event) {
203
- if (!store) return;
204
- const state = store.getState();
205
- const original = state.camera;
206
- state.set({ camera: camera as THREE.PerspectiveCamera });
207
- try {
208
- const name = names[event.type];
209
- if (name) state.events.handlers?.[name]?.(event);
210
- } finally {
211
- state.set({ camera: original });
212
- }
213
- },
214
- };
215
- setBridge(value);
216
- initial.current.onReady?.(session);
217
- return () => {
218
- disposed = true;
219
- if (animation) frameWindow.cancelAnimationFrame(animation);
220
- reject(new Error('Three session disposed before scene readiness'));
221
- root?.unmount();
222
- session.dispose();
223
- };
224
- }, []);
225
- return bridge ? <Context.Provider value={bridge}>{children}</Context.Provider> : null;
226
- }
227
- /** Declare scene content exactly once, outside individual pane components. */
228
- export function SharedScene({ children }: { children: ReactNode }) {
229
- const bridge = useBridge();
230
- useLayoutEffect(() => {
231
- bridge.setScene(children);
232
- }, [bridge, children]);
233
- useLayoutEffect(() => () => bridge.setScene(null), [bridge]);
234
- return null;
235
- }
236
- export interface ThreeViewProps extends ViewOptions {
237
- paneContext: PaneProps;
238
- children?: ReactNode;
239
- }
240
- /** The children here are HTML overlays. Shared Three.js JSX belongs in SharedScene. */
241
- export function ThreeView({ paneContext, children, ...options }: ThreeViewProps) {
242
- const bridge = useBridge();
243
- const element = useRef<HTMLDivElement>(null);
244
- const current = useRef(options);
245
- current.current = options;
246
- useLayoutEffect(() => {
247
- if (!element.current) return;
248
- const host = element.current;
249
- const view = bridge.session.register(
250
- { ...paneContext, element: host },
251
- {
252
- ...current.current,
253
- onPointer(event, hits, camera) {
254
- bridge.pointer(host, camera, event);
255
- current.current.onPointer?.(event, hits, camera);
256
- },
257
- },
258
- );
259
- paneContext.deferReady(bridge.ready);
260
- return () => view.dispose();
261
- }, [
262
- bridge,
263
- paneContext.pane.id,
264
- paneContext.document,
265
- options.camera,
266
- options.controls,
267
- options.pipeline,
268
- ]);
269
- return (
270
- <div ref={element} style={{ width: '100%', height: '100%', position: 'relative' }}>
271
- {children}
272
- </div>
273
- );
274
- }
275
-
276
- // Preserve R3F JSX augmentation for consumers importing only quilt-three/react.
277
- export type { ThreeElements, ThreeEvent } from '@react-three/fiber';
278
-
279
- /** Mount inside ThreeSessionProvider, alongside SharedScene (not inside its R3F tree).
280
- * Visibility is reactive; other options are mount-only.
281
- */
282
- export function CameraHelpers({ visible = true, ...options }: CameraHelpersOptions) {
283
- const session = useThreeSession();
284
- const initial = useRef(options);
285
- const instance = useRef<CameraHelpersController | null>(null);
286
- useEffect(() => {
287
- const helpers = new CameraHelpersController(session, { ...initial.current, visible });
288
- instance.current = helpers;
289
- return () => {
290
- helpers.dispose();
291
- instance.current = null;
292
- };
293
- }, [session]);
294
- useEffect(() => {
295
- instance.current?.setVisible(visible);
296
- }, [visible]);
297
- return null;
298
- }