@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,741 @@
1
+ /**
2
+ * Renderer-neutral root state for the Three host.
3
+ *
4
+ * The state shape follows React Three Fiber 9.6.1, but the callable binding is
5
+ * built on Octane's universal hooks so it is safe inside `*.three.tsrx`
6
+ * components. Zustand remains the framework-neutral storage primitive.
7
+ */
8
+ import * as THREE from 'three';
9
+ import { createContext, useRef, useSyncExternalStore, withSlot } from 'octane/universal';
10
+ import { createStore as createVanillaStore, type StoreApi } from 'zustand/vanilla';
11
+ import type {
12
+ DomEvent,
13
+ EventManager,
14
+ Intersection,
15
+ PointerCaptureTarget,
16
+ ThreeEvent,
17
+ } from './events.js';
18
+
19
+ export type { EventManager } from './events.js';
20
+
21
+ export type Dpr = number | readonly [min: number, max: number];
22
+
23
+ export interface Size {
24
+ width: number;
25
+ height: number;
26
+ top: number;
27
+ left: number;
28
+ }
29
+
30
+ export type Frameloop = 'always' | 'demand' | 'never';
31
+
32
+ export interface Viewport extends Size {
33
+ /** The first configured pixel ratio. */
34
+ initialDpr: number;
35
+ /** The current pixel ratio. */
36
+ dpr: number;
37
+ /** Pixels per viewport unit. */
38
+ factor: number;
39
+ /** Camera distance from the viewport target. */
40
+ distance: number;
41
+ /** Pixel width divided by pixel height. */
42
+ aspect: number;
43
+ getCurrentViewport(
44
+ camera?: Camera,
45
+ target?: THREE.Vector3 | Parameters<THREE.Vector3['set']>,
46
+ size?: Size,
47
+ ): Omit<Viewport, 'dpr' | 'initialDpr' | 'getCurrentViewport'>;
48
+ }
49
+
50
+ export type Camera = (THREE.OrthographicCamera | THREE.PerspectiveCamera) & {
51
+ manual?: boolean;
52
+ };
53
+
54
+ /** Minimum renderer contract used by the root and frame loop. */
55
+ export interface Renderer {
56
+ render(scene: THREE.Scene, camera: THREE.Camera): unknown;
57
+ setPixelRatio?(dpr: number): void;
58
+ setSize?(width: number, height: number, updateStyle?: boolean): void;
59
+ dispose?(): void;
60
+ forceContextLoss?(): void;
61
+ domElement?: unknown;
62
+ renderLists?: { dispose?(): void };
63
+ shadowMap?: {
64
+ enabled: boolean;
65
+ type: THREE.ShadowMapType;
66
+ needsUpdate?: boolean;
67
+ [key: string]: unknown;
68
+ };
69
+ xr?: {
70
+ enabled?: boolean;
71
+ isPresenting?: boolean;
72
+ setAnimationLoop?(callback: ((timestamp: number, frame?: XRFrame) => void) | null): void;
73
+ addEventListener?(type: string, callback: () => void): void;
74
+ removeEventListener?(type: string, callback: () => void): void;
75
+ };
76
+ outputColorSpace?: THREE.ColorSpace;
77
+ toneMapping?: THREE.ToneMapping;
78
+ [key: string]: any;
79
+ }
80
+
81
+ export interface XRManager {
82
+ connect(): void;
83
+ disconnect(): void;
84
+ }
85
+
86
+ export type RenderCallback = (state: RootState, delta: number, frame?: XRFrame) => void;
87
+
88
+ export interface Subscription {
89
+ ref: { current: RenderCallback };
90
+ priority: number;
91
+ store: RootStore;
92
+ }
93
+
94
+ export interface Performance {
95
+ current: number;
96
+ min: number;
97
+ max: number;
98
+ debounce: number;
99
+ regress(): void;
100
+ }
101
+
102
+ export interface InternalState {
103
+ interaction: THREE.Object3D[];
104
+ hovered: Map<string, ThreeEvent<DomEvent>>;
105
+ subscribers: Subscription[];
106
+ capturedMap: Map<number, Map<THREE.Object3D, PointerCaptureTarget>>;
107
+ initialClick: [x: number, y: number];
108
+ initialHits: THREE.Object3D[];
109
+ lastEvent: { current: DomEvent | null };
110
+ active: boolean;
111
+ priority: number;
112
+ frames: number;
113
+ subscribe(ref: { current: RenderCallback }, priority: number, store: RootStore): () => void;
114
+ }
115
+
116
+ export interface RootState {
117
+ set: StoreApi<RootState>['setState'];
118
+ get: StoreApi<RootState>['getState'];
119
+ gl: Renderer;
120
+ /** Future-facing name for the active rendering object. */
121
+ renderer: Renderer;
122
+ camera: Camera;
123
+ scene: THREE.Scene;
124
+ raycaster: THREE.Raycaster;
125
+ clock: THREE.Clock;
126
+ events: EventManager<any>;
127
+ xr: XRManager;
128
+ controls: THREE.EventDispatcher | null;
129
+ pointer: THREE.Vector2;
130
+ /** @deprecated Use `pointer`. */
131
+ mouse: THREE.Vector2;
132
+ legacy: boolean;
133
+ linear: boolean;
134
+ flat: boolean;
135
+ frameloop: Frameloop;
136
+ performance: Performance;
137
+ size: Size;
138
+ viewport: Viewport;
139
+ invalidate(frames?: number): void;
140
+ advance(timestamp: number, runGlobalEffects?: boolean): void;
141
+ setEvents(events: Partial<EventManager<any>>): void;
142
+ setSize(width: number, height: number, top?: number, left?: number): void;
143
+ setDpr(dpr: Dpr): void;
144
+ setFrameloop(frameloop?: Frameloop): void;
145
+ onPointerMissed?: (event: MouseEvent) => void;
146
+ previousRoot?: RootStore;
147
+ internal: InternalState;
148
+ }
149
+
150
+ /**
151
+ * R3F-shaped root store.
152
+ *
153
+ * The callable selector form is retained for upstream compatibility, but a
154
+ * call through a dynamic value cannot receive an Octane compiler slot. Keep
155
+ * such calls unconditional and ordered, or prefer compiler-visible
156
+ * `useStore(selector)` / `useThree(selector)` in authored scene components.
157
+ */
158
+ export type RootStore = {
159
+ (): RootState;
160
+ <T>(selector: (state: RootState) => T, equalityFn?: (previous: T, next: T) => boolean): T;
161
+ } & StoreApi<RootState>;
162
+
163
+ type Invalidate = (state?: RootState, frames?: number) => void;
164
+ type Advance = (
165
+ timestamp: number,
166
+ runGlobalEffects?: boolean,
167
+ state?: RootState,
168
+ frame?: XRFrame,
169
+ ) => void;
170
+
171
+ const identity = <T>(value: T): T => value;
172
+ const STORE_CLEANUPS = new WeakMap<RootStore, () => void>();
173
+ const ROOT_OBJECT_STORES = new WeakMap<object, RootStore>();
174
+
175
+ interface PointerCaptureIdentity {
176
+ eventObject: THREE.Object3D;
177
+ intersection: Intersection;
178
+ pointerIds: Set<number>;
179
+ }
180
+
181
+ const POINTER_CAPTURE_IDENTITIES = new WeakMap<object, PointerCaptureIdentity>();
182
+ const ACTIVE_POINTER_CAPTURE_IDENTITIES = new WeakMap<
183
+ THREE.Object3D,
184
+ Set<PointerCaptureIdentity>
185
+ >();
186
+ const POINTER_CAPTURE_TARGET_IDENTITIES = new WeakMap<
187
+ PointerCaptureTarget,
188
+ PointerCaptureIdentity
189
+ >();
190
+
191
+ /** Create the mutable identity cell used by an event's pointer-capture facade. */
192
+ export function createPointerCaptureIdentity(intersection: Intersection): PointerCaptureIdentity {
193
+ return { eventObject: intersection.eventObject, intersection, pointerIds: new Set() };
194
+ }
195
+
196
+ /** Associate a public capture facade with its renderer-owned identity cell. */
197
+ export function registerPointerCaptureFacade(
198
+ facade: object,
199
+ identity: PointerCaptureIdentity,
200
+ ): void {
201
+ POINTER_CAPTURE_IDENTITIES.set(facade, identity);
202
+ }
203
+
204
+ export function makeIntersectionId(
205
+ event: Pick<Intersection, 'eventObject' | 'object' | 'index' | 'instanceId'>,
206
+ ): string {
207
+ return `${(event.eventObject ?? event.object).uuid}/${event.index}/${event.instanceId}`;
208
+ }
209
+
210
+ function replaceIntersectionObject(
211
+ intersection: Intersection,
212
+ previous: THREE.Object3D,
213
+ next: THREE.Object3D,
214
+ ): Intersection {
215
+ const object = intersection.object === previous ? next : intersection.object;
216
+ const eventObject = intersection.eventObject === previous ? next : intersection.eventObject;
217
+ if (object === intersection.object && eventObject === intersection.eventObject)
218
+ return intersection;
219
+ return { ...intersection, object, eventObject };
220
+ }
221
+
222
+ function pointerCaptureIdentityObjects(identity: PointerCaptureIdentity): Set<THREE.Object3D> {
223
+ return new Set([identity.eventObject, identity.intersection.object]);
224
+ }
225
+
226
+ function indexPointerCaptureIdentity(identity: PointerCaptureIdentity): void {
227
+ if (identity.pointerIds.size === 0) return;
228
+ for (const object of pointerCaptureIdentityObjects(identity)) {
229
+ const identities = ACTIVE_POINTER_CAPTURE_IDENTITIES.get(object);
230
+ if (identities === undefined) {
231
+ ACTIVE_POINTER_CAPTURE_IDENTITIES.set(object, new Set([identity]));
232
+ } else {
233
+ identities.add(identity);
234
+ }
235
+ }
236
+ }
237
+
238
+ function unindexPointerCaptureIdentity(identity: PointerCaptureIdentity): void {
239
+ for (const object of pointerCaptureIdentityObjects(identity)) {
240
+ const identities = ACTIVE_POINTER_CAPTURE_IDENTITIES.get(object);
241
+ if (identities === undefined) continue;
242
+ identities.delete(identity);
243
+ if (identities.size === 0) ACTIVE_POINTER_CAPTURE_IDENTITIES.delete(object);
244
+ }
245
+ }
246
+
247
+ function activatePointerCaptureIdentity(
248
+ capture: PointerCaptureTarget,
249
+ identity: PointerCaptureIdentity,
250
+ pointerId: number,
251
+ ): void {
252
+ POINTER_CAPTURE_TARGET_IDENTITIES.set(capture, identity);
253
+ if (identity.pointerIds.has(pointerId)) return;
254
+ identity.pointerIds.add(pointerId);
255
+ if (identity.pointerIds.size === 1) indexPointerCaptureIdentity(identity);
256
+ }
257
+
258
+ function deactivatePointerCaptureIdentity(capture: PointerCaptureTarget, pointerId: number): void {
259
+ const identity = POINTER_CAPTURE_TARGET_IDENTITIES.get(capture);
260
+ if (identity === undefined || !identity.pointerIds.delete(pointerId)) return;
261
+ if (identity.pointerIds.size === 0) unindexPointerCaptureIdentity(identity);
262
+ }
263
+
264
+ function transferPointerCaptureIdentity(
265
+ identity: PointerCaptureIdentity,
266
+ previous: THREE.Object3D,
267
+ next: THREE.Object3D,
268
+ ): void {
269
+ const intersection = replaceIntersectionObject(identity.intersection, previous, next);
270
+ const eventObject = identity.eventObject === previous ? next : identity.eventObject;
271
+ if (intersection === identity.intersection && eventObject === identity.eventObject) return;
272
+
273
+ const active = identity.pointerIds.size > 0;
274
+ if (active) unindexPointerCaptureIdentity(identity);
275
+ identity.eventObject = eventObject;
276
+ identity.intersection = intersection;
277
+ if (active) indexPointerCaptureIdentity(identity);
278
+ }
279
+
280
+ function transferActivePointerCaptureIdentities(
281
+ previous: THREE.Object3D,
282
+ next: THREE.Object3D,
283
+ ): void {
284
+ const identities = ACTIVE_POINTER_CAPTURE_IDENTITIES.get(previous);
285
+ if (identities === undefined) return;
286
+ for (const identity of [...identities]) {
287
+ transferPointerCaptureIdentity(identity, previous, next);
288
+ }
289
+ }
290
+
291
+ function transferPointerCaptureEventIdentity(
292
+ event: ThreeEvent<DomEvent>,
293
+ previous: THREE.Object3D,
294
+ next: THREE.Object3D,
295
+ ): void {
296
+ for (const facade of [event.target, event.currentTarget]) {
297
+ const identity = POINTER_CAPTURE_IDENTITIES.get(facade);
298
+ if (identity === undefined) continue;
299
+ transferPointerCaptureIdentity(identity, previous, next);
300
+ }
301
+ }
302
+
303
+ export function setInternalPointerCapture(
304
+ capturedMap: Map<number, Map<THREE.Object3D, PointerCaptureTarget>>,
305
+ object: THREE.Object3D,
306
+ capture: PointerCaptureTarget,
307
+ identity: PointerCaptureIdentity,
308
+ pointerId: number,
309
+ ): void {
310
+ let captures = capturedMap.get(pointerId);
311
+ if (captures === undefined) {
312
+ captures = new Map();
313
+ capturedMap.set(pointerId, captures);
314
+ }
315
+ const previous = captures.get(object);
316
+ if (previous !== undefined) deactivatePointerCaptureIdentity(previous, pointerId);
317
+ captures.set(object, capture);
318
+ activatePointerCaptureIdentity(capture, identity, pointerId);
319
+ }
320
+
321
+ export function releaseInternalPointerCapture(
322
+ capturedMap: Map<number, Map<THREE.Object3D, PointerCaptureTarget>>,
323
+ object: THREE.Object3D,
324
+ captures: Map<THREE.Object3D, PointerCaptureTarget>,
325
+ pointerId: number,
326
+ ): void {
327
+ const capture = captures.get(object);
328
+ if (capture === undefined) return;
329
+ deactivatePointerCaptureIdentity(capture, pointerId);
330
+ captures.delete(object);
331
+ if (captures.size !== 0) return;
332
+ capturedMap.delete(pointerId);
333
+ capture.target.releasePointerCapture(pointerId);
334
+ }
335
+
336
+ export function clearInternalPointerCaptures(
337
+ capturedMap: Map<number, Map<THREE.Object3D, PointerCaptureTarget>>,
338
+ pointerId: number,
339
+ ): void {
340
+ const captures = capturedMap.get(pointerId);
341
+ if (captures === undefined) return;
342
+ for (const capture of captures.values()) {
343
+ deactivatePointerCaptureIdentity(capture, pointerId);
344
+ }
345
+ capturedMap.delete(pointerId);
346
+ }
347
+
348
+ /** Transfer every observable interaction identity during an accepted reconstruction. */
349
+ export function swapInteractivity(
350
+ store: RootStore,
351
+ previous: THREE.Object3D,
352
+ next: THREE.Object3D,
353
+ ): void {
354
+ const { internal } = store.getState();
355
+ for (let index = 0; index < internal.interaction.length; index++) {
356
+ if (internal.interaction[index] === previous) internal.interaction[index] = next;
357
+ }
358
+ for (let index = 0; index < internal.initialHits.length; index++) {
359
+ if (internal.initialHits[index] === previous) internal.initialHits[index] = next;
360
+ }
361
+ transferActivePointerCaptureIdentities(previous, next);
362
+ for (const [key, hovered] of [...internal.hovered]) {
363
+ const replaced = replaceIntersectionObject(hovered, previous, next);
364
+ const intersections = hovered.intersections.map((intersection) =>
365
+ replaceIntersectionObject(intersection, previous, next),
366
+ );
367
+ const nestedChanged = intersections.some(
368
+ (intersection, index) => intersection !== hovered.intersections[index],
369
+ );
370
+ if (replaced === hovered && !nestedChanged) continue;
371
+
372
+ transferPointerCaptureEventIdentity(hovered, previous, next);
373
+ const nextHovered = { ...replaced, intersections } as ThreeEvent<DomEvent>;
374
+ internal.hovered.delete(key);
375
+ internal.hovered.set(makeIntersectionId(nextHovered), nextHovered);
376
+ }
377
+ for (const [pointerId, captures] of internal.capturedMap) {
378
+ for (const [eventObject, capture] of [...captures]) {
379
+ const nextEventObject = eventObject === previous ? next : eventObject;
380
+ const intersection = replaceIntersectionObject(capture.intersection, previous, next);
381
+ if (nextEventObject === eventObject && intersection === capture.intersection) continue;
382
+
383
+ const nextCapture = { ...capture, intersection };
384
+ const identity = POINTER_CAPTURE_TARGET_IDENTITIES.get(capture);
385
+ if (identity !== undefined) POINTER_CAPTURE_TARGET_IDENTITIES.set(nextCapture, identity);
386
+ captures.delete(eventObject);
387
+ const collision = captures.get(nextEventObject);
388
+ if (collision !== undefined) deactivatePointerCaptureIdentity(collision, pointerId);
389
+ captures.set(nextEventObject, nextCapture);
390
+ }
391
+ }
392
+ }
393
+
394
+ /** Remove all interaction state for an object that left the accepted host tree. */
395
+ export function removeInteractivity(store: RootStore, object: THREE.Object3D): void {
396
+ const { internal } = store.getState();
397
+ internal.interaction = internal.interaction.filter((candidate) => candidate !== object);
398
+ internal.initialHits = internal.initialHits.filter((candidate) => candidate !== object);
399
+ for (const [key, hovered] of internal.hovered) {
400
+ if (hovered.eventObject === object || hovered.object === object) internal.hovered.delete(key);
401
+ }
402
+ for (const [pointerId, captures] of internal.capturedMap) {
403
+ for (const [eventObject, capture] of [...captures]) {
404
+ if (
405
+ eventObject === object ||
406
+ capture.intersection.object === object ||
407
+ capture.intersection.eventObject === object
408
+ ) {
409
+ releaseInternalPointerCapture(internal.capturedMap, eventObject, captures, pointerId);
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ export function associateRootObject(object: object, store: RootStore): void {
416
+ ROOT_OBJECT_STORES.set(object, store);
417
+ }
418
+
419
+ export function dissociateRootObject(object: object, store: RootStore): void {
420
+ if (ROOT_OBJECT_STORES.get(object) === store) ROOT_OBJECT_STORES.delete(object);
421
+ }
422
+
423
+ export function getRootObjectStore(object: object): RootStore | undefined {
424
+ return ROOT_OBJECT_STORES.get(object);
425
+ }
426
+
427
+ function isOrthographicCamera(camera: Camera): camera is THREE.OrthographicCamera & {
428
+ manual?: boolean;
429
+ } {
430
+ return (camera as THREE.OrthographicCamera | null)?.isOrthographicCamera === true;
431
+ }
432
+
433
+ export function calculateDpr(dpr: Dpr): number {
434
+ const target = typeof window === 'undefined' ? 1 : (window.devicePixelRatio ?? 2);
435
+ return typeof dpr === 'number' ? dpr : Math.min(Math.max(dpr[0], target), dpr[1]);
436
+ }
437
+
438
+ export function updateCamera(camera: Camera | null | undefined, size: Size): void {
439
+ if (camera == null || camera.manual === true) return;
440
+ if (isOrthographicCamera(camera)) {
441
+ camera.left = size.width / -2;
442
+ camera.right = size.width / 2;
443
+ camera.top = size.height / 2;
444
+ camera.bottom = size.height / -2;
445
+ } else {
446
+ camera.aspect = size.height === 0 ? 0 : size.width / size.height;
447
+ }
448
+ camera.updateProjectionMatrix();
449
+ }
450
+
451
+ interface SelectionCell<T> {
452
+ initialized: boolean;
453
+ value: T;
454
+ }
455
+
456
+ /** Universal-hook selector used by the callable store and `useThree`. */
457
+ export function useRootStoreSelector<T>(
458
+ store: RootStore,
459
+ selector: (state: RootState) => T,
460
+ equalityFn: (previous: T, next: T) => boolean = Object.is,
461
+ slot?: unknown,
462
+ ): T {
463
+ const run = (nested: boolean): T => {
464
+ const cell = nested
465
+ ? useRef<SelectionCell<T>>({ initialized: false, value: undefined as T }, 'selection')
466
+ : useRef<SelectionCell<T>>({ initialized: false, value: undefined as T });
467
+ const snapshot = () => {
468
+ const next = selector(store.getState());
469
+ if (!cell.current.initialized || !equalityFn(cell.current.value, next)) {
470
+ cell.current = { initialized: true, value: next };
471
+ }
472
+ return cell.current.value;
473
+ };
474
+ return nested
475
+ ? useSyncExternalStore(store.subscribe, snapshot, snapshot, 'subscription')
476
+ : useSyncExternalStore(store.subscribe, snapshot, snapshot);
477
+ };
478
+ return slot === undefined ? run(false) : withSlot(slot, () => run(true));
479
+ }
480
+
481
+ function bindRootStore(api: StoreApi<RootState>): RootStore {
482
+ let store!: RootStore;
483
+ const bound = ((...args: unknown[]) => {
484
+ const tail = args.at(-1);
485
+ const userArgs = typeof tail === 'symbol' ? args.slice(0, -1) : args;
486
+ const selector = (typeof userArgs[0] === 'function' ? userArgs[0] : identity) as (
487
+ state: RootState,
488
+ ) => unknown;
489
+ const equalityFn = (typeof userArgs[1] === 'function' ? userArgs[1] : Object.is) as (
490
+ previous: unknown,
491
+ next: unknown,
492
+ ) => boolean;
493
+ return useRootStoreSelector(
494
+ store,
495
+ selector,
496
+ equalityFn,
497
+ typeof tail === 'symbol' ? tail : undefined,
498
+ );
499
+ }) as RootStore;
500
+ Object.assign(bound, api);
501
+ store = bound;
502
+ return store;
503
+ }
504
+
505
+ /** Create an isolated callable store whose initial state is supplied by a portal layer. */
506
+ export function createPortalStore(initialState: RootState): RootStore {
507
+ let store!: RootStore;
508
+ const api = createVanillaStore<RootState>()((set, get) => ({
509
+ ...initialState,
510
+ set,
511
+ get,
512
+ }));
513
+ store = bindRootStore(api);
514
+ return store;
515
+ }
516
+
517
+ /** Walk a portal chain to the one root that owns scheduling and native events. */
518
+ export function getInitialRootStore(store: RootStore): RootStore {
519
+ const seen = new Set<RootStore>();
520
+ let current = store;
521
+ while (current.getState().previousRoot !== undefined) {
522
+ if (seen.has(current)) {
523
+ throw new Error('@octanejs/three: Portal store ancestry contains a cycle.');
524
+ }
525
+ seen.add(current);
526
+ current = current.getState().previousRoot!;
527
+ }
528
+ return current;
529
+ }
530
+
531
+ /** Create the callable R3F-shaped store around Zustand's vanilla API. */
532
+ export function createRootStore(invalidate: Invalidate, advance: Advance): RootStore {
533
+ let performanceTimeout: ReturnType<typeof setTimeout> | undefined;
534
+ const api = createVanillaStore<RootState>()((set, get) => {
535
+ const position = new THREE.Vector3();
536
+ const defaultTarget = new THREE.Vector3();
537
+ const tempTarget = new THREE.Vector3();
538
+ const pointer = new THREE.Vector2();
539
+
540
+ const getCurrentViewport: Viewport['getCurrentViewport'] = (
541
+ camera = get().camera,
542
+ target = defaultTarget,
543
+ size = get().size,
544
+ ) => {
545
+ const { width, height, top, left } = size;
546
+ const aspect = height === 0 ? 0 : width / height;
547
+ if ((target as THREE.Vector3).isVector3) tempTarget.copy(target as THREE.Vector3);
548
+ else tempTarget.set(...(target as Parameters<THREE.Vector3['set']>));
549
+ const distance =
550
+ camera == null ? 0 : camera.getWorldPosition(position).distanceTo(tempTarget);
551
+ if (camera != null && isOrthographicCamera(camera)) {
552
+ return {
553
+ width: camera.zoom === 0 ? 0 : width / camera.zoom,
554
+ height: camera.zoom === 0 ? 0 : height / camera.zoom,
555
+ top,
556
+ left,
557
+ factor: 1,
558
+ distance,
559
+ aspect,
560
+ };
561
+ }
562
+ const fov = camera == null ? 0 : (camera.fov * Math.PI) / 180;
563
+ const viewportHeight = 2 * Math.tan(fov / 2) * distance;
564
+ const viewportWidth = height === 0 ? 0 : viewportHeight * (width / height);
565
+ return {
566
+ width: viewportWidth,
567
+ height: viewportHeight,
568
+ top,
569
+ left,
570
+ factor: viewportWidth === 0 ? 0 : width / viewportWidth,
571
+ distance,
572
+ aspect,
573
+ };
574
+ };
575
+
576
+ const state: RootState = {
577
+ set,
578
+ get,
579
+ gl: null as unknown as Renderer,
580
+ renderer: null as unknown as Renderer,
581
+ camera: null as unknown as Camera,
582
+ scene: null as unknown as THREE.Scene,
583
+ raycaster: null as unknown as THREE.Raycaster,
584
+ clock: new THREE.Clock(),
585
+ events: { priority: 1, enabled: true, connected: undefined },
586
+ xr: { connect() {}, disconnect() {} },
587
+ controls: null,
588
+ pointer,
589
+ mouse: pointer,
590
+ legacy: false,
591
+ linear: false,
592
+ flat: false,
593
+ frameloop: 'always',
594
+ performance: {
595
+ current: 1,
596
+ min: 0.5,
597
+ max: 1,
598
+ debounce: 200,
599
+ regress() {
600
+ const current = get();
601
+ if (performanceTimeout !== undefined) clearTimeout(performanceTimeout);
602
+ if (current.performance.current !== current.performance.min) {
603
+ set((value) => ({
604
+ performance: { ...value.performance, current: value.performance.min },
605
+ }));
606
+ }
607
+ performanceTimeout = setTimeout(() => {
608
+ set((value) => ({
609
+ performance: { ...value.performance, current: value.performance.max },
610
+ }));
611
+ }, current.performance.debounce);
612
+ },
613
+ },
614
+ size: { width: 0, height: 0, top: 0, left: 0 },
615
+ viewport: {
616
+ initialDpr: 0,
617
+ dpr: 0,
618
+ width: 0,
619
+ height: 0,
620
+ top: 0,
621
+ left: 0,
622
+ factor: 0,
623
+ distance: 0,
624
+ aspect: 0,
625
+ getCurrentViewport,
626
+ },
627
+ invalidate: (frames = 1) => invalidate(get(), frames),
628
+ advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
629
+ setEvents: (events) => set((value) => ({ events: { ...value.events, ...events } })),
630
+ setSize: (width, height, top = 0, left = 0) => {
631
+ const size = { width, height, top, left };
632
+ set((value) => ({
633
+ size,
634
+ viewport: {
635
+ ...value.viewport,
636
+ ...getCurrentViewport(value.camera, defaultTarget, size),
637
+ },
638
+ }));
639
+ },
640
+ setDpr: (dpr) =>
641
+ set((value) => {
642
+ const resolved = calculateDpr(dpr);
643
+ return {
644
+ viewport: {
645
+ ...value.viewport,
646
+ dpr: resolved,
647
+ initialDpr: value.viewport.initialDpr || resolved,
648
+ },
649
+ };
650
+ }),
651
+ setFrameloop: (frameloop = 'always') => {
652
+ const clock = get().clock;
653
+ clock.stop();
654
+ clock.elapsedTime = 0;
655
+ if (frameloop !== 'never') {
656
+ clock.start();
657
+ clock.elapsedTime = 0;
658
+ }
659
+ set({ frameloop });
660
+ },
661
+ previousRoot: undefined,
662
+ internal: {
663
+ interaction: [],
664
+ hovered: new Map(),
665
+ subscribers: [],
666
+ capturedMap: new Map(),
667
+ initialClick: [0, 0],
668
+ initialHits: [],
669
+ lastEvent: { current: null },
670
+ active: false,
671
+ priority: 0,
672
+ frames: 0,
673
+ subscribe(ref, priority, subscriptionStore) {
674
+ const internal = get().internal;
675
+ if (priority > 0) internal.priority++;
676
+ internal.subscribers = [
677
+ ...internal.subscribers,
678
+ { ref, priority, store: subscriptionStore },
679
+ ].sort((left, right) => left.priority - right.priority);
680
+ return () => {
681
+ const current = get().internal;
682
+ if (priority > 0) current.priority--;
683
+ current.subscribers = current.subscribers.filter(
684
+ (subscription) => subscription.ref !== ref,
685
+ );
686
+ };
687
+ },
688
+ },
689
+ };
690
+ return state;
691
+ });
692
+
693
+ const store = bindRootStore(api);
694
+
695
+ let oldSize = store.getState().size;
696
+ let oldDpr = store.getState().viewport.dpr;
697
+ let oldCamera = store.getState().camera;
698
+ const unsubscribeRenderer = store.subscribe((state) => {
699
+ const { camera, size, viewport, gl } = state;
700
+ if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
701
+ oldSize = size;
702
+ oldDpr = viewport.dpr;
703
+ updateCamera(camera, size);
704
+ if (gl != null && viewport.dpr > 0) gl.setPixelRatio?.(viewport.dpr);
705
+ if (gl != null) {
706
+ const updateStyle =
707
+ typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
708
+ gl.setSize?.(size.width, size.height, updateStyle);
709
+ }
710
+ }
711
+ if (camera !== oldCamera) {
712
+ oldCamera = camera;
713
+ state.set((value) => ({
714
+ viewport: {
715
+ ...value.viewport,
716
+ ...value.viewport.getCurrentViewport(camera),
717
+ },
718
+ }));
719
+ }
720
+ });
721
+ const unsubscribeInvalidate = store.subscribe((state) => invalidate(state));
722
+ STORE_CLEANUPS.set(store, () => {
723
+ unsubscribeRenderer();
724
+ unsubscribeInvalidate();
725
+ if (performanceTimeout !== undefined) clearTimeout(performanceTimeout);
726
+ });
727
+ return store;
728
+ }
729
+
730
+ export function destroyRootStore(store: RootStore): void {
731
+ STORE_CLEANUPS.get(store)?.();
732
+ STORE_CLEANUPS.delete(store);
733
+ const state = store.getState();
734
+ state.clock.stop();
735
+ state.internal.active = false;
736
+ state.internal.subscribers.length = 0;
737
+ state.internal.priority = 0;
738
+ state.internal.frames = 0;
739
+ }
740
+
741
+ export const RootStoreContext = createContext<RootStore | null>(null);