@octanejs/three 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.
@@ -0,0 +1,652 @@
1
+ /**
2
+ * Three root configuration and lifecycle.
3
+ *
4
+ * Component execution remains in Octane's universal runtime. This controller
5
+ * owns only renderer construction, R3F-shaped root state, the Three host
6
+ * container, and the configuration barrier before a scene may render.
7
+ */
8
+ import * as THREE from 'three';
9
+ import {
10
+ defineUniversalComponent,
11
+ universalComponent,
12
+ universalContext,
13
+ useLayoutEffect,
14
+ type RendererRegion,
15
+ type UniversalComponent,
16
+ type UniversalRoot,
17
+ } from 'octane/universal';
18
+ import { applyThreeProps } from './props.js';
19
+ import {
20
+ createThreeContainer,
21
+ createThreeDriver,
22
+ type ThreeHostContainer,
23
+ type ThreeHostEnvironment,
24
+ } from './driver.js';
25
+ import type { EventManager } from './events.js';
26
+ import { advance, invalidate, registerRootStore, unregisterRootStore } from './loop.js';
27
+ import {
28
+ RootStoreContext,
29
+ associateRootObject,
30
+ createRootStore,
31
+ dissociateRootObject,
32
+ destroyRootStore,
33
+ type Camera,
34
+ type Dpr,
35
+ type Frameloop,
36
+ type Performance,
37
+ type Renderer,
38
+ type RootState,
39
+ type RootStore,
40
+ type Size,
41
+ } from './store.js';
42
+ import { createUniversalRoot } from 'octane/universal';
43
+
44
+ export interface CanvasLike {
45
+ width?: number;
46
+ height?: number;
47
+ parentElement?: {
48
+ getBoundingClientRect(): Pick<DOMRect, 'width' | 'height' | 'top' | 'left'>;
49
+ } | null;
50
+ }
51
+
52
+ export type DefaultGLProps<TCanvas extends CanvasLike = CanvasLike> = Omit<
53
+ THREE.WebGLRendererParameters,
54
+ 'canvas'
55
+ > & {
56
+ canvas: TCanvas;
57
+ };
58
+
59
+ export type GLProps<TCanvas extends CanvasLike = CanvasLike> =
60
+ | Renderer
61
+ | ((defaults: DefaultGLProps<TCanvas>) => Renderer | Promise<Renderer>)
62
+ | (Partial<THREE.WebGLRendererParameters> & Record<string, unknown>);
63
+
64
+ export type CameraProps =
65
+ | Camera
66
+ | (Partial<THREE.PerspectiveCamera & THREE.OrthographicCamera> & { manual?: boolean });
67
+
68
+ export interface RenderProps<TCanvas extends CanvasLike = CanvasLike> {
69
+ gl?: GLProps<TCanvas>;
70
+ size?: Partial<Size> & Pick<Size, 'width' | 'height'>;
71
+ shadows?:
72
+ | boolean
73
+ | 'basic'
74
+ | 'percentage'
75
+ | 'soft'
76
+ | 'variance'
77
+ | Partial<NonNullable<Renderer['shadowMap']>>;
78
+ legacy?: boolean;
79
+ linear?: boolean;
80
+ flat?: boolean;
81
+ orthographic?: boolean;
82
+ frameloop?: Frameloop;
83
+ performance?: Partial<Omit<Performance, 'regress'>>;
84
+ dpr?: Dpr;
85
+ raycaster?: Partial<THREE.Raycaster> & { params?: Partial<THREE.Raycaster['params']> };
86
+ scene?: THREE.Scene | Partial<THREE.Scene>;
87
+ camera?: CameraProps;
88
+ /** Event manager installed while the current manager has no native handler table. */
89
+ events?: (store: RootStore) => EventManager<any>;
90
+ onCreated?: (state: RootState) => void;
91
+ /** Response for pointer clicks that miss every target. */
92
+ onPointerMissed?: (event: MouseEvent) => void;
93
+ }
94
+
95
+ export interface ThreeRoot<TCanvas extends CanvasLike = CanvasLike> {
96
+ readonly store: RootStore;
97
+ configure(config?: RenderProps<TCanvas>): Promise<ThreeRoot<TCanvas>>;
98
+ render<P>(component: UniversalComponent<P>, props: P): RootStore;
99
+ unmount(): void;
100
+ }
101
+
102
+ interface PendingRender {
103
+ readonly component: UniversalComponent<any>;
104
+ readonly props: any;
105
+ }
106
+
107
+ interface RootProviderProps {
108
+ readonly store: RootStore;
109
+ readonly onCreated?: (state: RootState) => void;
110
+ readonly eventTarget?: unknown;
111
+ readonly component?: UniversalComponent<any>;
112
+ readonly componentProps?: any;
113
+ readonly region?: RendererRegion<any>;
114
+ }
115
+
116
+ interface ThreeRootInternals<TCanvas extends CanvasLike> {
117
+ readonly canvas: TCanvas;
118
+ readonly store: RootStore;
119
+ readonly controller: ThreeRoot<TCanvas>;
120
+ container: ThreeHostContainer | null;
121
+ hostRoot: UniversalRoot | null;
122
+ rendererPromise: Promise<Renderer> | null;
123
+ configurationQueue: Promise<void>;
124
+ pendingConfigurations: number;
125
+ lastPendingConfig: RenderProps<TCanvas> | null;
126
+ lastPendingPromise: Promise<ThreeRoot<TCanvas>> | null;
127
+ eventsManagedByBoundary: boolean;
128
+ configured: boolean;
129
+ configurationReady: boolean;
130
+ disposed: boolean;
131
+ generation: number;
132
+ pendingRender: PendingRender | null;
133
+ lastConfiguredCamera?: CameraProps;
134
+ onCreated?: (state: RootState) => void;
135
+ }
136
+
137
+ interface RootRecord<TCanvas extends CanvasLike = CanvasLike> {
138
+ readonly store: RootStore;
139
+ readonly root: ThreeRoot<TCanvas>;
140
+ }
141
+
142
+ const roots = new Map<CanvasLike, RootRecord<any>>();
143
+
144
+ const ROOT_PROVIDER_LIFETIME = Symbol('octane.three.root-provider.lifetime');
145
+ const disposedRenderers = new WeakSet<object>();
146
+ const rootInternals = new WeakMap<ThreeRoot<any>, ThreeRootInternals<any>>();
147
+ const DEFAULT_CONFIGURATION = Object.freeze({}) as RenderProps<any>;
148
+
149
+ const RootProvider = defineUniversalComponent<RootProviderProps>('three', (props) => {
150
+ useLayoutEffect(
151
+ () => {
152
+ const state = props.store.getState();
153
+ state.internal.active = true;
154
+ props.onCreated?.(state);
155
+ const events = props.store.getState().events;
156
+ if (props.eventTarget !== undefined && !events.connected) {
157
+ events.connect?.(props.eventTarget);
158
+ }
159
+ state.invalidate();
160
+ return () => {
161
+ state.internal.active = false;
162
+ state.internal.frames = 0;
163
+ };
164
+ },
165
+ [],
166
+ ROOT_PROVIDER_LIFETIME,
167
+ );
168
+ return universalContext(RootStoreContext, props.store, () => {
169
+ if (props.region !== undefined) {
170
+ return universalComponent(
171
+ 'three',
172
+ props.region.component as UniversalComponent<any>,
173
+ props.region.props,
174
+ );
175
+ }
176
+ if (props.component === undefined) {
177
+ return null;
178
+ }
179
+ return universalComponent('three', props.component, props.componentProps);
180
+ });
181
+ });
182
+
183
+ function isRenderer(value: unknown): value is Renderer {
184
+ return typeof (value as Renderer | null)?.render === 'function';
185
+ }
186
+
187
+ function disposeRenderer(renderer: Renderer | null | undefined): void {
188
+ if (renderer == null || typeof renderer !== 'object' || disposedRenderers.has(renderer)) return;
189
+ disposedRenderers.add(renderer);
190
+ try {
191
+ renderer.renderLists?.dispose?.();
192
+ } catch {
193
+ // Renderer cleanup is best effort, matching Three/R3F teardown.
194
+ }
195
+ try {
196
+ renderer.forceContextLoss?.();
197
+ } catch {
198
+ // Renderer cleanup is best effort, matching Three/R3F teardown.
199
+ }
200
+ try {
201
+ renderer.dispose?.();
202
+ } catch {
203
+ // Renderer cleanup is best effort, matching Three/R3F teardown.
204
+ }
205
+ }
206
+
207
+ function computeInitialSize(canvas: CanvasLike, size?: RenderProps<any>['size']): Size {
208
+ if (size !== undefined) {
209
+ return { width: size.width, height: size.height, top: size.top ?? 0, left: size.left ?? 0 };
210
+ }
211
+ if (canvas.parentElement != null) {
212
+ const bounds = canvas.parentElement.getBoundingClientRect();
213
+ return {
214
+ width: bounds.width,
215
+ height: bounds.height,
216
+ top: bounds.top,
217
+ left: bounds.left,
218
+ };
219
+ }
220
+ return {
221
+ width: canvas.width ?? 0,
222
+ height: canvas.height ?? 0,
223
+ top: 0,
224
+ left: 0,
225
+ };
226
+ }
227
+
228
+ function sameSize(left: Size, right: Size): boolean {
229
+ return (
230
+ left.width === right.width &&
231
+ left.height === right.height &&
232
+ left.top === right.top &&
233
+ left.left === right.left
234
+ );
235
+ }
236
+
237
+ function applyRaycasterOptions(
238
+ raycaster: THREE.Raycaster,
239
+ options: RenderProps<any>['raycaster'],
240
+ ): void {
241
+ if (options === undefined) return;
242
+ const { params, ...ordinary } = options;
243
+ applyThreeProps(raycaster, ordinary as Record<string, unknown>);
244
+ if (params !== undefined) {
245
+ raycaster.params = {
246
+ ...raycaster.params,
247
+ ...params,
248
+ Mesh: { ...raycaster.params.Mesh, ...params.Mesh },
249
+ Line: { ...raycaster.params.Line, ...params.Line },
250
+ LOD: { ...raycaster.params.LOD, ...params.LOD },
251
+ Points: { ...raycaster.params.Points, ...params.Points },
252
+ Sprite: { ...raycaster.params.Sprite, ...params.Sprite },
253
+ };
254
+ }
255
+ }
256
+
257
+ function createCamera(orthographic: boolean, options?: CameraProps): Camera {
258
+ if ((options as THREE.Camera | undefined)?.isCamera === true) return options as Camera;
259
+ const camera = (
260
+ orthographic
261
+ ? new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000)
262
+ : new THREE.PerspectiveCamera(75, 0, 0.1, 1000)
263
+ ) as Camera;
264
+ camera.position.z = 5;
265
+ if (options !== undefined) {
266
+ applyThreeProps(camera, options as Record<string, unknown>);
267
+ if (
268
+ camera.manual !== true &&
269
+ ('aspect' in options ||
270
+ 'left' in options ||
271
+ 'right' in options ||
272
+ 'bottom' in options ||
273
+ 'top' in options)
274
+ ) {
275
+ camera.manual = true;
276
+ camera.updateProjectionMatrix();
277
+ }
278
+ }
279
+ if (options == null || !('rotation' in options)) camera.lookAt(0, 0, 0);
280
+ return camera;
281
+ }
282
+
283
+ function configureShadows(renderer: Renderer, shadows: RenderProps<any>['shadows']): void {
284
+ const shadowMap = renderer.shadowMap;
285
+ if (shadowMap === undefined) return;
286
+ const oldEnabled = shadowMap.enabled;
287
+ const oldType = shadowMap.type;
288
+ shadowMap.enabled = Boolean(shadows);
289
+ if (typeof shadows === 'boolean') {
290
+ if (shadows) shadowMap.type = THREE.PCFSoftShadowMap;
291
+ } else if (typeof shadows === 'string') {
292
+ const types = {
293
+ basic: THREE.BasicShadowMap,
294
+ percentage: THREE.PCFShadowMap,
295
+ soft: THREE.PCFSoftShadowMap,
296
+ variance: THREE.VSMShadowMap,
297
+ };
298
+ shadowMap.type = types[shadows] ?? THREE.PCFSoftShadowMap;
299
+ } else if (shadows != null) {
300
+ Object.assign(shadowMap, shadows);
301
+ }
302
+ if (oldEnabled !== shadowMap.enabled || oldType !== shadowMap.type) shadowMap.needsUpdate = true;
303
+ }
304
+
305
+ function ensureHostRoot(internals: ThreeRootInternals<any>): void {
306
+ if (internals.hostRoot !== null) return;
307
+ const state = internals.store.getState();
308
+ let hostRoot: UniversalRoot | null = null;
309
+ const environment: ThreeHostEnvironment = {
310
+ store: internals.store,
311
+ invalidate: () => internals.store.getState().invalidate(),
312
+ eventScope(priority, run) {
313
+ const root = hostRoot;
314
+ if (root === null) {
315
+ throw new Error('@octanejs/three: Event dispatch started before the host root existed.');
316
+ }
317
+ return root.eventScope(priority, run);
318
+ },
319
+ dispatchEvent(listener, payload) {
320
+ if (hostRoot === null) {
321
+ throw new Error('@octanejs/three: Event dispatch started before the host root existed.');
322
+ }
323
+ return hostRoot.dispatchEvent(listener, payload);
324
+ },
325
+ get linear() {
326
+ return internals.store.getState().linear;
327
+ },
328
+ };
329
+ const container = createThreeContainer({ scene: state.scene, environment });
330
+ hostRoot = createUniversalRoot(container, createThreeDriver());
331
+ internals.container = container;
332
+ internals.hostRoot = hostRoot;
333
+ }
334
+
335
+ async function ensureRenderer<TCanvas extends CanvasLike>(
336
+ internals: ThreeRootInternals<TCanvas>,
337
+ glConfig: GLProps<TCanvas> | undefined,
338
+ generation: number,
339
+ ): Promise<Renderer | null> {
340
+ const state = internals.store.getState();
341
+ if (state.gl != null) return state.gl;
342
+ if (internals.rendererPromise === null) {
343
+ const defaults = {
344
+ canvas: internals.canvas,
345
+ powerPreference: 'high-performance',
346
+ antialias: true,
347
+ alpha: true,
348
+ } as DefaultGLProps<TCanvas>;
349
+ internals.rendererPromise = Promise.resolve(
350
+ typeof glConfig === 'function' ? glConfig(defaults) : glConfig,
351
+ ).then((custom) => {
352
+ if (isRenderer(custom)) return custom;
353
+ return new THREE.WebGLRenderer({
354
+ ...defaults,
355
+ ...(glConfig as Partial<THREE.WebGLRendererParameters> | undefined),
356
+ } as THREE.WebGLRendererParameters) as unknown as Renderer;
357
+ });
358
+ }
359
+ const pendingRenderer = internals.rendererPromise;
360
+ let renderer: Renderer;
361
+ try {
362
+ renderer = await pendingRenderer;
363
+ } catch (error) {
364
+ // A rejected async factory must not poison the root permanently. Keep the
365
+ // identity guard so a late rejection can never clear a newer attempt.
366
+ if (internals.rendererPromise === pendingRenderer) internals.rendererPromise = null;
367
+ throw error;
368
+ }
369
+ if (internals.disposed || internals.generation !== generation) {
370
+ disposeRenderer(renderer);
371
+ return null;
372
+ }
373
+ if (internals.store.getState().gl == null) {
374
+ internals.store.setState({ gl: renderer, renderer });
375
+ }
376
+ return renderer;
377
+ }
378
+
379
+ async function applyConfiguration<TCanvas extends CanvasLike>(
380
+ internals: ThreeRootInternals<TCanvas>,
381
+ props: RenderProps<TCanvas>,
382
+ generation: number,
383
+ ): Promise<void> {
384
+ if (internals.disposed || internals.generation !== generation) return;
385
+ const {
386
+ gl: glConfig,
387
+ size: configuredSize,
388
+ scene: sceneOptions,
389
+ events: eventFactory,
390
+ onPointerMissed,
391
+ onCreated,
392
+ shadows = false,
393
+ linear = false,
394
+ flat = false,
395
+ legacy = false,
396
+ orthographic = false,
397
+ frameloop = 'always',
398
+ dpr = [1, 2],
399
+ performance,
400
+ raycaster: raycasterOptions,
401
+ camera: cameraOptions,
402
+ } = props;
403
+ const renderer = await ensureRenderer(internals, glConfig, generation);
404
+ if (renderer === null || internals.disposed || internals.generation !== generation) return;
405
+
406
+ let state = internals.store.getState();
407
+ if (state.raycaster == null) internals.store.setState({ raycaster: new THREE.Raycaster() });
408
+ state = internals.store.getState();
409
+ applyRaycasterOptions(state.raycaster, raycasterOptions);
410
+
411
+ if (
412
+ state.camera == null ||
413
+ (state.camera === internals.lastConfiguredCamera &&
414
+ cameraOptions !== internals.lastConfiguredCamera)
415
+ ) {
416
+ internals.lastConfiguredCamera = cameraOptions;
417
+ const camera = createCamera(orthographic, cameraOptions);
418
+ state.raycaster.camera = camera;
419
+ internals.store.setState({ camera });
420
+ }
421
+ state = internals.store.getState();
422
+ if (state.scene == null) {
423
+ const scene =
424
+ (sceneOptions as THREE.Scene | undefined)?.isScene === true
425
+ ? (sceneOptions as THREE.Scene)
426
+ : new THREE.Scene();
427
+ if (sceneOptions !== undefined && scene !== sceneOptions) {
428
+ applyThreeProps(scene, sceneOptions as Record<string, unknown>);
429
+ }
430
+ internals.store.setState({ scene });
431
+ }
432
+ associateRootObject(internals.store.getState().scene, internals.store);
433
+ ensureHostRoot(internals);
434
+ state = internals.store.getState();
435
+ if (eventFactory !== undefined && state.events.handlers === undefined) {
436
+ const previousEvents = state.events;
437
+ const previousTarget = previousEvents.connected;
438
+ const manager = eventFactory(internals.store);
439
+ if (previousTarget) previousEvents.disconnect?.();
440
+ internals.store.setState({ events: manager });
441
+ state = internals.store.getState();
442
+ if (previousTarget && !state.events.connected) {
443
+ state.events.connect?.(previousTarget);
444
+ state = internals.store.getState();
445
+ }
446
+ }
447
+ if (state.onPointerMissed !== onPointerMissed) {
448
+ internals.store.setState({ onPointerMissed });
449
+ state = internals.store.getState();
450
+ }
451
+ if (state.internal.active && !internals.eventsManagedByBoundary && !state.events.connected) {
452
+ state.events.connect?.(internals.canvas);
453
+ }
454
+
455
+ const size = computeInitialSize(internals.canvas, configuredSize);
456
+ if (!sameSize(size, state.size)) state.setSize(size.width, size.height, size.top, size.left);
457
+ state = internals.store.getState();
458
+ state.setDpr(dpr);
459
+ state = internals.store.getState();
460
+ if (state.frameloop !== frameloop) state.setFrameloop(frameloop);
461
+ if (performance !== undefined) {
462
+ internals.store.setState((current) => ({
463
+ performance: { ...current.performance, ...performance },
464
+ }));
465
+ }
466
+
467
+ configureShadows(renderer, shadows);
468
+ THREE.ColorManagement.enabled = !legacy;
469
+ if (!internals.configured) {
470
+ renderer.outputColorSpace = linear ? THREE.LinearSRGBColorSpace : THREE.SRGBColorSpace;
471
+ renderer.toneMapping = flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping;
472
+ }
473
+ state = internals.store.getState();
474
+ if (state.legacy !== legacy || state.linear !== linear || state.flat !== flat) {
475
+ internals.store.setState({ legacy, linear, flat });
476
+ }
477
+ if (glConfig != null && typeof glConfig !== 'function' && !isRenderer(glConfig)) {
478
+ applyThreeProps(renderer, glConfig as Record<string, unknown>);
479
+ }
480
+ internals.onCreated = onCreated;
481
+ internals.configured = true;
482
+ }
483
+
484
+ function performRender(internals: ThreeRootInternals<any>, pending: PendingRender): void {
485
+ if (internals.disposed || !internals.configured || internals.hostRoot === null) return;
486
+ internals.pendingRender = null;
487
+ internals.hostRoot.render(RootProvider, {
488
+ store: internals.store,
489
+ onCreated: internals.onCreated,
490
+ eventTarget: internals.canvas,
491
+ component: pending.component,
492
+ componentProps: pending.props,
493
+ });
494
+ }
495
+
496
+ function reportAsyncError(error: unknown): void {
497
+ queueMicrotask(() => {
498
+ throw error;
499
+ });
500
+ }
501
+
502
+ export function createRoot<TCanvas extends CanvasLike>(canvas: TCanvas): ThreeRoot<TCanvas> {
503
+ const previous = roots.get(canvas) as RootRecord<TCanvas> | undefined;
504
+ if (previous !== undefined) {
505
+ console.warn('@octanejs/three: createRoot should only be called once for a canvas.');
506
+ return previous.root;
507
+ }
508
+ const store = createRootStore(invalidate, advance);
509
+ let internals!: ThreeRootInternals<TCanvas>;
510
+ const controller: ThreeRoot<TCanvas> = {
511
+ store,
512
+ configure(config = DEFAULT_CONFIGURATION): Promise<ThreeRoot<TCanvas>> {
513
+ if (internals.disposed) {
514
+ return Promise.reject(new Error('@octanejs/three: Cannot configure an unmounted root.'));
515
+ }
516
+ if (
517
+ internals.pendingConfigurations > 0 &&
518
+ internals.lastPendingConfig === config &&
519
+ internals.lastPendingPromise !== null
520
+ ) {
521
+ return internals.lastPendingPromise;
522
+ }
523
+ const generation = internals.generation;
524
+ internals.configurationReady = false;
525
+ internals.pendingConfigurations++;
526
+ const work = internals.configurationQueue.then(() =>
527
+ applyConfiguration(internals, config, generation),
528
+ );
529
+ internals.configurationQueue = work.catch(() => {});
530
+ let succeeded = false;
531
+ const result = work
532
+ .then(() => {
533
+ succeeded = true;
534
+ })
535
+ .finally(() => {
536
+ internals.pendingConfigurations--;
537
+ if (internals.lastPendingConfig === config) {
538
+ internals.lastPendingConfig = null;
539
+ internals.lastPendingPromise = null;
540
+ }
541
+ if (internals.pendingConfigurations === 0) {
542
+ internals.configurationReady = succeeded;
543
+ if (succeeded && internals.pendingRender !== null && !internals.disposed) {
544
+ performRender(internals, internals.pendingRender);
545
+ }
546
+ }
547
+ }) as Promise<void>;
548
+ const publicResult = result.then(() => controller);
549
+ internals.lastPendingConfig = config;
550
+ internals.lastPendingPromise = publicResult;
551
+ return publicResult;
552
+ },
553
+ render<P>(component: UniversalComponent<P>, props: P): RootStore {
554
+ if (internals.disposed) {
555
+ throw new Error('@octanejs/three: Cannot render an unmounted root.');
556
+ }
557
+ const pending: PendingRender = { component, props };
558
+ internals.pendingRender = pending;
559
+ if (internals.configurationReady && internals.pendingConfigurations === 0) {
560
+ performRender(internals, pending);
561
+ } else if (internals.pendingConfigurations === 0) {
562
+ void controller.configure().catch(reportAsyncError);
563
+ }
564
+ return store;
565
+ },
566
+ unmount(): void {
567
+ if (internals.disposed) return;
568
+ internals.disposed = true;
569
+ internals.generation++;
570
+ internals.pendingRender = null;
571
+ const state = store.getState();
572
+ if (state.scene != null) dissociateRootObject(state.scene, store);
573
+ state.internal.active = false;
574
+ state.internal.frames = 0;
575
+ unregisterRootStore(store);
576
+ try {
577
+ state.events.disconnect?.();
578
+ } finally {
579
+ try {
580
+ internals.hostRoot?.unmount();
581
+ } finally {
582
+ internals.container?.flushDisposals();
583
+ try {
584
+ state.xr.disconnect();
585
+ } finally {
586
+ disposeRenderer(state.gl);
587
+ destroyRootStore(store);
588
+ roots.delete(canvas);
589
+ rootInternals.delete(controller);
590
+ }
591
+ }
592
+ }
593
+ },
594
+ };
595
+ internals = {
596
+ canvas,
597
+ store,
598
+ controller,
599
+ container: null,
600
+ hostRoot: null,
601
+ rendererPromise: null,
602
+ configurationQueue: Promise.resolve(),
603
+ pendingConfigurations: 0,
604
+ lastPendingConfig: null,
605
+ lastPendingPromise: null,
606
+ eventsManagedByBoundary: false,
607
+ configured: false,
608
+ configurationReady: false,
609
+ disposed: false,
610
+ generation: 0,
611
+ pendingRender: null,
612
+ lastConfiguredCamera: undefined,
613
+ };
614
+ rootInternals.set(controller, internals);
615
+ roots.set(canvas, { store, root: controller });
616
+ registerRootStore(store);
617
+ return controller;
618
+ }
619
+
620
+ export function unmountComponentAtNode<TCanvas extends CanvasLike>(canvas: TCanvas): void {
621
+ roots.get(canvas)?.root.unmount();
622
+ }
623
+
624
+ export interface ThreeBoundaryMount {
625
+ readonly root: UniversalRoot;
626
+ readonly component: typeof RootProvider;
627
+ readonly props: RootProviderProps;
628
+ }
629
+
630
+ /** Internal Canvas adapter that retains DOM boundary error/context ownership. */
631
+ export function createThreeBoundaryMount(
632
+ root: ThreeRoot,
633
+ region?: RendererRegion,
634
+ ): ThreeBoundaryMount {
635
+ const internals = rootInternals.get(root);
636
+ if (internals === undefined || internals.disposed) {
637
+ throw new Error('@octanejs/three: Cannot mount a boundary for an unmounted root.');
638
+ }
639
+ if (!internals.configurationReady || internals.hostRoot === null) {
640
+ throw new Error('@octanejs/three: Canvas children cannot mount before configure() settles.');
641
+ }
642
+ internals.eventsManagedByBoundary = true;
643
+ return {
644
+ root: internals.hostRoot,
645
+ component: RootProvider,
646
+ props: {
647
+ store: internals.store,
648
+ onCreated: internals.onCreated,
649
+ region,
650
+ },
651
+ };
652
+ }