@react-three/fiber 10.0.0-alpha.2 → 10.0.0-canary.164c7be

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.
@@ -1,13 +1,21 @@
1
1
  import * as webgpu from 'three/webgpu';
2
- import { RenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, SRGBColorSpace, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFSoftShadowMap, VSMShadowMap, PCFShadowMap, BasicShadowMap, WebGPURenderer, Color, Vector4, PostProcessing } from 'three/webgpu';
2
+ import { RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene, WebGLCubeRenderTarget, HalfFloatType, Color, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, Vector4, PostProcessing } from 'three/webgpu';
3
3
  import { Inspector } from 'three/addons/inspector/Inspector.js';
4
- import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
  import * as React from 'react';
6
- import React__default, { useMemo, useLayoutEffect, useEffect, useContext, useRef, useImperativeHandle, useCallback, useState } from 'react';
6
+ import React__default, { useLayoutEffect, useRef, useMemo, useEffect, useContext, useImperativeHandle, useCallback, useState } from 'react';
7
7
  import useMeasure from 'react-use-measure';
8
8
  import { useFiber, useContextBridge, traverseFiber, FiberProvider } from 'its-fine';
9
+ import { useThree as useThree$1, useLoader as useLoader$1, useFrame as useFrame$1, createPortal as createPortal$1, applyProps as applyProps$1, extend as extend$1 } from '@react-three/fiber';
10
+ import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
11
+ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
12
+ import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
13
+ import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
14
+ import { GainMapLoader } from '@monogrid/gainmap-js';
9
15
  import Tb, { unstable_scheduleCallback, unstable_IdlePriority } from 'scheduler';
10
16
  import { createWithEqualityFn } from 'zustand/traditional';
17
+ import { getScheduler } from '@pmndrs/scheduler';
18
+ export { Scheduler, getScheduler } from '@pmndrs/scheduler';
11
19
  import { suspend, preload, clear } from 'suspend-react';
12
20
  import { dequal } from 'dequal/lite';
13
21
  import 'zustand/shallow';
@@ -46,9 +54,392 @@ const THREE = /*#__PURE__*/_mergeNamespaces({
46
54
  WebGLRenderer: WebGLRenderer
47
55
  }, [webgpu]);
48
56
 
49
- var __defProp$3 = Object.defineProperty;
50
- var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
51
- var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
57
+ const primaryRegistry = /* @__PURE__ */ new Map();
58
+ const pendingSubscribers = /* @__PURE__ */ new Map();
59
+ function registerPrimary(id, renderer, store) {
60
+ if (primaryRegistry.has(id)) {
61
+ console.warn(`Canvas with id="${id}" already registered. Overwriting.`);
62
+ }
63
+ const entry = { renderer, store };
64
+ primaryRegistry.set(id, entry);
65
+ const subscribers = pendingSubscribers.get(id);
66
+ if (subscribers) {
67
+ subscribers.forEach((callback) => callback(entry));
68
+ pendingSubscribers.delete(id);
69
+ }
70
+ return () => {
71
+ const currentEntry = primaryRegistry.get(id);
72
+ if (currentEntry?.renderer === renderer) {
73
+ primaryRegistry.delete(id);
74
+ }
75
+ };
76
+ }
77
+ function getPrimary(id) {
78
+ return primaryRegistry.get(id);
79
+ }
80
+ function waitForPrimary(id, timeout = 5e3) {
81
+ const existing = primaryRegistry.get(id);
82
+ if (existing) {
83
+ return Promise.resolve(existing);
84
+ }
85
+ return new Promise((resolve, reject) => {
86
+ const timeoutId = setTimeout(() => {
87
+ const subscribers = pendingSubscribers.get(id);
88
+ if (subscribers) {
89
+ const index = subscribers.indexOf(callback);
90
+ if (index !== -1) subscribers.splice(index, 1);
91
+ if (subscribers.length === 0) pendingSubscribers.delete(id);
92
+ }
93
+ reject(new Error(`Timeout waiting for canvas with id="${id}". Make sure a <Canvas id="${id}"> is mounted.`));
94
+ }, timeout);
95
+ const callback = (entry) => {
96
+ clearTimeout(timeoutId);
97
+ resolve(entry);
98
+ };
99
+ if (!pendingSubscribers.has(id)) {
100
+ pendingSubscribers.set(id, []);
101
+ }
102
+ pendingSubscribers.get(id).push(callback);
103
+ });
104
+ }
105
+ function hasPrimary(id) {
106
+ return primaryRegistry.has(id);
107
+ }
108
+ function unregisterPrimary(id) {
109
+ primaryRegistry.delete(id);
110
+ }
111
+ function getPrimaryIds() {
112
+ return Array.from(primaryRegistry.keys());
113
+ }
114
+
115
+ const presetsObj = {
116
+ apartment: "lebombo_1k.hdr",
117
+ city: "potsdamer_platz_1k.hdr",
118
+ dawn: "kiara_1_dawn_1k.hdr",
119
+ forest: "forest_slope_1k.hdr",
120
+ lobby: "st_fagans_interior_1k.hdr",
121
+ night: "dikhololo_night_1k.hdr",
122
+ park: "rooitou_park_1k.hdr",
123
+ studio: "studio_small_03_1k.hdr",
124
+ sunset: "venice_sunset_1k.hdr",
125
+ warehouse: "empty_warehouse_01_1k.hdr"
126
+ };
127
+
128
+ const CUBEMAP_ROOT = "https://raw.githack.com/pmndrs/drei-assets/456060a26bbeb8fdf79326f224b6d99b8bcce736/hdri/";
129
+ const isArray = (arr) => Array.isArray(arr);
130
+ const defaultFiles = ["/px.png", "/nx.png", "/py.png", "/ny.png", "/pz.png", "/nz.png"];
131
+ function useEnvironment({
132
+ files = defaultFiles,
133
+ path = "",
134
+ preset = void 0,
135
+ colorSpace = void 0,
136
+ extensions
137
+ } = {}) {
138
+ if (preset) {
139
+ validatePreset(preset);
140
+ files = presetsObj[preset];
141
+ path = CUBEMAP_ROOT;
142
+ }
143
+ const multiFile = isArray(files);
144
+ const { extension, isCubemap } = getExtension(files);
145
+ const loader = getLoader$1(extension);
146
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
147
+ const renderer = useThree$1((state) => state.renderer);
148
+ useLayoutEffect(() => {
149
+ if (extension !== "webp" && extension !== "jpg" && extension !== "jpeg") return;
150
+ function clearGainmapTexture() {
151
+ useLoader$1.clear(loader, multiFile ? [files] : files);
152
+ }
153
+ renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
154
+ }, [extension, files, loader, multiFile, renderer.domElement]);
155
+ const loaderResult = useLoader$1(
156
+ loader,
157
+ multiFile ? [files] : files,
158
+ (loader2) => {
159
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
160
+ loader2.setRenderer?.(renderer);
161
+ }
162
+ loader2.setPath?.(path);
163
+ if (extensions) extensions(loader2);
164
+ }
165
+ );
166
+ let texture = multiFile ? (
167
+ // @ts-ignore
168
+ loaderResult[0]
169
+ ) : loaderResult;
170
+ if (extension === "jpg" || extension === "jpeg" || extension === "webp") {
171
+ texture = texture.renderTarget?.texture;
172
+ }
173
+ texture.mapping = isCubemap ? CubeReflectionMapping : EquirectangularReflectionMapping;
174
+ texture.colorSpace = colorSpace ?? (isCubemap ? "srgb" : "srgb-linear");
175
+ return texture;
176
+ }
177
+ const preloadDefaultOptions = {
178
+ files: defaultFiles,
179
+ path: "",
180
+ preset: void 0,
181
+ extensions: void 0
182
+ };
183
+ useEnvironment.preload = (preloadOptions) => {
184
+ const options = { ...preloadDefaultOptions, ...preloadOptions };
185
+ let { files, path = "" } = options;
186
+ const { preset, extensions } = options;
187
+ if (preset) {
188
+ validatePreset(preset);
189
+ files = presetsObj[preset];
190
+ path = CUBEMAP_ROOT;
191
+ }
192
+ const { extension } = getExtension(files);
193
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
194
+ throw new Error("useEnvironment: Preloading gainmaps is not supported");
195
+ }
196
+ const loader = getLoader$1(extension);
197
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
198
+ useLoader$1.preload(loader, isArray(files) ? [files] : files, (loader2) => {
199
+ loader2.setPath?.(path);
200
+ if (extensions) extensions(loader2);
201
+ });
202
+ };
203
+ const clearDefaultOptins = {
204
+ files: defaultFiles,
205
+ preset: void 0
206
+ };
207
+ useEnvironment.clear = (clearOptions) => {
208
+ const options = { ...clearDefaultOptins, ...clearOptions };
209
+ let { files } = options;
210
+ const { preset } = options;
211
+ if (preset) {
212
+ validatePreset(preset);
213
+ files = presetsObj[preset];
214
+ }
215
+ const { extension } = getExtension(files);
216
+ const loader = getLoader$1(extension);
217
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
218
+ useLoader$1.clear(loader, isArray(files) ? [files] : files);
219
+ };
220
+ function validatePreset(preset) {
221
+ if (!(preset in presetsObj)) throw new Error("Preset must be one of: " + Object.keys(presetsObj).join(", "));
222
+ }
223
+ function getExtension(files) {
224
+ const isCubemap = isArray(files) && files.length === 6;
225
+ const isGainmap = isArray(files) && files.length === 3 && files.some((file) => file.endsWith("json"));
226
+ const firstEntry = isArray(files) ? files[0] : files;
227
+ const extension = isCubemap ? "cube" : isGainmap ? "webp" : firstEntry.startsWith("data:application/exr") ? "exr" : firstEntry.startsWith("data:application/hdr") ? "hdr" : firstEntry.startsWith("data:image/jpeg") ? "jpg" : firstEntry.split(".").pop()?.split("?")?.shift()?.toLowerCase();
228
+ return { extension, isCubemap, isGainmap };
229
+ }
230
+ function getLoader$1(extension) {
231
+ const loader = extension === "cube" ? CubeTextureLoader : extension === "hdr" ? HDRLoader : extension === "exr" ? EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader : extension === "webp" ? GainMapLoader : null;
232
+ return loader;
233
+ }
234
+
235
+ const isRef$1 = (obj) => obj.current && obj.current.isScene;
236
+ const resolveScene = (scene) => isRef$1(scene) ? scene.current : scene;
237
+ function setEnvProps(background, scene, defaultScene, texture, sceneProps = {}) {
238
+ sceneProps = {
239
+ backgroundBlurriness: 0,
240
+ backgroundIntensity: 1,
241
+ backgroundRotation: [0, 0, 0],
242
+ environmentIntensity: 1,
243
+ environmentRotation: [0, 0, 0],
244
+ ...sceneProps
245
+ };
246
+ const target = resolveScene(scene || defaultScene);
247
+ const oldbg = target.background;
248
+ const oldenv = target.environment;
249
+ const oldSceneProps = {
250
+ // @ts-ignore
251
+ backgroundBlurriness: target.backgroundBlurriness,
252
+ // @ts-ignore
253
+ backgroundIntensity: target.backgroundIntensity,
254
+ // @ts-ignore
255
+ backgroundRotation: target.backgroundRotation?.clone?.() ?? [0, 0, 0],
256
+ // @ts-ignore
257
+ environmentIntensity: target.environmentIntensity,
258
+ // @ts-ignore
259
+ environmentRotation: target.environmentRotation?.clone?.() ?? [0, 0, 0]
260
+ };
261
+ if (background !== "only") target.environment = texture;
262
+ if (background) target.background = texture;
263
+ applyProps$1(target, sceneProps);
264
+ return () => {
265
+ if (background !== "only") target.environment = oldenv;
266
+ if (background) target.background = oldbg;
267
+ applyProps$1(target, oldSceneProps);
268
+ };
269
+ }
270
+ function EnvironmentMap({ scene, background = false, map, ...config }) {
271
+ const defaultScene = useThree$1((state) => state.scene);
272
+ React.useLayoutEffect(() => {
273
+ if (map) return setEnvProps(background, scene, defaultScene, map, config);
274
+ });
275
+ return null;
276
+ }
277
+ function EnvironmentCube({
278
+ background = false,
279
+ scene,
280
+ blur,
281
+ backgroundBlurriness,
282
+ backgroundIntensity,
283
+ backgroundRotation,
284
+ environmentIntensity,
285
+ environmentRotation,
286
+ ...rest
287
+ }) {
288
+ const texture = useEnvironment(rest);
289
+ const defaultScene = useThree$1((state) => state.scene);
290
+ React.useLayoutEffect(() => {
291
+ return setEnvProps(background, scene, defaultScene, texture, {
292
+ backgroundBlurriness: blur ?? backgroundBlurriness,
293
+ backgroundIntensity,
294
+ backgroundRotation,
295
+ environmentIntensity,
296
+ environmentRotation
297
+ });
298
+ });
299
+ React.useEffect(() => {
300
+ return () => {
301
+ texture.dispose();
302
+ };
303
+ }, [texture]);
304
+ return null;
305
+ }
306
+ function EnvironmentPortal({
307
+ children,
308
+ near = 0.1,
309
+ far = 1e3,
310
+ resolution = 256,
311
+ frames = 1,
312
+ map,
313
+ background = false,
314
+ blur,
315
+ backgroundBlurriness,
316
+ backgroundIntensity,
317
+ backgroundRotation,
318
+ environmentIntensity,
319
+ environmentRotation,
320
+ scene,
321
+ files,
322
+ path,
323
+ preset = void 0,
324
+ extensions
325
+ }) {
326
+ const gl = useThree$1((state) => state.gl);
327
+ const defaultScene = useThree$1((state) => state.scene);
328
+ const camera = React.useRef(null);
329
+ const [virtualScene] = React.useState(() => new Scene());
330
+ const fbo = React.useMemo(() => {
331
+ const fbo2 = new WebGLCubeRenderTarget(resolution);
332
+ fbo2.texture.type = HalfFloatType;
333
+ return fbo2;
334
+ }, [resolution]);
335
+ React.useEffect(() => {
336
+ return () => {
337
+ fbo.dispose();
338
+ };
339
+ }, [fbo]);
340
+ React.useLayoutEffect(() => {
341
+ if (frames === 1) {
342
+ const autoClear = gl.autoClear;
343
+ gl.autoClear = true;
344
+ camera.current.update(gl, virtualScene);
345
+ gl.autoClear = autoClear;
346
+ }
347
+ return setEnvProps(background, scene, defaultScene, fbo.texture, {
348
+ backgroundBlurriness: blur ?? backgroundBlurriness,
349
+ backgroundIntensity,
350
+ backgroundRotation,
351
+ environmentIntensity,
352
+ environmentRotation
353
+ });
354
+ }, [
355
+ children,
356
+ virtualScene,
357
+ fbo.texture,
358
+ scene,
359
+ defaultScene,
360
+ background,
361
+ frames,
362
+ gl,
363
+ blur,
364
+ backgroundBlurriness,
365
+ backgroundIntensity,
366
+ backgroundRotation,
367
+ environmentIntensity,
368
+ environmentRotation
369
+ ]);
370
+ let count = 1;
371
+ useFrame$1(() => {
372
+ if (frames === Infinity || count < frames) {
373
+ const autoClear = gl.autoClear;
374
+ gl.autoClear = true;
375
+ camera.current.update(gl, virtualScene);
376
+ gl.autoClear = autoClear;
377
+ count++;
378
+ }
379
+ });
380
+ return /* @__PURE__ */ jsx(Fragment, { children: createPortal$1(
381
+ /* @__PURE__ */ jsxs(Fragment, { children: [
382
+ children,
383
+ /* @__PURE__ */ jsx("cubeCamera", { ref: camera, args: [near, far, fbo] }),
384
+ files || preset ? /* @__PURE__ */ jsx(EnvironmentCube, { background: true, files, preset, path, extensions }) : map ? /* @__PURE__ */ jsx(EnvironmentMap, { background: true, map, extensions }) : null
385
+ ] }),
386
+ virtualScene
387
+ ) });
388
+ }
389
+ function EnvironmentGround(props) {
390
+ const textureDefault = useEnvironment(props);
391
+ const texture = props.map || textureDefault;
392
+ React.useMemo(() => extend$1({ GroundProjectedEnvImpl: GroundedSkybox }), []);
393
+ React.useEffect(() => {
394
+ return () => {
395
+ textureDefault.dispose();
396
+ };
397
+ }, [textureDefault]);
398
+ const height = props.ground?.height ?? 15;
399
+ const radius = props.ground?.radius ?? 60;
400
+ const scale = props.ground?.scale ?? 1e3;
401
+ const args = React.useMemo(
402
+ () => [texture, height, radius],
403
+ [texture, height, radius]
404
+ );
405
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
406
+ /* @__PURE__ */ jsx(EnvironmentMap, { ...props, map: texture }),
407
+ /* @__PURE__ */ jsx("groundProjectedEnvImpl", { args, scale })
408
+ ] });
409
+ }
410
+ function EnvironmentColor({ color, scene }) {
411
+ const defaultScene = useThree$1((state) => state.scene);
412
+ React.useLayoutEffect(() => {
413
+ if (color === void 0) return;
414
+ const target = resolveScene(scene || defaultScene);
415
+ const oldBg = target.background;
416
+ target.background = new Color(color);
417
+ return () => {
418
+ target.background = oldBg;
419
+ };
420
+ });
421
+ return null;
422
+ }
423
+ function EnvironmentDualSource(props) {
424
+ const { backgroundFiles, ...envProps } = props;
425
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
426
+ /* @__PURE__ */ jsx(EnvironmentCube, { ...envProps, background: false }),
427
+ /* @__PURE__ */ jsx(EnvironmentCube, { ...props, files: backgroundFiles, background: "only" })
428
+ ] });
429
+ }
430
+ function Environment(props) {
431
+ if (props.color && !props.files && !props.preset && !props.map) {
432
+ return /* @__PURE__ */ jsx(EnvironmentColor, { ...props });
433
+ }
434
+ if (props.backgroundFiles && props.backgroundFiles !== props.files) {
435
+ return /* @__PURE__ */ jsx(EnvironmentDualSource, { ...props });
436
+ }
437
+ return props.ground ? /* @__PURE__ */ jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsx(EnvironmentCube, { ...props });
438
+ }
439
+
440
+ var __defProp$1 = Object.defineProperty;
441
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
442
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
52
443
  const act = React["act"];
53
444
  const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React.useLayoutEffect : React.useEffect;
54
445
  function useMutableCallback(fn) {
@@ -80,7 +471,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
80
471
  return _a = class extends React.Component {
81
472
  constructor() {
82
473
  super(...arguments);
83
- __publicField$3(this, "state", { error: false });
474
+ __publicField$1(this, "state", { error: false });
84
475
  }
85
476
  componentDidCatch(err) {
86
477
  this.props.set(err);
@@ -88,7 +479,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
88
479
  render() {
89
480
  return this.state.error ? null : this.props.children;
90
481
  }
91
- }, __publicField$3(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
482
+ }, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
92
483
  })();
93
484
 
94
485
  const is = {
@@ -225,7 +616,8 @@ function prepare(target, root, type, props) {
225
616
  object,
226
617
  eventCount: 0,
227
618
  handlers: {},
228
- isHidden: false
619
+ isHidden: false,
620
+ deferredRefs: []
229
621
  };
230
622
  if (object) object.__r3f = instance;
231
623
  }
@@ -274,7 +666,7 @@ function createOcclusionObserverNode(store, uniform) {
274
666
  let occlusionSetupPromise = null;
275
667
  function enableOcclusion(store) {
276
668
  const state = store.getState();
277
- const { internal, renderer, rootScene } = state;
669
+ const { internal, renderer } = state;
278
670
  if (internal.occlusionEnabled || occlusionSetupPromise) return;
279
671
  const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
280
672
  if (!hasOcclusionSupport) {
@@ -437,6 +829,22 @@ function hasVisibilityHandlers(handlers) {
437
829
  return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
438
830
  }
439
831
 
832
+ const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
833
+ function fromRef(ref) {
834
+ return { [FROM_REF]: ref };
835
+ }
836
+ function isFromRef(value) {
837
+ return value !== null && typeof value === "object" && FROM_REF in value;
838
+ }
839
+
840
+ const ONCE = Symbol.for("@react-three/fiber.once");
841
+ function once(...args) {
842
+ return { [ONCE]: args.length ? args : true };
843
+ }
844
+ function isOnce(value) {
845
+ return value !== null && typeof value === "object" && ONCE in value;
846
+ }
847
+
440
848
  const RESERVED_PROPS = [
441
849
  "children",
442
850
  "key",
@@ -507,7 +915,7 @@ function getMemoizedPrototype(root) {
507
915
  ctor = new root.constructor();
508
916
  MEMOIZED_PROTOTYPES.set(root.constructor, ctor);
509
917
  }
510
- } catch (e) {
918
+ } catch {
511
919
  }
512
920
  return ctor;
513
921
  }
@@ -553,6 +961,25 @@ function applyProps(object, props) {
553
961
  continue;
554
962
  }
555
963
  if (value === void 0) continue;
964
+ if (isFromRef(value)) {
965
+ instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
966
+ continue;
967
+ }
968
+ if (isOnce(value)) {
969
+ if (instance?.appliedOnce?.has(prop)) continue;
970
+ if (instance) {
971
+ instance.appliedOnce ?? (instance.appliedOnce = /* @__PURE__ */ new Set());
972
+ instance.appliedOnce.add(prop);
973
+ }
974
+ const { root: targetRoot, key: targetKey } = resolve(object, prop);
975
+ const args = value[ONCE];
976
+ if (typeof targetRoot[targetKey] === "function") {
977
+ targetRoot[targetKey](...args === true ? [] : args);
978
+ } else if (args !== true && args.length > 0) {
979
+ targetRoot[targetKey] = args[0];
980
+ }
981
+ continue;
982
+ }
556
983
  let { root, key, target } = resolve(object, prop);
557
984
  if (target === void 0 && (typeof root !== "object" || root === null)) {
558
985
  throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`);
@@ -575,7 +1002,10 @@ function applyProps(object, props) {
575
1002
  else target.set(value);
576
1003
  } else {
577
1004
  root[key] = value;
578
- if (rootState && !rootState.linear && colorMaps.includes(key) && isTexture(value) && root[key]?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
1005
+ if (key.endsWith("Node") && root.isMaterial) {
1006
+ root.needsUpdate = true;
1007
+ }
1008
+ if (rootState && rootState.renderer?.outputColorSpace === SRGBColorSpace && colorMaps.includes(key) && isTexture(value) && root[key]?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
579
1009
  root[key].format === RGBAFormat && root[key].type === UnsignedByteType) {
580
1010
  root[key].colorSpace = rootState.textureColorSpace;
581
1011
  }
@@ -608,38 +1038,60 @@ function applyProps(object, props) {
608
1038
  return object;
609
1039
  }
610
1040
 
1041
+ const DEFAULT_POINTER_ID = 0;
1042
+ const XR_POINTER_ID_START = 1e3;
1043
+ function getPointerState(internal, pointerId) {
1044
+ let state = internal.pointerMap.get(pointerId);
1045
+ if (!state) {
1046
+ state = {
1047
+ hovered: /* @__PURE__ */ new Map(),
1048
+ captured: /* @__PURE__ */ new Map(),
1049
+ initialClick: [0, 0],
1050
+ initialHits: []
1051
+ };
1052
+ internal.pointerMap.set(pointerId, state);
1053
+ }
1054
+ return state;
1055
+ }
1056
+ function getPointerId(event) {
1057
+ return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
1058
+ }
611
1059
  function makeId(event) {
612
1060
  return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
613
1061
  }
614
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
615
- const captureData = captures.get(obj);
1062
+ function releaseInternalPointerCapture(internal, obj, pointerId) {
1063
+ const pointerState = internal.pointerMap.get(pointerId);
1064
+ if (!pointerState) return;
1065
+ const captureData = pointerState.captured.get(obj);
616
1066
  if (captureData) {
617
- captures.delete(obj);
618
- if (captures.size === 0) {
619
- capturedMap.delete(pointerId);
620
- captureData.target.releasePointerCapture(pointerId);
621
- }
1067
+ pointerState.captured.delete(obj);
1068
+ captureData.target.releasePointerCapture(pointerId);
622
1069
  }
623
1070
  }
624
1071
  function removeInteractivity(store, object) {
625
1072
  const { internal } = store.getState();
626
1073
  internal.interaction = internal.interaction.filter((o) => o !== object);
627
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
628
- internal.hovered.forEach((value, key) => {
629
- if (value.eventObject === object || value.object === object) {
630
- internal.hovered.delete(key);
1074
+ for (const [pointerId, pointerState] of internal.pointerMap) {
1075
+ pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
1076
+ pointerState.hovered.forEach((value, key) => {
1077
+ if (value.eventObject === object || value.object === object) {
1078
+ pointerState.hovered.delete(key);
1079
+ }
1080
+ });
1081
+ if (pointerState.captured.has(object)) {
1082
+ releaseInternalPointerCapture(internal, object, pointerId);
631
1083
  }
632
- });
633
- internal.capturedMap.forEach((captures, pointerId) => {
634
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
635
- });
1084
+ }
636
1085
  unregisterVisibility(store, object);
637
1086
  }
638
1087
  function createEvents(store) {
639
- function calculateDistance(event) {
1088
+ function calculateDistance(event, pointerId) {
640
1089
  const { internal } = store.getState();
641
- const dx = event.offsetX - internal.initialClick[0];
642
- const dy = event.offsetY - internal.initialClick[1];
1090
+ const pointerState = internal.pointerMap.get(pointerId);
1091
+ if (!pointerState) return 0;
1092
+ const [initialX, initialY] = pointerState.initialClick;
1093
+ const dx = event.offsetX - initialX;
1094
+ const dy = event.offsetY - initialY;
643
1095
  return Math.round(Math.sqrt(dx * dx + dy * dy));
644
1096
  }
645
1097
  function filterPointerEvents(objects) {
@@ -675,6 +1127,15 @@ function createEvents(store) {
675
1127
  return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
676
1128
  }
677
1129
  let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
1130
+ const aInteractivePriority = a.object.userData?.interactivePriority;
1131
+ const bInteractivePriority = b.object.userData?.interactivePriority;
1132
+ if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
1133
+ if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
1134
+ if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
1135
+ if (aInteractivePriority !== bInteractivePriority) {
1136
+ return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
1137
+ }
1138
+ }
678
1139
  const aState = getRootState(a.object);
679
1140
  const bState = getRootState(b.object);
680
1141
  const aPriority = aState?.events?.priority ?? 1;
@@ -696,9 +1157,13 @@ function createEvents(store) {
696
1157
  eventObject = eventObject.parent;
697
1158
  }
698
1159
  }
699
- if ("pointerId" in event && state.internal.capturedMap.has(event.pointerId)) {
700
- for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
701
- if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1160
+ if ("pointerId" in event) {
1161
+ const pointerId = event.pointerId;
1162
+ const pointerState = state.internal.pointerMap.get(pointerId);
1163
+ if (pointerState?.captured.size) {
1164
+ for (const captureData of pointerState.captured.values()) {
1165
+ if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1166
+ }
702
1167
  }
703
1168
  }
704
1169
  return intersections;
@@ -711,27 +1176,25 @@ function createEvents(store) {
711
1176
  if (state) {
712
1177
  const { raycaster, pointer, camera, internal } = state;
713
1178
  const unprojectedPoint = new Vector3(pointer.x, pointer.y, 0).unproject(camera);
714
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1179
+ const hasPointerCapture = (id) => {
1180
+ const pointerState = internal.pointerMap.get(id);
1181
+ return pointerState?.captured.has(hit.eventObject) ?? false;
1182
+ };
715
1183
  const setPointerCapture = (id) => {
716
1184
  const captureData = { intersection: hit, target: event.target };
717
- if (internal.capturedMap.has(id)) {
718
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
719
- } else {
720
- internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
721
- }
1185
+ const pointerState = getPointerState(internal, id);
1186
+ pointerState.captured.set(hit.eventObject, captureData);
722
1187
  event.target.setPointerCapture(id);
723
1188
  };
724
1189
  const releasePointerCapture = (id) => {
725
- const captures = internal.capturedMap.get(id);
726
- if (captures) {
727
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
728
- }
1190
+ releaseInternalPointerCapture(internal, hit.eventObject, id);
729
1191
  };
730
1192
  const extractEventProps = {};
731
1193
  for (const prop in event) {
732
1194
  const property = event[prop];
733
1195
  if (typeof property !== "function") extractEventProps[prop] = property;
734
1196
  }
1197
+ const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
735
1198
  const raycastEvent = {
736
1199
  ...hit,
737
1200
  ...extractEventProps,
@@ -742,18 +1205,19 @@ function createEvents(store) {
742
1205
  unprojectedPoint,
743
1206
  ray: raycaster.ray,
744
1207
  camera,
1208
+ pointerId: eventPointerId,
745
1209
  // Hijack stopPropagation, which just sets a flag
746
1210
  stopPropagation() {
747
- const capturesForPointer = "pointerId" in event && internal.capturedMap.get(event.pointerId);
1211
+ const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
748
1212
  if (
749
1213
  // ...if this pointer hasn't been captured
750
- !capturesForPointer || // ... or if the hit object is capturing the pointer
751
- capturesForPointer.has(hit.eventObject)
1214
+ !pointerState?.captured.size || // ... or if the hit object is capturing the pointer
1215
+ pointerState.captured.has(hit.eventObject)
752
1216
  ) {
753
1217
  raycastEvent.stopped = localState.stopped = true;
754
- if (internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1218
+ if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
755
1219
  const higher = intersections.slice(0, intersections.indexOf(hit));
756
- cancelPointer([...higher, hit]);
1220
+ cancelPointer([...higher, hit], eventPointerId);
757
1221
  }
758
1222
  }
759
1223
  },
@@ -769,15 +1233,18 @@ function createEvents(store) {
769
1233
  }
770
1234
  return intersections;
771
1235
  }
772
- function cancelPointer(intersections) {
1236
+ function cancelPointer(intersections, pointerId) {
773
1237
  const { internal } = store.getState();
774
- for (const hoveredObj of internal.hovered.values()) {
1238
+ const pid = pointerId ?? DEFAULT_POINTER_ID;
1239
+ const pointerState = internal.pointerMap.get(pid);
1240
+ if (!pointerState) return;
1241
+ for (const [hoveredId, hoveredObj] of pointerState.hovered) {
775
1242
  if (!intersections.length || !intersections.find(
776
1243
  (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
777
1244
  )) {
778
1245
  const eventObject = hoveredObj.eventObject;
779
1246
  const instance = eventObject.__r3f;
780
- internal.hovered.delete(makeId(hoveredObj));
1247
+ pointerState.hovered.delete(hoveredId);
781
1248
  if (instance?.eventCount) {
782
1249
  const handlers = instance.handlers;
783
1250
  const data = { ...hoveredObj, intersections };
@@ -806,41 +1273,118 @@ function createEvents(store) {
806
1273
  instance?.handlers.onDropMissed?.(event);
807
1274
  }
808
1275
  }
1276
+ function cleanupPointer(pointerId) {
1277
+ const { internal } = store.getState();
1278
+ const pointerState = internal.pointerMap.get(pointerId);
1279
+ if (pointerState) {
1280
+ for (const [, hoveredObj] of pointerState.hovered) {
1281
+ const eventObject = hoveredObj.eventObject;
1282
+ const instance = eventObject.__r3f;
1283
+ if (instance?.eventCount) {
1284
+ const handlers = instance.handlers;
1285
+ const data = { ...hoveredObj, intersections: [] };
1286
+ handlers.onPointerOut?.(data);
1287
+ handlers.onPointerLeave?.(data);
1288
+ }
1289
+ }
1290
+ internal.pointerMap.delete(pointerId);
1291
+ }
1292
+ internal.pointerDirty.delete(pointerId);
1293
+ }
1294
+ function processDeferredPointer(event, pointerId) {
1295
+ const state = store.getState();
1296
+ const { internal } = state;
1297
+ if (!state.events.enabled) return;
1298
+ const filter = filterPointerEvents;
1299
+ const hits = intersect(event, filter);
1300
+ cancelPointer(hits, pointerId);
1301
+ function onIntersect(data) {
1302
+ const eventObject = data.eventObject;
1303
+ const instance = eventObject.__r3f;
1304
+ if (!instance?.eventCount) return;
1305
+ const handlers = instance.handlers;
1306
+ if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1307
+ const id = makeId(data);
1308
+ const pointerState = getPointerState(internal, pointerId);
1309
+ const hoveredItem = pointerState.hovered.get(id);
1310
+ if (!hoveredItem) {
1311
+ pointerState.hovered.set(id, data);
1312
+ handlers.onPointerOver?.(data);
1313
+ handlers.onPointerEnter?.(data);
1314
+ } else if (hoveredItem.stopped) {
1315
+ data.stopPropagation();
1316
+ }
1317
+ }
1318
+ handlers.onPointerMove?.(data);
1319
+ }
1320
+ handleIntersects(hits, event, 0, onIntersect);
1321
+ }
809
1322
  function handlePointer(name) {
810
1323
  switch (name) {
811
1324
  case "onPointerLeave":
812
- case "onPointerCancel":
813
1325
  case "onDragLeave":
814
1326
  return () => cancelPointer([]);
1327
+ // Global cancel of these events
1328
+ case "onPointerCancel":
1329
+ return (event) => {
1330
+ const pointerId = getPointerId(event);
1331
+ cleanupPointer(pointerId);
1332
+ };
815
1333
  case "onLostPointerCapture":
816
1334
  return (event) => {
817
1335
  const { internal } = store.getState();
818
- if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) {
1336
+ const pointerId = getPointerId(event);
1337
+ const pointerState = internal.pointerMap.get(pointerId);
1338
+ if (pointerState?.captured.size) {
819
1339
  requestAnimationFrame(() => {
820
- if (internal.capturedMap.has(event.pointerId)) {
821
- internal.capturedMap.delete(event.pointerId);
822
- cancelPointer([]);
1340
+ const pointerState2 = internal.pointerMap.get(pointerId);
1341
+ if (pointerState2?.captured.size) {
1342
+ pointerState2.captured.clear();
823
1343
  }
1344
+ cancelPointer([], pointerId);
824
1345
  });
825
1346
  }
826
1347
  };
827
1348
  }
828
1349
  return function handleEvent(event) {
829
1350
  const state = store.getState();
830
- const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
1351
+ const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
1352
+ const pointerId = getPointerId(event);
831
1353
  internal.lastEvent.current = event;
832
- if (!state.events.enabled) return;
1354
+ if (!events.enabled) return;
833
1355
  const isPointerMove = name === "onPointerMove";
834
1356
  const isDragOver = name === "onDragOver";
835
1357
  const isDrop = name === "onDrop";
836
1358
  const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
1359
+ const isPointerDown = name === "onPointerDown";
1360
+ const isPointerUp = name === "onPointerUp";
1361
+ const isWheel = name === "onWheel";
1362
+ const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
1363
+ if (isPointerMove && canDeferRaycasts) {
1364
+ events.compute?.(event, state);
1365
+ internal.pointerDirty.set(pointerId, event);
1366
+ return;
1367
+ }
1368
+ if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
1369
+ events.compute?.(event, state);
1370
+ internal.pointerDirty.set(pointerId, event);
1371
+ return;
1372
+ }
1373
+ if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
1374
+ const deferredEvent = internal.pointerDirty.get(pointerId);
1375
+ internal.pointerDirty.delete(pointerId);
1376
+ processDeferredPointer(deferredEvent, pointerId);
1377
+ }
837
1378
  const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
838
1379
  const hits = intersect(event, filter);
839
- const delta = isClickEvent ? calculateDistance(event) : 0;
840
- if (name === "onPointerDown") {
841
- internal.initialClick = [event.offsetX, event.offsetY];
842
- internal.initialHits = hits.map((hit) => hit.eventObject);
843
- }
1380
+ const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
1381
+ if (isPointerDown) {
1382
+ const pointerState2 = getPointerState(internal, pointerId);
1383
+ pointerState2.initialClick = [event.offsetX, event.offsetY];
1384
+ pointerState2.initialHits = hits.map((hit) => hit.eventObject);
1385
+ }
1386
+ const pointerState = internal.pointerMap.get(pointerId);
1387
+ const initialHits = pointerState?.initialHits ?? [];
844
1388
  if (isClickEvent && !hits.length) {
845
1389
  if (delta <= 2) {
846
1390
  pointerMissed(event, internal.interaction);
@@ -855,7 +1399,9 @@ function createEvents(store) {
855
1399
  dropMissed(event, internal.interaction);
856
1400
  if (onDropMissed) onDropMissed(event);
857
1401
  }
858
- if (isPointerMove || isDragOver) cancelPointer(hits);
1402
+ if (isPointerMove || isDragOver) {
1403
+ cancelPointer(hits, pointerId);
1404
+ }
859
1405
  function onIntersect(data) {
860
1406
  const eventObject = data.eventObject;
861
1407
  const instance = eventObject.__r3f;
@@ -864,9 +1410,10 @@ function createEvents(store) {
864
1410
  if (isPointerMove) {
865
1411
  if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
866
1412
  const id = makeId(data);
867
- const hoveredItem = internal.hovered.get(id);
1413
+ const pointerState2 = getPointerState(internal, pointerId);
1414
+ const hoveredItem = pointerState2.hovered.get(id);
868
1415
  if (!hoveredItem) {
869
- internal.hovered.set(id, data);
1416
+ pointerState2.hovered.set(id, data);
870
1417
  handlers.onPointerOver?.(data);
871
1418
  handlers.onPointerEnter?.(data);
872
1419
  } else if (hoveredItem.stopped) {
@@ -876,9 +1423,10 @@ function createEvents(store) {
876
1423
  handlers.onPointerMove?.(data);
877
1424
  } else if (isDragOver) {
878
1425
  const id = makeId(data);
879
- const hoveredItem = internal.hovered.get(id);
1426
+ const pointerState2 = getPointerState(internal, pointerId);
1427
+ const hoveredItem = pointerState2.hovered.get(id);
880
1428
  if (!hoveredItem) {
881
- internal.hovered.set(id, data);
1429
+ pointerState2.hovered.set(id, data);
882
1430
  handlers.onDragOverEnter?.(data);
883
1431
  } else if (hoveredItem.stopped) {
884
1432
  data.stopPropagation();
@@ -889,18 +1437,18 @@ function createEvents(store) {
889
1437
  } else {
890
1438
  const handler = handlers[name];
891
1439
  if (handler) {
892
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1440
+ if (!isClickEvent || initialHits.includes(eventObject)) {
893
1441
  pointerMissed(
894
1442
  event,
895
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1443
+ internal.interaction.filter((object) => !initialHits.includes(object))
896
1444
  );
897
1445
  handler(data);
898
1446
  }
899
1447
  } else {
900
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1448
+ if (isClickEvent && initialHits.includes(eventObject)) {
901
1449
  pointerMissed(
902
1450
  event,
903
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1451
+ internal.interaction.filter((object) => !initialHits.includes(object))
904
1452
  );
905
1453
  }
906
1454
  }
@@ -909,7 +1457,15 @@ function createEvents(store) {
909
1457
  handleIntersects(hits, event, delta, onIntersect);
910
1458
  };
911
1459
  }
912
- return { handlePointer };
1460
+ function flushDeferredPointers() {
1461
+ const { internal, events } = store.getState();
1462
+ if (!events.frameTimedRaycasts) return;
1463
+ for (const [pointerId, event] of internal.pointerDirty) {
1464
+ processDeferredPointer(event, pointerId);
1465
+ }
1466
+ internal.pointerDirty.clear();
1467
+ }
1468
+ return { handlePointer, flushDeferredPointers, processDeferredPointer };
913
1469
  }
914
1470
  const DOM_EVENTS = {
915
1471
  onClick: ["click", false],
@@ -928,11 +1484,16 @@ const DOM_EVENTS = {
928
1484
  onLostPointerCapture: ["lostpointercapture", true]
929
1485
  };
930
1486
  function createPointerEvents(store) {
931
- const { handlePointer } = createEvents(store);
1487
+ const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
1488
+ let nextXRPointerId = XR_POINTER_ID_START;
1489
+ const xrPointers = /* @__PURE__ */ new Map();
932
1490
  return {
933
1491
  priority: 1,
934
1492
  enabled: true,
935
- compute(event, state, previous) {
1493
+ frameTimedRaycasts: true,
1494
+ alwaysFireOnScroll: true,
1495
+ updateOnFrame: false,
1496
+ compute(event, state) {
936
1497
  state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
937
1498
  state.raycaster.setFromCamera(state.pointer, state.camera);
938
1499
  },
@@ -941,11 +1502,33 @@ function createPointerEvents(store) {
941
1502
  (acc, key) => ({ ...acc, [key]: handlePointer(key) }),
942
1503
  {}
943
1504
  ),
944
- update: () => {
1505
+ update: (pointerId) => {
945
1506
  const { events, internal } = store.getState();
946
- if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current);
1507
+ if (!events.handlers) return;
1508
+ if (pointerId !== void 0) {
1509
+ const event = internal.pointerDirty.get(pointerId);
1510
+ if (event) {
1511
+ internal.pointerDirty.delete(pointerId);
1512
+ processDeferredPointer(event, pointerId);
1513
+ } else if (internal.lastEvent?.current) {
1514
+ processDeferredPointer(internal.lastEvent.current, pointerId);
1515
+ }
1516
+ } else {
1517
+ flushDeferredPointers();
1518
+ if (internal.lastEvent?.current) {
1519
+ events.handlers.onPointerMove(internal.lastEvent.current);
1520
+ }
1521
+ }
1522
+ },
1523
+ flush: () => {
1524
+ const { events, internal } = store.getState();
1525
+ flushDeferredPointers();
1526
+ if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
1527
+ events.handlers.onPointerMove(internal.lastEvent.current);
1528
+ }
947
1529
  },
948
1530
  connect: (target) => {
1531
+ if (!target) return;
949
1532
  const { set, events } = store.getState();
950
1533
  events.disconnect?.();
951
1534
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -969,6 +1552,32 @@ function createPointerEvents(store) {
969
1552
  }
970
1553
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
971
1554
  }
1555
+ },
1556
+ registerPointer: (config) => {
1557
+ const pointerId = nextXRPointerId++;
1558
+ xrPointers.set(pointerId, config);
1559
+ const { internal } = store.getState();
1560
+ getPointerState(internal, pointerId);
1561
+ return pointerId;
1562
+ },
1563
+ unregisterPointer: (pointerId) => {
1564
+ xrPointers.delete(pointerId);
1565
+ const { internal } = store.getState();
1566
+ const pointerState = internal.pointerMap.get(pointerId);
1567
+ if (pointerState) {
1568
+ for (const [, hoveredObj] of pointerState.hovered) {
1569
+ const eventObject = hoveredObj.eventObject;
1570
+ const instance = eventObject.__r3f;
1571
+ if (instance?.eventCount) {
1572
+ const handlers = instance.handlers;
1573
+ const data = { ...hoveredObj, intersections: [] };
1574
+ handlers.onPointerOut?.(data);
1575
+ handlers.onPointerLeave?.(data);
1576
+ }
1577
+ }
1578
+ internal.pointerMap.delete(pointerId);
1579
+ }
1580
+ internal.pointerDirty.delete(pointerId);
972
1581
  }
973
1582
  };
974
1583
  }
@@ -1059,23 +1668,29 @@ const createStore = (invalidate, advance) => {
1059
1668
  set,
1060
1669
  get,
1061
1670
  // Mock objects that have to be configured
1671
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
1672
+ primaryStore: null,
1062
1673
  gl: null,
1063
1674
  renderer: null,
1064
1675
  camera: null,
1065
1676
  frustum: new Frustum(),
1066
1677
  autoUpdateFrustum: true,
1067
1678
  raycaster: null,
1068
- events: { priority: 1, enabled: true, connected: false },
1679
+ events: {
1680
+ priority: 1,
1681
+ enabled: true,
1682
+ connected: false,
1683
+ frameTimedRaycasts: true,
1684
+ alwaysFireOnScroll: true,
1685
+ updateOnFrame: false
1686
+ },
1069
1687
  scene: null,
1070
1688
  rootScene: null,
1071
1689
  xr: null,
1072
1690
  inspector: null,
1073
1691
  invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
1074
1692
  advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
1075
- legacy: false,
1076
- linear: false,
1077
- flat: false,
1078
- textureColorSpace: "srgb",
1693
+ textureColorSpace: SRGBColorSpace,
1079
1694
  isLegacy: false,
1080
1695
  webGPUSupported: false,
1081
1696
  isNative: false,
@@ -1133,6 +1748,7 @@ const createStore = (invalidate, advance) => {
1133
1748
  size: newSize,
1134
1749
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
1135
1750
  }));
1751
+ getScheduler().invalidate();
1136
1752
  }
1137
1753
  }
1138
1754
  return;
@@ -1147,6 +1763,7 @@ const createStore = (invalidate, advance) => {
1147
1763
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
1148
1764
  _sizeImperative: true
1149
1765
  }));
1766
+ getScheduler().invalidate();
1150
1767
  },
1151
1768
  setDpr: (dpr) => set((state2) => {
1152
1769
  const resolved = calculateDpr(dpr);
@@ -1157,11 +1774,14 @@ const createStore = (invalidate, advance) => {
1157
1774
  },
1158
1775
  setError: (error) => set(() => ({ error })),
1159
1776
  error: null,
1160
- //* TSL State (managed via hooks: useUniforms, useNodes, useTextures, usePostProcessing) ==============================
1777
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
1161
1778
  uniforms: {},
1162
1779
  nodes: {},
1780
+ buffers: {},
1781
+ gpuStorage: {},
1163
1782
  textures: /* @__PURE__ */ new Map(),
1164
- postProcessing: null,
1783
+ _textureRefs: /* @__PURE__ */ new Map(),
1784
+ renderPipeline: null,
1165
1785
  passes: {},
1166
1786
  _hmrVersion: 0,
1167
1787
  _sizeImperative: false,
@@ -1170,12 +1790,16 @@ const createStore = (invalidate, advance) => {
1170
1790
  internal: {
1171
1791
  // Events
1172
1792
  interaction: [],
1173
- hovered: /* @__PURE__ */ new Map(),
1174
1793
  subscribers: [],
1794
+ // Per-pointer state (new unified structure)
1795
+ pointerMap: /* @__PURE__ */ new Map(),
1796
+ pointerDirty: /* @__PURE__ */ new Map(),
1797
+ lastEvent: React.createRef(),
1798
+ // Deprecated but kept for backwards compatibility
1799
+ hovered: /* @__PURE__ */ new Map(),
1175
1800
  initialClick: [0, 0],
1176
1801
  initialHits: [],
1177
1802
  capturedMap: /* @__PURE__ */ new Map(),
1178
- lastEvent: React.createRef(),
1179
1803
  // Visibility tracking (onFramed, onOccluded, onVisible)
1180
1804
  visibilityRegistry: /* @__PURE__ */ new Map(),
1181
1805
  // Occlusion system (WebGPU only)
@@ -1258,13 +1882,22 @@ const createStore = (invalidate, advance) => {
1258
1882
  rootStore.subscribe(() => {
1259
1883
  const { camera, size, viewport, set, internal } = rootStore.getState();
1260
1884
  const actualRenderer = internal.actualRenderer;
1885
+ const canvasTarget = internal.canvasTarget;
1261
1886
  if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1262
1887
  oldSize = size;
1263
1888
  oldDpr = viewport.dpr;
1264
1889
  updateCamera(camera, size);
1265
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1266
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && actualRenderer.domElement instanceof HTMLCanvasElement;
1267
- actualRenderer.setSize(size.width, size.height, updateStyle);
1890
+ if (internal.isSecondary && canvasTarget) {
1891
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1892
+ canvasTarget.setSize(size.width, size.height, false);
1893
+ } else {
1894
+ if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1895
+ actualRenderer.setSize(size.width, size.height, false);
1896
+ if (canvasTarget) {
1897
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1898
+ canvasTarget.setSize(size.width, size.height, false);
1899
+ }
1900
+ }
1268
1901
  }
1269
1902
  if (camera !== oldCamera) {
1270
1903
  oldCamera = camera;
@@ -1335,1124 +1968,86 @@ useLoader.clear = function(loader, input) {
1335
1968
  };
1336
1969
  useLoader.loader = getLoader;
1337
1970
 
1338
- var __defProp$2 = Object.defineProperty;
1339
- var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1340
- var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
1341
- const DEFAULT_PHASES = ["start", "input", "physics", "update", "render", "finish"];
1342
- class PhaseGraph {
1343
- constructor() {
1344
- /** Ordered list of phase nodes */
1345
- __publicField$2(this, "phases", []);
1346
- /** Quick lookup by name */
1347
- __publicField$2(this, "phaseMap", /* @__PURE__ */ new Map());
1348
- /** Cached ordered names (invalidated on changes) */
1349
- __publicField$2(this, "orderedNamesCache", null);
1350
- this.initializeDefaultPhases();
1351
- }
1352
- //* Initialization --------------------------------
1353
- initializeDefaultPhases() {
1354
- for (const name of DEFAULT_PHASES) {
1355
- const node = { name, isAutoGenerated: false };
1356
- this.phases.push(node);
1357
- this.phaseMap.set(name, node);
1358
- }
1359
- this.invalidateCache();
1360
- }
1361
- //* Public API --------------------------------
1362
- /**
1363
- * Add a named phase to the graph
1364
- * @param name - Phase name (must be unique)
1365
- * @param options - Position options (before or after another phase)
1366
- */
1367
- addPhase(name, options = {}) {
1368
- if (this.phaseMap.has(name)) {
1369
- console.warn(`[useFrame] Phase "${name}" already exists`);
1370
- return;
1371
- }
1372
- const { before, after } = options;
1373
- const node = { name, isAutoGenerated: false };
1374
- let insertIndex = this.phases.length;
1375
- const targetIndex = this.getPhaseIndex(before ?? after);
1376
- if (targetIndex !== -1) {
1377
- insertIndex = before ? targetIndex : targetIndex + 1;
1971
+ function useFrame(callback, priorityOrOptions) {
1972
+ const store = React.useContext(context);
1973
+ const isInsideCanvas = store !== null;
1974
+ const scheduler = getScheduler();
1975
+ const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
1976
+ id: priorityOrOptions.id,
1977
+ phase: priorityOrOptions.phase,
1978
+ priority: priorityOrOptions.priority,
1979
+ fps: priorityOrOptions.fps,
1980
+ drop: priorityOrOptions.drop,
1981
+ enabled: priorityOrOptions.enabled,
1982
+ before: priorityOrOptions.before,
1983
+ after: priorityOrOptions.after
1984
+ }) : "";
1985
+ const options = React.useMemo(() => {
1986
+ return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
1987
+ }, [optionsKey]);
1988
+ const reactId = React.useId();
1989
+ const id = options.id ?? reactId;
1990
+ const callbackRef = useMutableCallback(callback);
1991
+ const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
1992
+ useIsomorphicLayoutEffect(() => {
1993
+ if (!callback) return;
1994
+ if (isInsideCanvas) {
1995
+ const state = store.getState();
1996
+ const rootId = state.internal.rootId;
1997
+ if (isLegacyPriority) {
1998
+ state.internal.priority++;
1999
+ let parentRoot = state.previousRoot;
2000
+ while (parentRoot) {
2001
+ const parentState = parentRoot.getState();
2002
+ if (parentState?.internal) parentState.internal.priority++;
2003
+ parentRoot = parentState?.previousRoot;
2004
+ }
2005
+ notifyDepreciated({
2006
+ heading: "useFrame with numeric priority is deprecated",
2007
+ body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
2008
+ link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2009
+ });
2010
+ }
2011
+ const wrappedCallback = (frameState, delta) => {
2012
+ const localState = store.getState();
2013
+ const mergedState = {
2014
+ ...localState,
2015
+ time: frameState.time,
2016
+ delta: frameState.delta,
2017
+ elapsed: frameState.elapsed,
2018
+ frame: frameState.frame
2019
+ };
2020
+ callbackRef.current?.(mergedState, delta);
2021
+ };
2022
+ const unregister = scheduler.register(wrappedCallback, {
2023
+ id,
2024
+ rootId,
2025
+ ...options
2026
+ });
2027
+ return () => {
2028
+ unregister();
2029
+ if (isLegacyPriority) {
2030
+ const currentState = store.getState();
2031
+ if (currentState.internal) {
2032
+ currentState.internal.priority--;
2033
+ let parentRoot = currentState.previousRoot;
2034
+ while (parentRoot) {
2035
+ const parentState = parentRoot.getState();
2036
+ if (parentState?.internal) parentState.internal.priority--;
2037
+ parentRoot = parentState?.previousRoot;
2038
+ }
2039
+ }
2040
+ }
2041
+ };
1378
2042
  } else {
1379
- const constraintType = before ? "before" : "after";
1380
- console.warn(`[useFrame] Phase "${before ?? after}" not found for '${constraintType}' constraint`);
1381
- }
1382
- this.phases.splice(insertIndex, 0, node);
1383
- this.phaseMap.set(name, node);
1384
- this.invalidateCache();
1385
- }
1386
- /**
1387
- * Get ordered list of phase names
1388
- */
1389
- getOrderedPhases() {
1390
- if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
1391
- return this.orderedNamesCache;
1392
- }
1393
- /**
1394
- * Check if a phase exists
1395
- */
1396
- hasPhase(name) {
1397
- return this.phaseMap.has(name);
1398
- }
1399
- /**
1400
- * Get the index of a phase (-1 if not found)
1401
- */
1402
- getPhaseIndex(name) {
1403
- if (!name) return -1;
1404
- return this.phases.findIndex((p) => p.name === name);
1405
- }
1406
- /**
1407
- * Ensure a phase exists, creating an auto-generated one if needed.
1408
- * Used for resolving before/after constraints.
1409
- *
1410
- * @param name - The phase name to ensure exists
1411
- * @returns The phase name (may be auto-generated like 'before:render')
1412
- */
1413
- ensurePhase(name) {
1414
- if (this.phaseMap.has(name)) return name;
1415
- const node = { name, isAutoGenerated: true };
1416
- this.phases.push(node);
1417
- this.phaseMap.set(name, node);
1418
- this.invalidateCache();
1419
- return name;
1420
- }
1421
- /**
1422
- * Resolve where a job with before/after constraints should go.
1423
- * Creates auto-generated phases if needed.
1424
- *
1425
- * @param before - Phase(s) to run before
1426
- * @param after - Phase(s) to run after
1427
- * @returns The resolved phase name
1428
- */
1429
- resolveConstraintPhase(before, after) {
1430
- const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
1431
- const afterArr = after ? Array.isArray(after) ? after : [after] : [];
1432
- if (beforeArr.length > 0) {
1433
- return this.ensureAutoPhase(beforeArr[0], "before", 0);
1434
- }
1435
- if (afterArr.length > 0) {
1436
- return this.ensureAutoPhase(afterArr[0], "after", 1);
1437
- }
1438
- return "update";
1439
- }
1440
- /**
1441
- * Ensure an auto-generated phase exists relative to a target phase.
1442
- * Creates the phase if it doesn't exist, inserting it at the correct position.
1443
- *
1444
- * @param target - The target phase name to position relative to
1445
- * @param prefix - Prefix for auto-generated phase name ('before' or 'after')
1446
- * @param offset - Insertion offset (0 for before, 1 for after)
1447
- * @returns The auto-generated phase name
1448
- */
1449
- ensureAutoPhase(target, prefix, offset) {
1450
- const autoName = `${prefix}:${target}`;
1451
- if (this.phaseMap.has(autoName)) return autoName;
1452
- const node = { name: autoName, isAutoGenerated: true };
1453
- const targetIndex = this.getPhaseIndex(target);
1454
- if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
1455
- else this.phases.push(node);
1456
- this.phaseMap.set(autoName, node);
1457
- this.invalidateCache();
1458
- return autoName;
1459
- }
1460
- // Internal --------------------------------
1461
- invalidateCache() {
1462
- this.orderedNamesCache = null;
1463
- }
1464
- }
1465
-
1466
- function rebuildSortedJobs(jobs, phaseGraph) {
1467
- const orderedPhases = phaseGraph.getOrderedPhases();
1468
- const buckets = /* @__PURE__ */ new Map();
1469
- for (const phase of orderedPhases) {
1470
- buckets.set(phase, []);
1471
- }
1472
- for (const job of jobs.values()) {
1473
- if (!job.enabled) continue;
1474
- let bucket = buckets.get(job.phase);
1475
- if (!bucket) {
1476
- bucket = [];
1477
- buckets.set(job.phase, bucket);
1478
- }
1479
- bucket.push(job);
1480
- }
1481
- const sortedBuckets = [];
1482
- for (const phase of orderedPhases) {
1483
- const bucket = buckets.get(phase);
1484
- if (!bucket || bucket.length === 0) continue;
1485
- bucket.sort((a, b) => {
1486
- if (a.priority !== b.priority) return b.priority - a.priority;
1487
- return a.index - b.index;
1488
- });
1489
- sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
1490
- }
1491
- for (const [phase, bucket] of buckets) {
1492
- if (!orderedPhases.includes(phase) && bucket.length > 0) {
1493
- bucket.sort((a, b) => {
1494
- if (a.priority !== b.priority) return b.priority - a.priority;
1495
- return a.index - b.index;
1496
- });
1497
- sortedBuckets.push(bucket);
1498
- }
1499
- }
1500
- return sortedBuckets.flat();
1501
- }
1502
- function hasCrossJobConstraints(bucket) {
1503
- const jobIds = new Set(bucket.map((j) => j.id));
1504
- for (const job of bucket) {
1505
- for (const ref of job.before) {
1506
- if (jobIds.has(ref)) return true;
1507
- }
1508
- for (const ref of job.after) {
1509
- if (jobIds.has(ref)) return true;
1510
- }
1511
- }
1512
- return false;
1513
- }
1514
- function topologicalSort(jobs) {
1515
- const n = jobs.length;
1516
- if (n <= 1) return jobs;
1517
- const jobMap = /* @__PURE__ */ new Map();
1518
- const inDegree = /* @__PURE__ */ new Map();
1519
- const adjacency = /* @__PURE__ */ new Map();
1520
- for (const job of jobs) {
1521
- jobMap.set(job.id, job);
1522
- inDegree.set(job.id, 0);
1523
- adjacency.set(job.id, []);
1524
- }
1525
- for (const job of jobs) {
1526
- for (const ref of job.before) {
1527
- if (jobMap.has(ref)) {
1528
- adjacency.get(job.id).push(ref);
1529
- inDegree.set(ref, inDegree.get(ref) + 1);
1530
- }
1531
- }
1532
- for (const ref of job.after) {
1533
- if (jobMap.has(ref)) {
1534
- adjacency.get(ref).push(job.id);
1535
- inDegree.set(job.id, inDegree.get(job.id) + 1);
1536
- }
1537
- }
1538
- }
1539
- const queue = [];
1540
- for (const job of jobs) {
1541
- if (inDegree.get(job.id) === 0) {
1542
- queue.push(job);
1543
- }
1544
- }
1545
- queue.sort((a, b) => {
1546
- if (a.priority !== b.priority) return b.priority - a.priority;
1547
- return a.index - b.index;
1548
- });
1549
- const result = [];
1550
- while (queue.length > 0) {
1551
- const job = queue.shift();
1552
- result.push(job);
1553
- const neighbors = adjacency.get(job.id) || [];
1554
- for (const neighborId of neighbors) {
1555
- const newDegree = inDegree.get(neighborId) - 1;
1556
- inDegree.set(neighborId, newDegree);
1557
- if (newDegree === 0) {
1558
- const neighbor = jobMap.get(neighborId);
1559
- insertSorted(queue, neighbor);
1560
- }
1561
- }
1562
- }
1563
- if (result.length !== n) {
1564
- console.warn("[useFrame] Circular dependency detected in job constraints");
1565
- const resultIds = new Set(result.map((j) => j.id));
1566
- for (const job of jobs) {
1567
- if (!resultIds.has(job.id)) result.push(job);
1568
- }
1569
- }
1570
- return result;
1571
- }
1572
- function insertSorted(arr, job) {
1573
- let i = 0;
1574
- while (i < arr.length) {
1575
- const cmp = arr[i];
1576
- if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
1577
- break;
1578
- }
1579
- i++;
1580
- }
1581
- arr.splice(i, 0, job);
1582
- }
1583
-
1584
- function shouldRun(job, now) {
1585
- if (!job.enabled) return false;
1586
- if (!job.fps) return true;
1587
- const minInterval = 1e3 / job.fps;
1588
- const lastRun = job.lastRun ?? 0;
1589
- const elapsed = now - lastRun;
1590
- if (elapsed < minInterval) return false;
1591
- if (job.drop) {
1592
- job.lastRun = now;
1593
- } else {
1594
- const steps = Math.floor(elapsed / minInterval);
1595
- job.lastRun = lastRun + steps * minInterval;
1596
- if (job.lastRun < now - minInterval) {
1597
- job.lastRun = now;
1598
- }
1599
- }
1600
- return true;
1601
- }
1602
- function resetJobTiming(job) {
1603
- job.lastRun = void 0;
1604
- }
1605
-
1606
- var __defProp$1 = Object.defineProperty;
1607
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1608
- var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1609
- const hmrData = (() => {
1610
- if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
1611
- if (typeof import_meta_hot !== "undefined") return import_meta_hot;
1612
- try {
1613
- return (0, eval)("import.meta.hot");
1614
- } catch {
1615
- return void 0;
1616
- }
1617
- })();
1618
- const _Scheduler = class _Scheduler {
1619
- //* Constructor ================================
1620
- constructor() {
1621
- //* Critical State ================================
1622
- __publicField$1(this, "roots", /* @__PURE__ */ new Map());
1623
- __publicField$1(this, "phaseGraph");
1624
- __publicField$1(this, "loopState", {
1625
- running: false,
1626
- rafHandle: null,
1627
- lastTime: null,
1628
- // null = uninitialized, 0+ = valid timestamp
1629
- frameCount: 0,
1630
- elapsedTime: 0,
1631
- createdAt: performance.now()
1632
- });
1633
- __publicField$1(this, "stoppedTime", 0);
1634
- //* Private State ================================
1635
- __publicField$1(this, "nextRootIndex", 0);
1636
- __publicField$1(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
1637
- __publicField$1(this, "globalAfterJobs", /* @__PURE__ */ new Map());
1638
- __publicField$1(this, "nextGlobalIndex", 0);
1639
- __publicField$1(this, "idleCallbacks", /* @__PURE__ */ new Set());
1640
- __publicField$1(this, "nextJobIndex", 0);
1641
- __publicField$1(this, "jobStateListeners", /* @__PURE__ */ new Map());
1642
- __publicField$1(this, "pendingFrames", 0);
1643
- __publicField$1(this, "_frameloop", "always");
1644
- //* Independent Mode & Error Handling State ================================
1645
- __publicField$1(this, "_independent", false);
1646
- __publicField$1(this, "errorHandler", null);
1647
- __publicField$1(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1648
- //* Core Loop Execution Methods ================================
1649
- /**
1650
- * Main RAF loop callback.
1651
- * Executes frame, handles demand mode, and schedules next frame.
1652
- * @param {number} timestamp - RAF timestamp in milliseconds
1653
- * @returns {void}
1654
- * @private
1655
- */
1656
- __publicField$1(this, "loop", (timestamp) => {
1657
- if (!this.loopState.running) return;
1658
- this.executeFrame(timestamp);
1659
- if (this._frameloop === "demand") {
1660
- this.pendingFrames = Math.max(0, this.pendingFrames - 1);
1661
- if (this.pendingFrames === 0) {
1662
- this.notifyIdle(timestamp);
1663
- return this.stop();
1664
- }
1665
- }
1666
- this.loopState.rafHandle = requestAnimationFrame(this.loop);
1667
- });
1668
- this.phaseGraph = new PhaseGraph();
1669
- }
1670
- static get instance() {
1671
- return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1672
- }
1673
- static set instance(value) {
1674
- globalThis[_Scheduler.INSTANCE_KEY] = value;
1675
- }
1676
- /**
1677
- * Get the global scheduler instance (creates if doesn't exist).
1678
- * Uses HMR data to preserve instance across hot reloads.
1679
- * @returns {Scheduler} The singleton scheduler instance
1680
- */
1681
- static get() {
1682
- if (!_Scheduler.instance && hmrData?.data?.scheduler) {
1683
- _Scheduler.instance = hmrData.data.scheduler;
1684
- }
1685
- if (!_Scheduler.instance) {
1686
- _Scheduler.instance = new _Scheduler();
1687
- if (hmrData?.data) {
1688
- hmrData.data.scheduler = _Scheduler.instance;
1689
- }
1690
- }
1691
- return _Scheduler.instance;
1692
- }
1693
- /**
1694
- * Reset the singleton instance. Stops the loop and clears all state.
1695
- * Primarily used for testing to ensure clean state between tests.
1696
- * @returns {void}
1697
- */
1698
- static reset() {
1699
- if (_Scheduler.instance) {
1700
- _Scheduler.instance.stop();
1701
- _Scheduler.instance = null;
1702
- }
1703
- if (hmrData?.data) {
1704
- hmrData.data.scheduler = null;
1705
- }
1706
- }
1707
- //* Getters & Setters ================================
1708
- get phases() {
1709
- return this.phaseGraph.getOrderedPhases();
1710
- }
1711
- get frameloop() {
1712
- return this._frameloop;
1713
- }
1714
- set frameloop(mode) {
1715
- if (this._frameloop === mode) return;
1716
- const wasAlways = this._frameloop === "always";
1717
- this._frameloop = mode;
1718
- if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
1719
- else if (mode !== "always" && wasAlways) this.stop();
1720
- }
1721
- get isRunning() {
1722
- return this.loopState.running;
1723
- }
1724
- get isReady() {
1725
- return this.roots.size > 0;
1726
- }
1727
- get independent() {
1728
- return this._independent;
1729
- }
1730
- set independent(value) {
1731
- this._independent = value;
1732
- if (value) this.ensureDefaultRoot();
1733
- }
1734
- //* Root Management Methods ================================
1735
- /**
1736
- * Register a root (Canvas) with the scheduler.
1737
- * The first root to register starts the RAF loop (if frameloop='always').
1738
- * @param {string} id - Unique identifier for this root
1739
- * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
1740
- * @returns {() => void} Unsubscribe function to remove this root
1741
- */
1742
- registerRoot(id, options = {}) {
1743
- if (this.roots.has(id)) {
1744
- console.warn(`[Scheduler] Root "${id}" already registered`);
1745
- return () => this.unregisterRoot(id);
1746
- }
1747
- const entry = {
1748
- id,
1749
- getState: options.getState ?? (() => ({})),
1750
- jobs: /* @__PURE__ */ new Map(),
1751
- sortedJobs: [],
1752
- needsRebuild: false
1753
- };
1754
- if (options.onError) {
1755
- this.errorHandler = options.onError;
1756
- }
1757
- this.roots.set(id, entry);
1758
- if (this.roots.size === 1) {
1759
- this.notifyRootReady();
1760
- if (this._frameloop === "always") this.start();
1761
- }
1762
- return () => this.unregisterRoot(id);
1763
- }
1764
- /**
1765
- * Unregister a root from the scheduler.
1766
- * Cleans up all job state listeners for this root's jobs.
1767
- * The last root to unregister stops the RAF loop.
1768
- * @param {string} id - The root ID to unregister
1769
- * @returns {void}
1770
- */
1771
- unregisterRoot(id) {
1772
- const root = this.roots.get(id);
1773
- if (!root) return;
1774
- for (const jobId of root.jobs.keys()) {
1775
- this.jobStateListeners.delete(jobId);
1776
- }
1777
- this.roots.delete(id);
1778
- if (this.roots.size === 0) {
1779
- this.stop();
1780
- this.errorHandler = null;
1781
- }
1782
- }
1783
- /**
1784
- * Subscribe to be notified when a root becomes available.
1785
- * Fires immediately if a root already exists.
1786
- * @param {() => void} callback - Function called when first root registers
1787
- * @returns {() => void} Unsubscribe function
1788
- */
1789
- onRootReady(callback) {
1790
- if (this.roots.size > 0) {
1791
- callback();
1792
- return () => {
1793
- };
1794
- }
1795
- this.rootReadyCallbacks.add(callback);
1796
- return () => this.rootReadyCallbacks.delete(callback);
1797
- }
1798
- /**
1799
- * Notify all registered root-ready callbacks.
1800
- * Called when the first root registers.
1801
- * @returns {void}
1802
- * @private
1803
- */
1804
- notifyRootReady() {
1805
- for (const cb of this.rootReadyCallbacks) {
1806
- try {
1807
- cb();
1808
- } catch (error) {
1809
- console.error("[Scheduler] Error in root-ready callback:", error);
1810
- }
1811
- }
1812
- this.rootReadyCallbacks.clear();
1813
- }
1814
- /**
1815
- * Ensure a default root exists for independent mode.
1816
- * Creates a minimal root with no state provider.
1817
- * @returns {void}
1818
- * @private
1819
- */
1820
- ensureDefaultRoot() {
1821
- if (!this.roots.has("__default__")) {
1822
- this.registerRoot("__default__");
1823
- }
1824
- }
1825
- /**
1826
- * Trigger error handling for job errors.
1827
- * Uses the bound error handler if available, otherwise logs to console.
1828
- * @param {Error} error - The error to handle
1829
- * @returns {void}
1830
- */
1831
- triggerError(error) {
1832
- if (this.errorHandler) this.errorHandler(error);
1833
- else console.error("[Scheduler]", error);
1834
- }
1835
- //* Phase Management Methods ================================
1836
- /**
1837
- * Add a named phase to the scheduler's execution order.
1838
- * Marks all roots for rebuild to incorporate the new phase.
1839
- * @param {string} name - The phase name (e.g., 'physics', 'postprocess')
1840
- * @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
1841
- * @returns {void}
1842
- * @example
1843
- * scheduler.addPhase('physics', { before: 'update' });
1844
- * scheduler.addPhase('postprocess', { after: 'render' });
1845
- */
1846
- addPhase(name, options) {
1847
- this.phaseGraph.addPhase(name, options);
1848
- for (const root of this.roots.values()) {
1849
- root.needsRebuild = true;
1850
- }
1851
- }
1852
- /**
1853
- * Check if a phase exists in the scheduler.
1854
- * @param {string} name - The phase name to check
1855
- * @returns {boolean} True if the phase exists
1856
- */
1857
- hasPhase(name) {
1858
- return this.phaseGraph.hasPhase(name);
1859
- }
1860
- //* Global Job Registration Methods (Deprecated APIs) ================================
1861
- /**
1862
- * Register a global job that runs once per frame (not per-root).
1863
- * Used internally by deprecated addEffect/addAfterEffect APIs.
1864
- * @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
1865
- * @param {string} id - Unique identifier for this global job
1866
- * @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
1867
- * @returns {() => void} Unsubscribe function to remove this global job
1868
- * @deprecated Use useFrame with phases instead
1869
- */
1870
- registerGlobal(phase, id, callback) {
1871
- const job = { id, callback };
1872
- if (phase === "before") {
1873
- this.globalBeforeJobs.set(id, job);
1874
- } else {
1875
- this.globalAfterJobs.set(id, job);
1876
- }
1877
- return () => {
1878
- if (phase === "before") this.globalBeforeJobs.delete(id);
1879
- else this.globalAfterJobs.delete(id);
1880
- };
1881
- }
1882
- //* Idle Callback Methods (Deprecated API) ================================
1883
- /**
1884
- * Register an idle callback that fires when the loop stops.
1885
- * Used internally by deprecated addTail API.
1886
- * @param {(timestamp: number) => void} callback - Function called when loop becomes idle
1887
- * @returns {() => void} Unsubscribe function to remove this idle callback
1888
- * @deprecated Use demand mode with invalidate() instead
1889
- */
1890
- onIdle(callback) {
1891
- this.idleCallbacks.add(callback);
1892
- return () => this.idleCallbacks.delete(callback);
1893
- }
1894
- /**
1895
- * Notify all registered idle callbacks.
1896
- * Called when the loop stops in demand mode.
1897
- * @param {number} timestamp - The RAF timestamp when idle occurred
1898
- * @returns {void}
1899
- * @private
1900
- */
1901
- notifyIdle(timestamp) {
1902
- for (const cb of this.idleCallbacks) {
1903
- try {
1904
- cb(timestamp);
1905
- } catch (error) {
1906
- console.error("[Scheduler] Error in idle callback:", error);
1907
- }
1908
- }
1909
- }
1910
- //* Job Registration & Management Methods ================================
1911
- /**
1912
- * Register a job (frame callback) with a specific root.
1913
- * This is the core registration method used by useFrame internally.
1914
- * @param {FrameNextCallback} callback - The function to call each frame
1915
- * @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
1916
- * @param {string} [options.rootId] - Target root ID (defaults to first registered root)
1917
- * @param {string} [options.id] - Unique job ID (auto-generated if not provided)
1918
- * @param {string} [options.phase] - Execution phase (defaults to 'update')
1919
- * @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
1920
- * @param {number} [options.fps] - FPS throttle limit
1921
- * @param {boolean} [options.drop] - Drop frames when behind (default true)
1922
- * @param {boolean} [options.enabled] - Whether job is active (default true)
1923
- * @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
1924
- * @returns {() => void} Unsubscribe function to remove this job
1925
- */
1926
- register(callback, options = {}) {
1927
- const rootId = options.rootId;
1928
- const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
1929
- if (!root) {
1930
- console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
1931
- return () => {
1932
- };
1933
- }
1934
- const id = options.id ?? this.generateJobId();
1935
- let phase = options.phase ?? "update";
1936
- if (!options.phase && (options.before || options.after)) {
1937
- phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
1938
- }
1939
- const before = this.normalizeConstraints(options.before);
1940
- const after = this.normalizeConstraints(options.after);
1941
- const job = {
1942
- id,
1943
- callback,
1944
- phase,
1945
- before,
1946
- after,
1947
- priority: options.priority ?? 0,
1948
- index: this.nextJobIndex++,
1949
- fps: options.fps,
1950
- drop: options.drop ?? true,
1951
- enabled: options.enabled ?? true,
1952
- system: options.system ?? false
1953
- };
1954
- if (root.jobs.has(id)) {
1955
- console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
1956
- }
1957
- root.jobs.set(id, job);
1958
- root.needsRebuild = true;
1959
- return () => this.unregister(id, root.id);
1960
- }
1961
- /**
1962
- * Unregister a job by its ID.
1963
- * Searches all roots if rootId is not provided.
1964
- * @param {string} id - The job ID to unregister
1965
- * @param {string} [rootId] - Optional root ID to search (searches all if not provided)
1966
- * @returns {void}
1967
- */
1968
- unregister(id, rootId) {
1969
- const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
1970
- if (root?.jobs.delete(id)) {
1971
- root.needsRebuild = true;
1972
- this.jobStateListeners.delete(id);
1973
- }
1974
- }
1975
- /**
1976
- * Update a job's options dynamically.
1977
- * Searches all roots to find the job by ID.
1978
- * Phase/constraint changes trigger a rebuild of the sorted job list.
1979
- * @param {string} id - The job ID to update
1980
- * @param {Partial<JobOptions>} options - The options to update
1981
- * @returns {void}
1982
- */
1983
- updateJob(id, options) {
1984
- let job;
1985
- let root;
1986
- for (const r of this.roots.values()) {
1987
- job = r.jobs.get(id);
1988
- if (job) {
1989
- root = r;
1990
- break;
1991
- }
1992
- }
1993
- if (!job || !root) return;
1994
- if (options.priority !== void 0) job.priority = options.priority;
1995
- if (options.fps !== void 0) job.fps = options.fps;
1996
- if (options.drop !== void 0) job.drop = options.drop;
1997
- if (options.enabled !== void 0) {
1998
- const wasEnabled = job.enabled;
1999
- job.enabled = options.enabled;
2000
- if (!wasEnabled && job.enabled) resetJobTiming(job);
2001
- if (wasEnabled !== job.enabled) root.needsRebuild = true;
2002
- }
2003
- if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
2004
- if (options.phase) job.phase = options.phase;
2005
- if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
2006
- if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
2007
- root.needsRebuild = true;
2008
- }
2009
- }
2010
- //* Job State Management Methods ================================
2011
- /**
2012
- * Check if a job is currently paused (disabled).
2013
- * @param {string} id - The job ID to check
2014
- * @returns {boolean} True if the job exists and is paused
2015
- */
2016
- isJobPaused(id) {
2017
- for (const root of this.roots.values()) {
2018
- const job = root.jobs.get(id);
2019
- if (job) return !job.enabled;
2020
- }
2021
- return false;
2022
- }
2023
- /**
2024
- * Subscribe to state changes for a specific job.
2025
- * Listener is called when job is paused or resumed.
2026
- * @param {string} id - The job ID to subscribe to
2027
- * @param {() => void} listener - Callback invoked on state changes
2028
- * @returns {() => void} Unsubscribe function
2029
- */
2030
- subscribeJobState(id, listener) {
2031
- if (!this.jobStateListeners.has(id)) {
2032
- this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
2033
- }
2034
- this.jobStateListeners.get(id).add(listener);
2035
- return () => {
2036
- this.jobStateListeners.get(id)?.delete(listener);
2037
- if (this.jobStateListeners.get(id)?.size === 0) {
2038
- this.jobStateListeners.delete(id);
2039
- }
2040
- };
2041
- }
2042
- /**
2043
- * Notify all listeners that a job's state has changed.
2044
- * @param {string} id - The job ID that changed
2045
- * @returns {void}
2046
- * @private
2047
- */
2048
- notifyJobStateChange(id) {
2049
- this.jobStateListeners.get(id)?.forEach((listener) => listener());
2050
- }
2051
- /**
2052
- * Pause a job by ID (sets enabled=false).
2053
- * Notifies any subscribed state listeners.
2054
- * @param {string} id - The job ID to pause
2055
- * @returns {void}
2056
- */
2057
- pauseJob(id) {
2058
- this.updateJob(id, { enabled: false });
2059
- this.notifyJobStateChange(id);
2060
- }
2061
- /**
2062
- * Resume a paused job by ID (sets enabled=true).
2063
- * Resets job timing to prevent frame accumulation.
2064
- * Notifies any subscribed state listeners.
2065
- * @param {string} id - The job ID to resume
2066
- * @returns {void}
2067
- */
2068
- resumeJob(id) {
2069
- this.updateJob(id, { enabled: true });
2070
- this.notifyJobStateChange(id);
2071
- }
2072
- //* Frame Loop Control Methods ================================
2073
- /**
2074
- * Start the requestAnimationFrame loop.
2075
- * Resets timing state (elapsedTime, frameCount) on start.
2076
- * No-op if already running.
2077
- * @returns {void}
2078
- */
2079
- start() {
2080
- if (this.loopState.running) return;
2081
- const { elapsedTime, createdAt } = this.loopState;
2082
- let adjustedCreated = 0;
2083
- if (this.stoppedTime > 0) {
2084
- adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
2085
- this.stoppedTime = 0;
2086
- }
2087
- Object.assign(this.loopState, {
2088
- running: true,
2089
- elapsedTime: elapsedTime ?? 0,
2090
- lastTime: performance.now(),
2091
- createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
2092
- frameCount: 0,
2093
- rafHandle: requestAnimationFrame(this.loop)
2094
- });
2095
- }
2096
- /**
2097
- * Stop the requestAnimationFrame loop.
2098
- * Cancels any pending RAF callback.
2099
- * No-op if not running.
2100
- * @returns {void}
2101
- */
2102
- stop() {
2103
- if (!this.loopState.running) return;
2104
- this.loopState.running = false;
2105
- if (this.loopState.rafHandle !== null) {
2106
- cancelAnimationFrame(this.loopState.rafHandle);
2107
- this.loopState.rafHandle = null;
2108
- }
2109
- this.stoppedTime = performance.now();
2110
- }
2111
- /**
2112
- * Request frames to be rendered in demand mode.
2113
- * Accumulates pending frames (capped at 60) and starts the loop if not running.
2114
- * No-op if frameloop is not 'demand'.
2115
- * @param {number} [frames=1] - Number of frames to request
2116
- * @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
2117
- * - `false` (default): Sets pending frames to the specified value (replaces existing count)
2118
- * - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
2119
- * @returns {void}
2120
- * @example
2121
- * // Request a single frame render
2122
- * scheduler.invalidate();
2123
- *
2124
- * @example
2125
- * // Request 5 frames (e.g., for animations)
2126
- * scheduler.invalidate(5);
2127
- *
2128
- * @example
2129
- * // Set pending frames to exactly 3 (don't stack with existing)
2130
- * scheduler.invalidate(3, false);
2131
- *
2132
- * @example
2133
- * // Add 2 more frames to existing pending count
2134
- * scheduler.invalidate(2, true);
2135
- */
2136
- invalidate(frames = 1, stackFrames = false) {
2137
- if (this._frameloop !== "demand") return;
2138
- const baseFrames = stackFrames ? this.pendingFrames : 0;
2139
- this.pendingFrames = Math.min(60, baseFrames + frames);
2140
- if (!this.loopState.running && this.pendingFrames > 0) this.start();
2141
- }
2142
- /**
2143
- * Reset timing state for deterministic testing.
2144
- * Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
2145
- * @returns {void}
2146
- */
2147
- resetTiming() {
2148
- this.loopState.lastTime = null;
2149
- this.loopState.frameCount = 0;
2150
- this.loopState.elapsedTime = 0;
2151
- this.loopState.createdAt = performance.now();
2152
- }
2153
- //* Manual Stepping Methods ================================
2154
- /**
2155
- * Manually execute a single frame for all roots.
2156
- * Useful for frameloop='never' mode or testing scenarios.
2157
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2158
- * @returns {void}
2159
- * @example
2160
- * // Manual control mode
2161
- * scheduler.frameloop = 'never';
2162
- * scheduler.step(); // Execute one frame
2163
- */
2164
- step(timestamp) {
2165
- const now = timestamp ?? performance.now();
2166
- this.executeFrame(now);
2167
- }
2168
- /**
2169
- * Manually execute a single job by its ID.
2170
- * Useful for testing individual job callbacks in isolation.
2171
- * @param {string} id - The job ID to step
2172
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2173
- * @returns {void}
2174
- */
2175
- stepJob(id, timestamp) {
2176
- let job;
2177
- let root;
2178
- for (const r of this.roots.values()) {
2179
- job = r.jobs.get(id);
2180
- if (job) {
2181
- root = r;
2182
- break;
2183
- }
2184
- }
2185
- if (!job || !root) {
2186
- console.warn(`[Scheduler] Job "${id}" not found`);
2187
- return;
2188
- }
2189
- const now = timestamp ?? performance.now();
2190
- const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
2191
- const delta = deltaMs / 1e3;
2192
- const elapsed = now - this.loopState.createdAt;
2193
- const providedState = root.getState?.() ?? {};
2194
- const frameState = {
2195
- ...providedState,
2196
- time: now,
2197
- delta,
2198
- elapsed,
2199
- frame: this.loopState.frameCount
2200
- };
2201
- try {
2202
- job.callback(frameState, delta);
2203
- } catch (error) {
2204
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2205
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2206
- }
2207
- }
2208
- /**
2209
- * Execute a single frame across all roots.
2210
- * Order: globalBefore → each root's jobs → globalAfter
2211
- * @param {number} timestamp - RAF timestamp in milliseconds
2212
- * @returns {void}
2213
- * @private
2214
- */
2215
- executeFrame(timestamp) {
2216
- const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
2217
- const delta = deltaMs / 1e3;
2218
- this.loopState.lastTime = timestamp;
2219
- this.loopState.frameCount++;
2220
- this.loopState.elapsedTime += deltaMs;
2221
- this.runGlobalJobs(this.globalBeforeJobs, timestamp);
2222
- for (const root of this.roots.values()) {
2223
- this.tickRoot(root, timestamp, delta);
2224
- }
2225
- this.runGlobalJobs(this.globalAfterJobs, timestamp);
2226
- }
2227
- /**
2228
- * Run all global jobs from a job map.
2229
- * Catches and logs errors without stopping execution.
2230
- * @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
2231
- * @param {number} timestamp - RAF timestamp in milliseconds
2232
- * @returns {void}
2233
- * @private
2234
- */
2235
- runGlobalJobs(jobs, timestamp) {
2236
- for (const job of jobs.values()) {
2237
- try {
2238
- job.callback(timestamp);
2239
- } catch (error) {
2240
- console.error(`[Scheduler] Error in global job "${job.id}":`, error);
2241
- }
2242
- }
2243
- }
2244
- /**
2245
- * Execute all jobs for a single root in sorted order.
2246
- * Rebuilds sorted job list if needed, then dispatches each job.
2247
- * Errors are caught and propagated via triggerError.
2248
- * @param {RootEntry} root - The root entry to tick
2249
- * @param {number} timestamp - RAF timestamp in milliseconds
2250
- * @param {number} delta - Time since last frame in seconds
2251
- * @returns {void}
2252
- * @private
2253
- */
2254
- tickRoot(root, timestamp, delta) {
2255
- if (root.needsRebuild) {
2256
- root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
2257
- root.needsRebuild = false;
2258
- }
2259
- const providedState = root.getState?.() ?? {};
2260
- const frameState = {
2261
- ...providedState,
2262
- time: timestamp,
2263
- delta,
2264
- elapsed: this.loopState.elapsedTime / 1e3,
2265
- // Convert ms to seconds
2266
- frame: this.loopState.frameCount
2267
- };
2268
- for (const job of root.sortedJobs) {
2269
- if (!shouldRun(job, timestamp)) continue;
2270
- try {
2271
- job.callback(frameState, delta);
2272
- } catch (error) {
2273
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2274
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2275
- }
2276
- }
2277
- }
2278
- //* Debug & Inspection Methods ================================
2279
- /**
2280
- * Get the total number of registered jobs across all roots.
2281
- * Includes both per-root jobs and global before/after jobs.
2282
- * @returns {number} Total job count
2283
- */
2284
- getJobCount() {
2285
- let count = 0;
2286
- for (const root of this.roots.values()) {
2287
- count += root.jobs.size;
2288
- }
2289
- return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
2290
- }
2291
- /**
2292
- * Get all registered job IDs across all roots.
2293
- * Includes both per-root jobs and global before/after jobs.
2294
- * @returns {string[]} Array of all job IDs
2295
- */
2296
- getJobIds() {
2297
- const ids = [];
2298
- for (const root of this.roots.values()) {
2299
- ids.push(...root.jobs.keys());
2300
- }
2301
- ids.push(...this.globalBeforeJobs.keys());
2302
- ids.push(...this.globalAfterJobs.keys());
2303
- return ids;
2304
- }
2305
- /**
2306
- * Get the number of registered roots (Canvas instances).
2307
- * @returns {number} Number of registered roots
2308
- */
2309
- getRootCount() {
2310
- return this.roots.size;
2311
- }
2312
- /**
2313
- * Check if any user (non-system) jobs are registered in a specific phase.
2314
- * Used by the default render job to know if a user has taken over rendering.
2315
- *
2316
- * @param phase The phase to check
2317
- * @param rootId Optional root ID to check (checks all roots if not provided)
2318
- * @returns true if any user jobs exist in the phase
2319
- */
2320
- hasUserJobsInPhase(phase, rootId) {
2321
- const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
2322
- return rootsToCheck.some((root) => {
2323
- if (!root) return false;
2324
- for (const job of root.jobs.values()) {
2325
- if (job.phase === phase && !job.system && job.enabled) return true;
2326
- }
2327
- return false;
2328
- });
2329
- }
2330
- //* Utility Methods ================================
2331
- /**
2332
- * Generate a unique root ID for automatic root registration.
2333
- * @returns {string} A unique root ID in the format 'root_N'
2334
- */
2335
- generateRootId() {
2336
- return `root_${this.nextRootIndex++}`;
2337
- }
2338
- /**
2339
- * Generate a unique job ID.
2340
- * @returns {string} A unique job ID in the format 'job_N'
2341
- * @private
2342
- */
2343
- generateJobId() {
2344
- return `job_${this.nextJobIndex}`;
2345
- }
2346
- /**
2347
- * Normalize before/after constraints to a Set.
2348
- * Handles undefined, single string, or array inputs.
2349
- * @param {string | string[] | undefined} value - The constraint value(s)
2350
- * @returns {Set<string>} Normalized Set of constraint strings
2351
- * @private
2352
- */
2353
- normalizeConstraints(value) {
2354
- if (!value) return /* @__PURE__ */ new Set();
2355
- if (Array.isArray(value)) return new Set(value);
2356
- return /* @__PURE__ */ new Set([value]);
2357
- }
2358
- };
2359
- //* Static State & Methods (Singleton Usage) ================================
2360
- //* Cross-Bundle Singleton Key ==============================
2361
- // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2362
- // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2363
- __publicField$1(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
2364
- let Scheduler = _Scheduler;
2365
- const getScheduler = () => Scheduler.get();
2366
- if (hmrData) {
2367
- hmrData.accept?.();
2368
- }
2369
-
2370
- function useFrame(callback, priorityOrOptions) {
2371
- const store = React.useContext(context);
2372
- const isInsideCanvas = store !== null;
2373
- const scheduler = getScheduler();
2374
- const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
2375
- id: priorityOrOptions.id,
2376
- phase: priorityOrOptions.phase,
2377
- priority: priorityOrOptions.priority,
2378
- fps: priorityOrOptions.fps,
2379
- drop: priorityOrOptions.drop,
2380
- enabled: priorityOrOptions.enabled,
2381
- before: priorityOrOptions.before,
2382
- after: priorityOrOptions.after
2383
- }) : "";
2384
- const options = React.useMemo(() => {
2385
- return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
2386
- }, [optionsKey]);
2387
- const reactId = React.useId();
2388
- const id = options.id ?? reactId;
2389
- const callbackRef = useMutableCallback(callback);
2390
- const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
2391
- useIsomorphicLayoutEffect(() => {
2392
- if (!callback) return;
2393
- if (isInsideCanvas) {
2394
- const state = store.getState();
2395
- const rootId = state.internal.rootId;
2396
- if (isLegacyPriority) {
2397
- state.internal.priority++;
2398
- let parentRoot = state.previousRoot;
2399
- while (parentRoot) {
2400
- const parentState = parentRoot.getState();
2401
- if (parentState?.internal) parentState.internal.priority++;
2402
- parentRoot = parentState?.previousRoot;
2403
- }
2404
- notifyDepreciated({
2405
- heading: "useFrame with numeric priority is deprecated",
2406
- body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
2407
- link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2408
- });
2409
- }
2410
- const wrappedCallback = (frameState, delta) => {
2411
- const localState = store.getState();
2412
- const mergedState = {
2413
- ...localState,
2414
- time: frameState.time,
2415
- delta: frameState.delta,
2416
- elapsed: frameState.elapsed,
2417
- frame: frameState.frame
2418
- };
2419
- callbackRef.current?.(mergedState, delta);
2420
- };
2421
- const unregister = scheduler.register(wrappedCallback, {
2422
- id,
2423
- rootId,
2424
- ...options
2425
- });
2426
- return () => {
2427
- unregister();
2428
- if (isLegacyPriority) {
2429
- const currentState = store.getState();
2430
- if (currentState.internal) {
2431
- currentState.internal.priority--;
2432
- let parentRoot = currentState.previousRoot;
2433
- while (parentRoot) {
2434
- const parentState = parentRoot.getState();
2435
- if (parentState?.internal) parentState.internal.priority--;
2436
- parentRoot = parentState?.previousRoot;
2437
- }
2438
- }
2439
- }
2440
- };
2441
- } else {
2442
- const registerOutside = () => {
2443
- return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
2444
- };
2445
- if (scheduler.independent || scheduler.isReady) {
2446
- return registerOutside();
2447
- }
2448
- let unregisterJob = null;
2449
- const unsubReady = scheduler.onRootReady(() => {
2450
- unregisterJob = registerOutside();
2451
- });
2452
- return () => {
2453
- unsubReady();
2454
- unregisterJob?.();
2455
- };
2043
+ return scheduler.register(
2044
+ (state, delta) => {
2045
+ const frameState = state;
2046
+ if (!frameState.renderer) return;
2047
+ callbackRef.current?.(frameState, delta);
2048
+ },
2049
+ { id, ...options }
2050
+ );
2456
2051
  }
2457
2052
  }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2458
2053
  const isPaused = React.useSyncExternalStore(
@@ -2543,22 +2138,29 @@ function buildFromCache(input, textureCache) {
2543
2138
  function useTexture(input, optionsOrOnLoad) {
2544
2139
  const renderer = useThree((state) => state.internal.actualRenderer);
2545
2140
  const store = useStore();
2546
- const textureCache = useThree((state) => state.textures);
2547
2141
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
2548
- const { onLoad, cache = false } = options;
2142
+ const { onLoad, cache = true } = options;
2143
+ const onLoadRef = useRef(onLoad);
2144
+ onLoadRef.current = onLoad;
2145
+ const onLoadCalledForRef = useRef(null);
2549
2146
  const urls = useMemo(() => getUrls(input), [input]);
2550
2147
  const cachedResult = useMemo(() => {
2551
2148
  if (!cache) return null;
2552
- if (!allUrlsCached(urls, textureCache)) return null;
2553
- return buildFromCache(input, textureCache);
2554
- }, [cache, urls, textureCache, input]);
2149
+ const textures = store.getState().textures;
2150
+ if (!allUrlsCached(urls, textures)) return null;
2151
+ return buildFromCache(input, textures);
2152
+ }, [cache, urls, input, store]);
2555
2153
  const loadedTextures = useLoader(
2556
2154
  TextureLoader,
2557
2155
  IsObject(input) ? Object.values(input) : input
2558
2156
  );
2157
+ const inputKey = urls.join("\0");
2559
2158
  useLayoutEffect(() => {
2560
- if (!cachedResult) onLoad?.(loadedTextures);
2561
- }, [onLoad, cachedResult, loadedTextures]);
2159
+ if (cachedResult) return;
2160
+ if (onLoadCalledForRef.current === inputKey) return;
2161
+ onLoadCalledForRef.current = inputKey;
2162
+ onLoadRef.current?.(loadedTextures);
2163
+ }, [cachedResult, loadedTextures, inputKey]);
2562
2164
  useEffect(() => {
2563
2165
  if (cachedResult) return;
2564
2166
  if ("initTexture" in renderer) {
@@ -2591,8 +2193,6 @@ function useTexture(input, optionsOrOnLoad) {
2591
2193
  }, [input, loadedTextures, cachedResult]);
2592
2194
  useEffect(() => {
2593
2195
  if (!cache) return;
2594
- if (cachedResult) return;
2595
- const set = store.setState;
2596
2196
  const urlTextureMap = [];
2597
2197
  if (typeof input === "string") {
2598
2198
  urlTextureMap.push([input, mappedTextures]);
@@ -2606,18 +2206,32 @@ function useTexture(input, optionsOrOnLoad) {
2606
2206
  urlTextureMap.push([url, textureRecord[key]]);
2607
2207
  }
2608
2208
  }
2609
- set((state) => {
2610
- const newMap = new Map(state.textures);
2611
- let changed = false;
2209
+ store.setState((state) => {
2210
+ const refs = new Map(state._textureRefs);
2211
+ let textures = state.textures;
2212
+ let added = false;
2612
2213
  for (const [url, texture] of urlTextureMap) {
2613
- if (!newMap.has(url)) {
2614
- newMap.set(url, texture);
2615
- changed = true;
2214
+ if (!textures.has(url)) {
2215
+ if (!added) {
2216
+ textures = new Map(textures);
2217
+ added = true;
2218
+ }
2219
+ textures.set(url, texture);
2616
2220
  }
2221
+ refs.set(url, (refs.get(url) ?? 0) + 1);
2617
2222
  }
2618
- return changed ? { textures: newMap } : state;
2223
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
2224
+ });
2225
+ return () => store.setState((state) => {
2226
+ const refs = new Map(state._textureRefs);
2227
+ for (const [url] of urlTextureMap) {
2228
+ const next = (refs.get(url) ?? 0) - 1;
2229
+ if (next <= 0) refs.delete(url);
2230
+ else refs.set(url, next);
2231
+ }
2232
+ return { _textureRefs: refs };
2619
2233
  });
2620
- }, [cache, input, mappedTextures, store, cachedResult]);
2234
+ }, [cache, input, mappedTextures, store]);
2621
2235
  return mappedTextures;
2622
2236
  }
2623
2237
  useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
@@ -2633,106 +2247,90 @@ const Texture = ({
2633
2247
  return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
2634
2248
  };
2635
2249
 
2636
- function getTextureValue(entry) {
2637
- if (entry instanceof Texture$1) return entry;
2638
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof Texture$1) {
2639
- return entry.value;
2640
- }
2641
- return null;
2642
- }
2643
- function useTextures() {
2250
+ function useTextures(selector) {
2644
2251
  const store = useStore();
2645
- return useMemo(() => {
2646
- const set = store.setState;
2252
+ const registry = useMemo(() => {
2647
2253
  const getState = store.getState;
2648
- const add = (key, value) => {
2649
- set((state) => {
2650
- const newMap = new Map(state.textures);
2651
- newMap.set(key, value);
2652
- return { textures: newMap };
2653
- });
2654
- };
2655
- const addMultiple = (items) => {
2656
- set((state) => {
2657
- const newMap = new Map(state.textures);
2658
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
2659
- for (const [key, value] of entries) {
2660
- newMap.set(key, value);
2661
- }
2662
- return { textures: newMap };
2663
- });
2664
- };
2665
- const remove = (key) => {
2666
- set((state) => {
2667
- const newMap = new Map(state.textures);
2668
- newMap.delete(key);
2669
- return { textures: newMap };
2670
- });
2671
- };
2672
- const removeMultiple = (keys) => {
2673
- set((state) => {
2674
- const newMap = new Map(state.textures);
2675
- for (const key of keys) newMap.delete(key);
2676
- return { textures: newMap };
2677
- });
2678
- };
2679
- const dispose = (key) => {
2680
- const entry = getState().textures.get(key);
2681
- if (entry) {
2682
- const tex = getTextureValue(entry);
2683
- tex?.dispose();
2684
- }
2685
- remove(key);
2686
- };
2687
- const disposeMultiple = (keys) => {
2688
- const textures = getState().textures;
2689
- for (const key of keys) {
2690
- const entry = textures.get(key);
2691
- if (entry) {
2692
- const tex = getTextureValue(entry);
2693
- tex?.dispose();
2694
- }
2695
- }
2696
- removeMultiple(keys);
2697
- };
2698
- const disposeAll = () => {
2699
- const textures = getState().textures;
2700
- for (const entry of textures.values()) {
2701
- const tex = getTextureValue(entry);
2702
- tex?.dispose();
2703
- }
2704
- set({ textures: /* @__PURE__ */ new Map() });
2705
- };
2254
+ const setState = store.setState;
2255
+ const getOne = (key) => getState().textures.get(key);
2706
2256
  return {
2707
- // Getter for the textures Map (reactive via getState)
2708
- get textures() {
2257
+ get all() {
2709
2258
  return getState().textures;
2710
2259
  },
2711
- // Read
2712
- get: (key) => getState().textures.get(key),
2260
+ get(input) {
2261
+ if (typeof input === "string") return getOne(input);
2262
+ if (Array.isArray(input)) return input.map(getOne);
2263
+ const out = {};
2264
+ for (const name in input) out[name] = getOne(input[name]);
2265
+ return out;
2266
+ },
2713
2267
  has: (key) => getState().textures.has(key),
2714
- // Write
2715
- add,
2716
- addMultiple,
2717
- // Remove (cache only)
2718
- remove,
2719
- removeMultiple,
2720
- // Dispose (GPU + cache)
2721
- dispose,
2722
- disposeMultiple,
2723
- disposeAll
2268
+ add(keyOrRecord, texture) {
2269
+ setState((state) => {
2270
+ const textures = new Map(state.textures);
2271
+ if (typeof keyOrRecord === "string") {
2272
+ textures.set(keyOrRecord, texture);
2273
+ } else {
2274
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2275
+ }
2276
+ return { textures };
2277
+ });
2278
+ },
2279
+ dispose(key, options) {
2280
+ const state = getState();
2281
+ const refs = state._textureRefs.get(key) ?? 0;
2282
+ if (refs > 0 && !options?.force) {
2283
+ console.warn(
2284
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2285
+ );
2286
+ return false;
2287
+ }
2288
+ state.textures.get(key)?.dispose();
2289
+ setState((s) => {
2290
+ const textures = new Map(s.textures);
2291
+ textures.delete(key);
2292
+ const nextRefs = new Map(s._textureRefs);
2293
+ nextRefs.delete(key);
2294
+ return { textures, _textureRefs: nextRefs };
2295
+ });
2296
+ return true;
2297
+ },
2298
+ disposeAll() {
2299
+ for (const texture of getState().textures.values()) texture.dispose();
2300
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2301
+ }
2724
2302
  };
2725
2303
  }, [store]);
2304
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2305
+ const selected = useThree(subscribe);
2306
+ return selector ? selected : registry;
2726
2307
  }
2727
2308
 
2728
- function useRenderTarget(width, height, options) {
2309
+ function useRenderTarget(widthOrOptions, heightOrOptions, options) {
2729
2310
  const isLegacy = useThree((s) => s.isLegacy);
2730
2311
  const size = useThree((s) => s.size);
2312
+ let width;
2313
+ let height;
2314
+ let opts;
2315
+ if (typeof widthOrOptions === "object") {
2316
+ opts = widthOrOptions;
2317
+ } else if (typeof widthOrOptions === "number") {
2318
+ width = widthOrOptions;
2319
+ if (typeof heightOrOptions === "object") {
2320
+ height = widthOrOptions;
2321
+ opts = heightOrOptions;
2322
+ } else if (typeof heightOrOptions === "number") {
2323
+ height = heightOrOptions;
2324
+ opts = options;
2325
+ } else {
2326
+ height = widthOrOptions;
2327
+ }
2328
+ }
2731
2329
  return useMemo(() => {
2732
2330
  const w = width ?? size.width;
2733
2331
  const h = height ?? size.height;
2734
- return new RenderTarget(w, h, options);
2735
- }, [width, height, size.width, size.height, options, isLegacy]);
2332
+ return new RenderTarget(w, h, opts);
2333
+ }, [width, height, size.width, size.height, opts, isLegacy]);
2736
2334
  }
2737
2335
 
2738
2336
  function useStore() {
@@ -2782,7 +2380,7 @@ function addTail(callback) {
2782
2380
  function invalidate(state, frames = 1, stackFrames = false) {
2783
2381
  getScheduler().invalidate(frames, stackFrames);
2784
2382
  }
2785
- function advance(timestamp, runGlobalEffects = true, state, frame) {
2383
+ function advance(timestamp) {
2786
2384
  getScheduler().step(timestamp);
2787
2385
  }
2788
2386
 
@@ -14236,6 +13834,7 @@ function swapInstances() {
14236
13834
  instance.object = instance.props.object ?? new target(...instance.props.args ?? []);
14237
13835
  instance.object.__r3f = instance;
14238
13836
  setFiberRef(fiber, instance.object);
13837
+ delete instance.appliedOnce;
14239
13838
  applyProps(instance.object, instance.props);
14240
13839
  if (instance.props.attach) {
14241
13840
  attach(parent, instance);
@@ -14309,8 +13908,22 @@ const reconciler = /* @__PURE__ */ createReconciler({
14309
13908
  const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
14310
13909
  if (isTailSibling) swapInstances();
14311
13910
  },
14312
- finalizeInitialChildren: () => false,
14313
- commitMount() {
13911
+ finalizeInitialChildren: (instance) => {
13912
+ for (const prop in instance.props) {
13913
+ if (isFromRef(instance.props[prop])) return true;
13914
+ }
13915
+ return false;
13916
+ },
13917
+ commitMount(instance) {
13918
+ const resolved = {};
13919
+ for (const prop in instance.props) {
13920
+ const value = instance.props[prop];
13921
+ if (isFromRef(value)) {
13922
+ const ref = value[FROM_REF];
13923
+ if (ref.current != null) resolved[prop] = ref.current;
13924
+ }
13925
+ }
13926
+ if (Object.keys(resolved).length) applyProps(instance.object, resolved);
14314
13927
  },
14315
13928
  getPublicInstance: (instance) => instance?.object,
14316
13929
  prepareForCommit: () => null,
@@ -14531,6 +14144,9 @@ function createRoot(canvas) {
14531
14144
  let resolve;
14532
14145
  pending = new Promise((_resolve) => resolve = _resolve);
14533
14146
  const {
14147
+ id: canvasId,
14148
+ primaryCanvas,
14149
+ scheduler: schedulerConfig,
14534
14150
  gl: glConfig,
14535
14151
  renderer: rendererConfig,
14536
14152
  size: propsSize,
@@ -14538,10 +14154,6 @@ function createRoot(canvas) {
14538
14154
  events,
14539
14155
  onCreated: onCreatedCallback,
14540
14156
  shadows = false,
14541
- linear = false,
14542
- flat = false,
14543
- textureColorSpace = SRGBColorSpace,
14544
- legacy = false,
14545
14157
  orthographic = false,
14546
14158
  frameloop = "always",
14547
14159
  dpr = [1, 2],
@@ -14553,11 +14165,14 @@ function createRoot(canvas) {
14553
14165
  onDropMissed,
14554
14166
  autoUpdateFrustum = true,
14555
14167
  occlusion = false,
14556
- _sizeProps
14168
+ _sizeProps,
14169
+ forceEven
14557
14170
  } = props;
14171
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || SRGBColorSpace;
14558
14172
  const state = store.getState();
14559
14173
  const defaultGPUProps = {
14560
- canvas
14174
+ canvas,
14175
+ antialias: true
14561
14176
  };
14562
14177
  if (glConfig && !R3F_BUILD_LEGACY) {
14563
14178
  throw new Error(
@@ -14568,15 +14183,52 @@ function createRoot(canvas) {
14568
14183
  throw new Error("Cannot use both gl and renderer props at the same time");
14569
14184
  }
14570
14185
  let renderer = state.internal.actualRenderer;
14571
- if (!state.internal.actualRenderer) {
14186
+ if (primaryCanvas && !state.internal.actualRenderer) {
14187
+ const primary = await waitForPrimary(primaryCanvas);
14188
+ renderer = primary.renderer;
14189
+ state.internal.actualRenderer = renderer;
14190
+ const canvasTarget = new CanvasTarget(canvas);
14191
+ primary.store.setState((prev) => ({
14192
+ internal: { ...prev.internal, isMultiCanvas: true }
14193
+ }));
14194
+ state.set((prev) => ({
14195
+ webGPUSupported: primary.store.getState().webGPUSupported,
14196
+ renderer,
14197
+ primaryStore: primary.store,
14198
+ internal: {
14199
+ ...prev.internal,
14200
+ canvasTarget,
14201
+ isMultiCanvas: true,
14202
+ isSecondary: true,
14203
+ targetId: primaryCanvas
14204
+ }
14205
+ }));
14206
+ } else if (!state.internal.actualRenderer) {
14572
14207
  renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
14573
14208
  if (!renderer.hasInitialized?.()) {
14209
+ const size2 = computeInitialSize(canvas, propsSize);
14210
+ if (size2.width > 0 && size2.height > 0) {
14211
+ const pixelRatio = calculateDpr(dpr);
14212
+ canvas.width = size2.width * pixelRatio;
14213
+ canvas.height = size2.height * pixelRatio;
14214
+ }
14574
14215
  await renderer.init();
14575
14216
  }
14576
14217
  const backend = renderer.backend;
14577
14218
  const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14578
14219
  state.internal.actualRenderer = renderer;
14579
- state.set({ webGPUSupported: isWebGPUBackend, renderer });
14220
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14221
+ if (canvasId && !state.internal.isSecondary) {
14222
+ const canvasTarget = new CanvasTarget(canvas);
14223
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14224
+ state.set((prev) => ({
14225
+ internal: {
14226
+ ...prev.internal,
14227
+ canvasTarget,
14228
+ unregisterPrimary
14229
+ }
14230
+ }));
14231
+ }
14580
14232
  }
14581
14233
  let raycaster = state.raycaster;
14582
14234
  if (!raycaster) state.set({ raycaster: raycaster = new Raycaster() });
@@ -14585,6 +14237,7 @@ function createRoot(canvas) {
14585
14237
  if (!is.equ(params, raycaster.params, shallowLoose)) {
14586
14238
  applyProps(raycaster, { params: { ...raycaster.params, ...params } });
14587
14239
  }
14240
+ let tempCamera = state.camera;
14588
14241
  if (!state.camera || state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose)) {
14589
14242
  lastCamera = cameraOptions;
14590
14243
  const isCamera = cameraOptions?.isCamera;
@@ -14604,6 +14257,7 @@ function createRoot(canvas) {
14604
14257
  if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0);
14605
14258
  }
14606
14259
  state.set({ camera });
14260
+ tempCamera = camera;
14607
14261
  raycaster.camera = camera;
14608
14262
  }
14609
14263
  if (!state.scene) {
@@ -14621,7 +14275,7 @@ function createRoot(canvas) {
14621
14275
  rootScene: scene,
14622
14276
  internal: { ...prev.internal, container: scene }
14623
14277
  }));
14624
- const camera = state.camera;
14278
+ const camera = tempCamera;
14625
14279
  if (camera && !camera.parent) scene.add(camera);
14626
14280
  }
14627
14281
  if (events && !state.events.handlers) {
@@ -14638,6 +14292,9 @@ function createRoot(canvas) {
14638
14292
  if (_sizeProps !== void 0) {
14639
14293
  state.set({ _sizeProps });
14640
14294
  }
14295
+ if (forceEven !== void 0 && state.internal.forceEven !== forceEven) {
14296
+ state.set((prev) => ({ internal: { ...prev.internal, forceEven } }));
14297
+ }
14641
14298
  const size = computeInitialSize(canvas, propsSize);
14642
14299
  if (!state._sizeImperative && !is.equ(size, state.size, shallowLoose)) {
14643
14300
  const wasImperative = state._sizeImperative;
@@ -14664,10 +14321,10 @@ function createRoot(canvas) {
14664
14321
  lastConfiguredProps.performance = performance;
14665
14322
  }
14666
14323
  if (!state.xr) {
14667
- const handleXRFrame = (timestamp, frame) => {
14324
+ const handleXRFrame = (timestamp, _frame) => {
14668
14325
  const state2 = store.getState();
14669
14326
  if (state2.frameloop === "never") return;
14670
- advance(timestamp, true);
14327
+ advance(timestamp);
14671
14328
  };
14672
14329
  const actualRenderer = state.internal.actualRenderer;
14673
14330
  const handleSessionChange = () => {
@@ -14679,16 +14336,16 @@ function createRoot(canvas) {
14679
14336
  };
14680
14337
  const xr = {
14681
14338
  connect() {
14682
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14683
- const actualRenderer2 = renderer2 || gl;
14684
- actualRenderer2.xr.addEventListener("sessionstart", handleSessionChange);
14685
- actualRenderer2.xr.addEventListener("sessionend", handleSessionChange);
14339
+ const { gl, renderer: renderer2 } = store.getState();
14340
+ const xrManager = (renderer2 || gl).xr;
14341
+ xrManager.addEventListener("sessionstart", handleSessionChange);
14342
+ xrManager.addEventListener("sessionend", handleSessionChange);
14686
14343
  },
14687
14344
  disconnect() {
14688
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14689
- const actualRenderer2 = renderer2 || gl;
14690
- actualRenderer2.xr.removeEventListener("sessionstart", handleSessionChange);
14691
- actualRenderer2.xr.removeEventListener("sessionend", handleSessionChange);
14345
+ const { gl, renderer: renderer2 } = store.getState();
14346
+ const xrManager = (renderer2 || gl).xr;
14347
+ xrManager.removeEventListener("sessionstart", handleSessionChange);
14348
+ xrManager.removeEventListener("sessionend", handleSessionChange);
14692
14349
  }
14693
14350
  };
14694
14351
  if (typeof renderer.xr?.addEventListener === "function") xr.connect();
@@ -14700,15 +14357,22 @@ function createRoot(canvas) {
14700
14357
  const oldType = renderer.shadowMap.type;
14701
14358
  renderer.shadowMap.enabled = !!shadows;
14702
14359
  if (is.boo(shadows)) {
14703
- renderer.shadowMap.type = PCFSoftShadowMap;
14360
+ renderer.shadowMap.type = PCFShadowMap;
14704
14361
  } else if (is.str(shadows)) {
14362
+ if (shadows === "soft") {
14363
+ notifyDepreciated({
14364
+ heading: 'shadows="soft" is deprecated',
14365
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14366
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14367
+ });
14368
+ }
14705
14369
  const types = {
14706
14370
  basic: BasicShadowMap,
14707
14371
  percentage: PCFShadowMap,
14708
- soft: PCFSoftShadowMap,
14372
+ soft: PCFShadowMap,
14709
14373
  variance: VSMShadowMap
14710
14374
  };
14711
- renderer.shadowMap.type = types[shadows] ?? PCFSoftShadowMap;
14375
+ renderer.shadowMap.type = types[shadows] ?? PCFShadowMap;
14712
14376
  } else if (is.obj(shadows)) {
14713
14377
  Object.assign(renderer.shadowMap, shadows);
14714
14378
  }
@@ -14716,27 +14380,69 @@ function createRoot(canvas) {
14716
14380
  renderer.shadowMap.needsUpdate = true;
14717
14381
  }
14718
14382
  }
14383
+ if (!configured) {
14384
+ renderer.outputColorSpace = SRGBColorSpace;
14385
+ renderer.toneMapping = ACESFilmicToneMapping;
14386
+ }
14719
14387
  if (textureColorSpace !== lastConfiguredProps.textureColorSpace) {
14720
14388
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
14721
14389
  lastConfiguredProps.textureColorSpace = textureColorSpace;
14722
14390
  }
14391
+ const r3fProps = ["textureColorSpace"];
14392
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14393
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
14723
14394
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
14724
- applyProps(renderer, glConfig);
14395
+ const glProps = {};
14396
+ for (const key in glConfig) {
14397
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14398
+ }
14399
+ applyProps(renderer, glProps);
14725
14400
  }
14726
14401
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
14727
14402
  const currentRenderer = state.renderer;
14728
14403
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
14729
- applyProps(currentRenderer, rendererConfig);
14404
+ const rendererProps = {};
14405
+ for (const key in rendererConfig) {
14406
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14407
+ }
14408
+ applyProps(currentRenderer, rendererProps);
14730
14409
  }
14731
14410
  }
14732
14411
  const scheduler = getScheduler();
14733
14412
  const rootId = state.internal.rootId;
14734
14413
  if (!rootId) {
14735
- const newRootId = scheduler.generateRootId();
14414
+ const newRootId = canvasId || scheduler.generateRootId();
14736
14415
  const unregisterRoot = scheduler.registerRoot(newRootId, {
14737
14416
  getState: () => store.getState(),
14738
14417
  onError: (err) => store.getState().setError(err)
14739
14418
  });
14419
+ const unregisterCanvasTarget = scheduler.register(
14420
+ () => {
14421
+ const state2 = store.getState();
14422
+ if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14423
+ const renderer2 = state2.internal.actualRenderer;
14424
+ renderer2.setCanvasTarget(state2.internal.canvasTarget);
14425
+ }
14426
+ },
14427
+ {
14428
+ id: `${newRootId}_canvasTarget`,
14429
+ rootId: newRootId,
14430
+ phase: "start",
14431
+ system: true
14432
+ }
14433
+ );
14434
+ const unregisterEventsFlush = scheduler.register(
14435
+ () => {
14436
+ const state2 = store.getState();
14437
+ state2.events.flush?.();
14438
+ },
14439
+ {
14440
+ id: `${newRootId}_events`,
14441
+ rootId: newRootId,
14442
+ phase: "input",
14443
+ system: true
14444
+ }
14445
+ );
14740
14446
  const unregisterFrustum = scheduler.register(
14741
14447
  () => {
14742
14448
  const state2 = store.getState();
@@ -14747,7 +14453,7 @@ function createRoot(canvas) {
14747
14453
  {
14748
14454
  id: `${newRootId}_frustum`,
14749
14455
  rootId: newRootId,
14750
- phase: "preRender",
14456
+ before: "render",
14751
14457
  system: true
14752
14458
  }
14753
14459
  );
@@ -14759,7 +14465,7 @@ function createRoot(canvas) {
14759
14465
  {
14760
14466
  id: `${newRootId}_visibility`,
14761
14467
  rootId: newRootId,
14762
- phase: "preRender",
14468
+ before: "render",
14763
14469
  system: true,
14764
14470
  after: `${newRootId}_frustum`
14765
14471
  }
@@ -14771,18 +14477,22 @@ function createRoot(canvas) {
14771
14477
  const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14772
14478
  if (userHandlesRender || state2.internal.priority) return;
14773
14479
  try {
14774
- if (state2.postProcessing?.render) state2.postProcessing.render();
14480
+ if (state2.renderPipeline?.render) state2.renderPipeline.render();
14775
14481
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
14776
14482
  } catch (error) {
14777
14483
  state2.setError(error instanceof Error ? error : new Error(String(error)));
14778
14484
  }
14779
14485
  },
14780
14486
  {
14781
- id: `${newRootId}_render`,
14487
+ // Use canvas ID directly as job ID if available, otherwise use generated rootId
14488
+ id: canvasId || `${newRootId}_render`,
14782
14489
  rootId: newRootId,
14783
14490
  phase: "render",
14784
- system: true
14491
+ system: true,
14785
14492
  // Internal flag: this is a system job, not user-controlled
14493
+ // Apply scheduler config for render ordering and rate limiting
14494
+ ...schedulerConfig?.after && { after: schedulerConfig.after },
14495
+ ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14786
14496
  }
14787
14497
  );
14788
14498
  state.set((state2) => ({
@@ -14791,6 +14501,8 @@ function createRoot(canvas) {
14791
14501
  rootId: newRootId,
14792
14502
  unregisterRoot: () => {
14793
14503
  unregisterRoot();
14504
+ unregisterCanvasTarget();
14505
+ unregisterEventsFlush();
14794
14506
  unregisterFrustum();
14795
14507
  unregisterVisibility();
14796
14508
  unregisterRender();
@@ -14849,15 +14561,24 @@ function unmountComponentAtNode(canvas, callback) {
14849
14561
  const renderer = state.internal.actualRenderer;
14850
14562
  const unregisterRoot = state.internal.unregisterRoot;
14851
14563
  if (unregisterRoot) unregisterRoot();
14564
+ const unregisterPrimary = state.internal.unregisterPrimary;
14565
+ if (unregisterPrimary) unregisterPrimary();
14566
+ const canvasTarget = state.internal.canvasTarget;
14567
+ if (canvasTarget?.dispose) canvasTarget.dispose();
14852
14568
  state.events.disconnect?.();
14853
14569
  cleanupHelperGroup(root.store);
14854
- renderer?.renderLists?.dispose?.();
14855
- renderer?.forceContextLoss?.();
14856
- if (renderer?.xr) state.xr.disconnect();
14570
+ if (state.isLegacy && renderer) {
14571
+ ;
14572
+ renderer.renderLists?.dispose?.();
14573
+ renderer.forceContextLoss?.();
14574
+ }
14575
+ if (!state.internal.isSecondary) {
14576
+ if (renderer?.xr) state.xr.disconnect();
14577
+ }
14857
14578
  dispose(state.scene);
14858
14579
  _roots.delete(canvas);
14859
14580
  if (callback) callback(canvas);
14860
- } catch (e) {
14581
+ } catch {
14861
14582
  }
14862
14583
  }, 500);
14863
14584
  }
@@ -14865,36 +14586,34 @@ function unmountComponentAtNode(canvas, callback) {
14865
14586
  }
14866
14587
  }
14867
14588
  function createPortal(children, container, state) {
14868
- return /* @__PURE__ */ jsx(PortalWrapper, { children, container, state });
14589
+ return /* @__PURE__ */ jsx(Portal, { children, container, state });
14869
14590
  }
14870
- function PortalWrapper({ children, container, state }) {
14591
+ function Portal({ children, container, state }) {
14871
14592
  const isRef = useCallback((obj) => obj && "current" in obj, []);
14872
- const [resolvedContainer, setResolvedContainer] = useState(() => {
14593
+ const [resolvedContainer, _setResolvedContainer] = useState(() => {
14873
14594
  if (isRef(container)) return container.current ?? null;
14874
14595
  return container;
14875
14596
  });
14597
+ const setResolvedContainer = useCallback(
14598
+ (newContainer) => {
14599
+ if (!newContainer || newContainer === resolvedContainer) return;
14600
+ _setResolvedContainer(isRef(newContainer) ? newContainer.current : newContainer);
14601
+ },
14602
+ [resolvedContainer, _setResolvedContainer, isRef]
14603
+ );
14876
14604
  useMemo(() => {
14877
- if (isRef(container)) {
14878
- const current = container.current;
14879
- if (!current) {
14880
- queueMicrotask(() => {
14881
- const updated = container.current;
14882
- if (updated && updated !== resolvedContainer) {
14883
- setResolvedContainer(updated);
14884
- }
14885
- });
14886
- } else if (current !== resolvedContainer) {
14887
- setResolvedContainer(current);
14888
- }
14889
- } else if (container !== resolvedContainer) {
14890
- setResolvedContainer(container);
14605
+ if (isRef(container) && !container.current) {
14606
+ return queueMicrotask(() => {
14607
+ setResolvedContainer(container.current);
14608
+ });
14891
14609
  }
14892
- }, [container, resolvedContainer, isRef]);
14610
+ setResolvedContainer(container);
14611
+ }, [container, isRef, setResolvedContainer]);
14893
14612
  if (!resolvedContainer) return /* @__PURE__ */ jsx(Fragment, {});
14894
14613
  const portalKey = resolvedContainer.uuid ?? `portal-${resolvedContainer.id ?? "unknown"}`;
14895
- return /* @__PURE__ */ jsx(Portal, { children, container: resolvedContainer, state }, portalKey);
14614
+ return /* @__PURE__ */ jsx(PortalInner, { children, container: resolvedContainer, state }, portalKey);
14896
14615
  }
14897
- function Portal({ state = {}, children, container }) {
14616
+ function PortalInner({ state = {}, children, container }) {
14898
14617
  const { events, size, injectScene = true, ...rest } = state;
14899
14618
  const previousRoot = useStore();
14900
14619
  const [raycaster] = useState(() => new Raycaster());
@@ -14915,11 +14634,12 @@ function Portal({ state = {}, children, container }) {
14915
14634
  };
14916
14635
  }, [portalScene, container, injectScene]);
14917
14636
  const inject = useMutableCallback((rootState, injectState) => {
14637
+ const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
14918
14638
  let viewport = void 0;
14919
- if (injectState.camera && size) {
14639
+ if (injectState.camera && (size || injectState.size)) {
14920
14640
  const camera = injectState.camera;
14921
- viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), size);
14922
- if (camera !== rootState.camera) updateCamera(camera, size);
14641
+ viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
14642
+ if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
14923
14643
  }
14924
14644
  return {
14925
14645
  // The intersect consists of the previous root state
@@ -14936,7 +14656,7 @@ function Portal({ state = {}, children, container }) {
14936
14656
  previousRoot,
14937
14657
  // Events, size and viewport can be overridden by the inject layer
14938
14658
  events: { ...rootState.events, ...injectState.events, ...events },
14939
- size: { ...rootState.size, ...size },
14659
+ size: resolvedSize,
14940
14660
  viewport: { ...rootState.viewport, ...viewport },
14941
14661
  // Layers are allowed to override events
14942
14662
  setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
@@ -14948,9 +14668,13 @@ function Portal({ state = {}, children, container }) {
14948
14668
  const store = createWithEqualityFn((set, get) => ({ ...rest, set, get }));
14949
14669
  const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
14950
14670
  onMutate(previousRoot.getState());
14951
- previousRoot.subscribe(onMutate);
14952
14671
  return store;
14953
14672
  }, [previousRoot, container]);
14673
+ useIsomorphicLayoutEffect(() => {
14674
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14675
+ const unsubscribe = previousRoot.subscribe(onMutate);
14676
+ return unsubscribe;
14677
+ }, [previousRoot, usePortalStore]);
14954
14678
  return (
14955
14679
  // @ts-ignore, reconciler types are not maintained
14956
14680
  /* @__PURE__ */ jsx(Fragment, { children: reconciler.createPortal(
@@ -14964,21 +14688,59 @@ function flushSync(fn) {
14964
14688
  return reconciler.flushSyncFromReconciler(fn);
14965
14689
  }
14966
14690
 
14691
+ function parseBackground(background) {
14692
+ if (!background) return null;
14693
+ if (typeof background === "object" && !background.isColor) {
14694
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14695
+ return {
14696
+ ...rest,
14697
+ preset,
14698
+ files: envMap || files,
14699
+ backgroundFiles: backgroundMap,
14700
+ background: true
14701
+ };
14702
+ }
14703
+ if (typeof background === "number") {
14704
+ return { color: background, background: true };
14705
+ }
14706
+ if (typeof background === "string") {
14707
+ if (background in presetsObj) {
14708
+ return { preset: background, background: true };
14709
+ }
14710
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14711
+ return { files: background, background: true };
14712
+ }
14713
+ return { color: background, background: true };
14714
+ }
14715
+ if (background.isColor) {
14716
+ return { color: background, background: true };
14717
+ }
14718
+ return null;
14719
+ }
14720
+
14721
+ function clearHmrCaches(store) {
14722
+ store.setState((state) => ({
14723
+ nodes: {},
14724
+ uniforms: {},
14725
+ buffers: {},
14726
+ gpuStorage: {},
14727
+ _hmrVersion: state._hmrVersion + 1
14728
+ }));
14729
+ }
14730
+
14967
14731
  function CanvasImpl({
14968
14732
  ref,
14969
14733
  children,
14970
14734
  fallback,
14971
14735
  resize,
14972
14736
  style,
14737
+ id,
14973
14738
  gl,
14974
- renderer,
14739
+ renderer: rendererProp,
14975
14740
  events = createPointerEvents,
14976
14741
  eventSource,
14977
14742
  eventPrefix,
14978
14743
  shadows,
14979
- linear,
14980
- flat,
14981
- legacy,
14982
14744
  orthographic,
14983
14745
  frameloop,
14984
14746
  dpr,
@@ -14986,6 +14748,8 @@ function CanvasImpl({
14986
14748
  raycaster,
14987
14749
  camera,
14988
14750
  scene,
14751
+ autoUpdateFrustum,
14752
+ occlusion,
14989
14753
  onPointerMissed,
14990
14754
  onDragOverMissed,
14991
14755
  onDropMissed,
@@ -14993,10 +14757,25 @@ function CanvasImpl({
14993
14757
  hmr,
14994
14758
  width,
14995
14759
  height,
14760
+ background,
14761
+ forceEven,
14996
14762
  ...props
14997
14763
  }) {
14764
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14765
+ let primaryCanvas;
14766
+ let scheduler;
14767
+ let renderer;
14768
+ if (isRendererConfig) {
14769
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14770
+ primaryCanvas = pc;
14771
+ scheduler = sc;
14772
+ renderer = Object.keys(rest).length > 0 ? rest : rendererProp;
14773
+ } else {
14774
+ renderer = rendererProp;
14775
+ }
14998
14776
  React.useMemo(() => extend(THREE), []);
14999
14777
  const Bridge = useBridge();
14778
+ const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
15000
14779
  const hasInitialSizeRef = React.useRef(false);
15001
14780
  const measureConfig = React.useMemo(() => {
15002
14781
  if (!hasInitialSizeRef.current) {
@@ -15013,15 +14792,20 @@ function CanvasImpl({
15013
14792
  };
15014
14793
  }, [resize, hasInitialSizeRef.current]);
15015
14794
  const [containerRef, containerRect] = useMeasure(measureConfig);
15016
- const effectiveSize = React.useMemo(
15017
- () => ({
15018
- width: width ?? containerRect.width,
15019
- height: height ?? containerRect.height,
14795
+ const effectiveSize = React.useMemo(() => {
14796
+ let w = width ?? containerRect.width;
14797
+ let h = height ?? containerRect.height;
14798
+ if (forceEven) {
14799
+ w = Math.ceil(w / 2) * 2;
14800
+ h = Math.ceil(h / 2) * 2;
14801
+ }
14802
+ return {
14803
+ width: w,
14804
+ height: h,
15020
14805
  top: containerRect.top,
15021
14806
  left: containerRect.left
15022
- }),
15023
- [width, height, containerRect]
15024
- );
14807
+ };
14808
+ }, [width, height, containerRect, forceEven]);
15025
14809
  if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
15026
14810
  hasInitialSizeRef.current = true;
15027
14811
  }
@@ -15061,23 +14845,26 @@ function CanvasImpl({
15061
14845
  async function run() {
15062
14846
  if (!effectActiveRef.current || !root.current) return;
15063
14847
  await root.current.configure({
14848
+ id,
14849
+ primaryCanvas,
14850
+ scheduler,
15064
14851
  gl,
15065
14852
  renderer,
15066
14853
  scene,
15067
14854
  events,
15068
14855
  shadows,
15069
- linear,
15070
- flat,
15071
- legacy,
15072
14856
  orthographic,
15073
14857
  frameloop,
15074
14858
  dpr,
15075
14859
  performance,
15076
14860
  raycaster,
15077
14861
  camera,
14862
+ autoUpdateFrustum,
14863
+ occlusion,
15078
14864
  size: effectiveSize,
15079
14865
  // Store size props for reset functionality
15080
14866
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
14867
+ forceEven,
15081
14868
  // Pass mutable reference to onPointerMissed so it's free to update
15082
14869
  onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
15083
14870
  onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
@@ -15101,7 +14888,10 @@ function CanvasImpl({
15101
14888
  });
15102
14889
  if (!effectActiveRef.current || !root.current) return;
15103
14890
  root.current.render(
15104
- /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsx(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: children ?? null }) }) })
14891
+ /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxs(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: [
14892
+ backgroundProps && /* @__PURE__ */ jsx(Environment, { ...backgroundProps }),
14893
+ children ?? null
14894
+ ] }) }) })
15105
14895
  );
15106
14896
  }
15107
14897
  run();
@@ -15128,20 +14918,15 @@ function CanvasImpl({
15128
14918
  const canvas = canvasRef.current;
15129
14919
  if (!canvas) return;
15130
14920
  const handleHMR = () => {
15131
- const rootEntry = _roots.get(canvas);
15132
- if (rootEntry?.store) {
15133
- rootEntry.store.setState((state) => ({
15134
- nodes: {},
15135
- uniforms: {},
15136
- _hmrVersion: state._hmrVersion + 1
15137
- }));
15138
- }
14921
+ queueMicrotask(() => {
14922
+ const rootEntry = _roots.get(canvas);
14923
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
14924
+ });
15139
14925
  };
15140
14926
  if (typeof import.meta !== "undefined" && import.meta.hot) {
15141
14927
  const hot = import.meta.hot;
15142
14928
  hot.on("vite:afterUpdate", handleHMR);
15143
- return () => hot.dispose?.(() => {
15144
- });
14929
+ return () => hot.off?.("vite:afterUpdate", handleHMR);
15145
14930
  }
15146
14931
  if (typeof module !== "undefined" && module.hot) {
15147
14932
  const hot = module.hot;
@@ -15164,7 +14949,16 @@ function CanvasImpl({
15164
14949
  ...style
15165
14950
  },
15166
14951
  ...props,
15167
- children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "r3f-canvas", style: { display: "block" }, children: fallback }) })
14952
+ children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
14953
+ "canvas",
14954
+ {
14955
+ ref: canvasRef,
14956
+ id,
14957
+ className: "r3f-canvas",
14958
+ style: { display: "block", width: "100%", height: "100%" },
14959
+ children: fallback
14960
+ }
14961
+ ) })
15168
14962
  }
15169
14963
  );
15170
14964
  }
@@ -15237,6 +15031,48 @@ let ScopedStore = _ScopedStore;
15237
15031
  function createScopedStore(data) {
15238
15032
  return new ScopedStore(data);
15239
15033
  }
15034
+ function createLazyCreatorState(state) {
15035
+ let _uniforms = null;
15036
+ let _nodes = null;
15037
+ let _buffers = null;
15038
+ let _gpuStorage = null;
15039
+ return Object.create(state, {
15040
+ uniforms: {
15041
+ get() {
15042
+ return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
15043
+ }
15044
+ },
15045
+ nodes: {
15046
+ get() {
15047
+ return _nodes ?? (_nodes = createScopedStore(state.nodes));
15048
+ }
15049
+ },
15050
+ buffers: {
15051
+ get() {
15052
+ return _buffers ?? (_buffers = createScopedStore(state.buffers));
15053
+ }
15054
+ },
15055
+ gpuStorage: {
15056
+ get() {
15057
+ return _gpuStorage ?? (_gpuStorage = createScopedStore(state.gpuStorage));
15058
+ }
15059
+ }
15060
+ });
15061
+ }
15062
+
15063
+ function resolvePrimary(local) {
15064
+ return local.getState().primaryStore ?? local;
15065
+ }
15066
+ function usePrimaryStore() {
15067
+ const local = useStore();
15068
+ const primary = local.getState().primaryStore;
15069
+ return useMemo(() => primary ?? local, [primary, local]);
15070
+ }
15071
+ function usePrimaryThree(selector = (state) => state, equalityFn) {
15072
+ const local = useStore();
15073
+ const primary = resolvePrimary(local);
15074
+ return primary(selector, equalityFn);
15075
+ }
15240
15076
 
15241
15077
  function addTexture(set, key, value) {
15242
15078
  set((state) => {
@@ -15277,6 +15113,27 @@ function createTextureOperations(set) {
15277
15113
  removeMultiple: (keys) => removeTextures(set, keys)
15278
15114
  };
15279
15115
  }
15116
+ function extractTSLValue(value) {
15117
+ if (value === null || value === void 0) return value;
15118
+ if (typeof value !== "object") return value;
15119
+ const node = value;
15120
+ if (!node.isNode) return value;
15121
+ if (node.isConstNode) {
15122
+ return node.value;
15123
+ }
15124
+ if ("value" in node) {
15125
+ let extractedValue = node.value;
15126
+ if (typeof node.traverse === "function") {
15127
+ node.traverse((n) => {
15128
+ if (n.isConstNode) {
15129
+ extractedValue = n.value;
15130
+ }
15131
+ });
15132
+ }
15133
+ return extractedValue;
15134
+ }
15135
+ return value;
15136
+ }
15280
15137
  function vectorize(inObject) {
15281
15138
  if (inObject === null || inObject === void 0) return inObject;
15282
15139
  if (typeof inObject === "string") {
@@ -15289,9 +15146,16 @@ function vectorize(inObject) {
15289
15146
  }
15290
15147
  if (typeof inObject !== "object") return inObject;
15291
15148
  const obj = inObject;
15149
+ if (obj.isNode) {
15150
+ return extractTSLValue(inObject);
15151
+ }
15292
15152
  if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
15293
15153
  if (obj.isMatrix3 || obj.isMatrix4) return inObject;
15294
15154
  if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
15155
+ if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
15156
+ const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
15157
+ return new Color(obj.r * scale, obj.g * scale, obj.b * scale);
15158
+ }
15295
15159
  if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
15296
15160
  if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
15297
15161
  return new Vector4(obj.x, obj.y, obj.z, obj.w);
@@ -15315,7 +15179,7 @@ function useCompareMemoize(value, deep) {
15315
15179
 
15316
15180
  const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15317
15181
  function useUniforms(creatorOrScope, scope) {
15318
- const store = useStore();
15182
+ const store = usePrimaryStore();
15319
15183
  const removeUniforms2 = useCallback(
15320
15184
  (names, targetScope) => {
15321
15185
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15354,17 +15218,14 @@ function useUniforms(creatorOrScope, scope) {
15354
15218
  const rebuildUniforms = useCallback(
15355
15219
  (targetScope) => {
15356
15220
  store.setState((state) => {
15357
- let newUniforms = state.uniforms;
15221
+ let newUniforms = {};
15358
15222
  if (targetScope && targetScope !== "root") {
15359
15223
  const { [targetScope]: _, ...rest } = state.uniforms;
15360
15224
  newUniforms = rest;
15361
15225
  } else if (targetScope === "root") {
15362
- newUniforms = {};
15363
15226
  for (const [key, value] of Object.entries(state.uniforms)) {
15364
15227
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15365
15228
  }
15366
- } else {
15367
- newUniforms = {};
15368
15229
  }
15369
15230
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15370
15231
  });
@@ -15372,20 +15233,26 @@ function useUniforms(creatorOrScope, scope) {
15372
15233
  [store]
15373
15234
  );
15374
15235
  const inputForMemoization = useMemo(() => {
15236
+ let raw = creatorOrScope;
15375
15237
  if (is.fun(creatorOrScope)) {
15376
- const state = store.getState();
15377
- const wrappedState = {
15378
- ...state,
15379
- uniforms: createScopedStore(state.uniforms),
15380
- nodes: createScopedStore(state.nodes)
15381
- };
15382
- return creatorOrScope(wrappedState);
15238
+ const wrappedState = createLazyCreatorState(store.getState());
15239
+ raw = creatorOrScope(wrappedState);
15240
+ }
15241
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
15242
+ const normalized = {};
15243
+ for (const [key, value] of Object.entries(raw)) {
15244
+ normalized[key] = vectorize(value);
15245
+ }
15246
+ return normalized;
15383
15247
  }
15384
- return creatorOrScope;
15248
+ return raw;
15385
15249
  }, [creatorOrScope, store]);
15386
15250
  const memoizedInput = useCompareMemoize(inputForMemoization);
15387
15251
  const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
15388
- const storeUniforms = useThree((s) => s.uniforms);
15252
+ const storeUniforms = usePrimaryThree((s) => s.uniforms);
15253
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15254
+ const readerDep = isReader ? storeUniforms : null;
15255
+ const creatorDep = isReader ? null : hmrVersion;
15389
15256
  const uniforms = useMemo(() => {
15390
15257
  if (memoizedInput === void 0) {
15391
15258
  return storeUniforms;
@@ -15440,28 +15307,20 @@ function useUniforms(creatorOrScope, scope) {
15440
15307
  }
15441
15308
  }
15442
15309
  return result;
15443
- }, [
15444
- store,
15445
- memoizedInput,
15446
- scope,
15447
- // Only include storeUniforms in deps for reader modes to enable reactivity
15448
- isReader ? storeUniforms : null
15449
- ]);
15310
+ }, [store, memoizedInput, scope, readerDep, creatorDep]);
15450
15311
  return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
15451
15312
  }
15452
15313
  function rebuildAllUniforms(store, scope) {
15314
+ store = store.getState().primaryStore ?? store;
15453
15315
  store.setState((state) => {
15454
- let newUniforms = state.uniforms;
15316
+ let newUniforms = {};
15455
15317
  if (scope && scope !== "root") {
15456
15318
  const { [scope]: _, ...rest } = state.uniforms;
15457
15319
  newUniforms = rest;
15458
15320
  } else if (scope === "root") {
15459
- newUniforms = {};
15460
15321
  for (const [key, value] of Object.entries(state.uniforms)) {
15461
15322
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15462
15323
  }
15463
- } else {
15464
- newUniforms = {};
15465
15324
  }
15466
15325
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15467
15326
  });
@@ -15529,15 +15388,17 @@ function isSameThreeType(a, b) {
15529
15388
  }
15530
15389
 
15531
15390
  const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15391
+ const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
15532
15392
  function useUniform(name, value) {
15533
15393
  const store = useStore();
15394
+ const hmrVersion = useThree((s) => s._hmrVersion);
15534
15395
  return useMemo(() => {
15535
15396
  const state = store.getState();
15536
15397
  const set = store.setState;
15537
15398
  const existing = state.uniforms[name];
15538
15399
  if (existing && isUniformNode(existing)) {
15539
- if (value !== void 0) {
15540
- existing.value = value;
15400
+ if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
15401
+ existing.value = typeof value === "string" ? new Color(value) : value;
15541
15402
  }
15542
15403
  return existing;
15543
15404
  }
@@ -15546,7 +15407,24 @@ function useUniform(name, value) {
15546
15407
  `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
15547
15408
  );
15548
15409
  }
15549
- const node = uniform(value);
15410
+ if (isUniformNode(value)) {
15411
+ const node2 = value;
15412
+ if (typeof node2.setName === "function") {
15413
+ node2.setName(name);
15414
+ }
15415
+ set((s) => ({
15416
+ uniforms: { ...s.uniforms, [name]: node2 }
15417
+ }));
15418
+ return node2;
15419
+ }
15420
+ let node;
15421
+ if (isTSLNode$1(value)) {
15422
+ node = uniform(value);
15423
+ } else if (typeof value === "string") {
15424
+ node = uniform(new Color(value));
15425
+ } else {
15426
+ node = uniform(value);
15427
+ }
15550
15428
  if (typeof node.setName === "function") {
15551
15429
  node.setName(name);
15552
15430
  }
@@ -15557,12 +15435,12 @@ function useUniform(name, value) {
15557
15435
  }
15558
15436
  }));
15559
15437
  return node;
15560
- }, [store, name]);
15438
+ }, [store, name, hmrVersion]);
15561
15439
  }
15562
15440
 
15563
15441
  const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
15564
15442
  function useNodes(creatorOrScope, scope) {
15565
- const store = useStore();
15443
+ const store = usePrimaryStore();
15566
15444
  const removeNodes2 = useCallback(
15567
15445
  (names, targetScope) => {
15568
15446
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15619,8 +15497,11 @@ function useNodes(creatorOrScope, scope) {
15619
15497
  [store]
15620
15498
  );
15621
15499
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15622
- const storeNodes = useThree((s) => s.nodes);
15623
- const hmrVersion = useThree((s) => s._hmrVersion);
15500
+ const storeNodes = usePrimaryThree((s) => s.nodes);
15501
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15502
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15503
+ const readerDep = isReader ? storeNodes : null;
15504
+ const creatorDep = isReader ? null : hmrVersion;
15624
15505
  const nodes = useMemo(() => {
15625
15506
  if (creatorOrScope === void 0) {
15626
15507
  return storeNodes;
@@ -15633,11 +15514,7 @@ function useNodes(creatorOrScope, scope) {
15633
15514
  const state = store.getState();
15634
15515
  const set = store.setState;
15635
15516
  const creator = creatorOrScope;
15636
- const wrappedState = {
15637
- ...state,
15638
- uniforms: createScopedStore(state.uniforms),
15639
- nodes: createScopedStore(state.nodes)
15640
- };
15517
+ const wrappedState = createLazyCreatorState(state);
15641
15518
  const created = creator(wrappedState);
15642
15519
  const result = {};
15643
15520
  let hasNewNodes = false;
@@ -15647,7 +15524,7 @@ function useNodes(creatorOrScope, scope) {
15647
15524
  if (currentScope[name]) {
15648
15525
  result[name] = currentScope[name];
15649
15526
  } else {
15650
- if (typeof node.label === "function") node.setName(`${scope}.${name}`);
15527
+ node.setName?.(`${scope}.${name}`);
15651
15528
  result[name] = node;
15652
15529
  hasNewNodes = true;
15653
15530
  }
@@ -15667,7 +15544,7 @@ function useNodes(creatorOrScope, scope) {
15667
15544
  if (existing && isTSLNode(existing)) {
15668
15545
  result[name] = existing;
15669
15546
  } else {
15670
- if (typeof node.label === "function") node.setName(name);
15547
+ node.setName?.(name);
15671
15548
  result[name] = node;
15672
15549
  hasNewNodes = true;
15673
15550
  }
@@ -15676,18 +15553,11 @@ function useNodes(creatorOrScope, scope) {
15676
15553
  set((s) => ({ nodes: { ...s.nodes, ...result } }));
15677
15554
  }
15678
15555
  return result;
15679
- }, [
15680
- store,
15681
- typeof creatorOrScope === "string" ? creatorOrScope : scope,
15682
- // Only include storeNodes in deps for reader modes to enable reactivity
15683
- // Creator mode intentionally excludes it to avoid re-running creator on unrelated changes
15684
- isReader ? storeNodes : null,
15685
- // Include hmrVersion for creator modes to allow rebuildNodes() to bust the cache
15686
- isReader ? null : hmrVersion
15687
- ]);
15556
+ }, [store, scopeDep, readerDep, creatorDep]);
15688
15557
  return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
15689
15558
  }
15690
15559
  function rebuildAllNodes(store, scope) {
15560
+ store = store.getState().primaryStore ?? store;
15691
15561
  store.setState((state) => {
15692
15562
  let newNodes = state.nodes;
15693
15563
  if (scope && scope !== "root") {
@@ -15732,22 +15602,364 @@ function clearRootNodes(set) {
15732
15602
  });
15733
15603
  }
15734
15604
  function useLocalNodes(creator) {
15735
- const store = useStore();
15736
- const uniforms = useThree((s) => s.uniforms);
15737
- const nodes = useThree((s) => s.nodes);
15738
- const textures = useThree((s) => s.textures);
15605
+ const store = usePrimaryStore();
15606
+ const uniforms = usePrimaryThree((s) => s.uniforms);
15607
+ const nodes = usePrimaryThree((s) => s.nodes);
15608
+ const textures = usePrimaryThree((s) => s.textures);
15609
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15739
15610
  return useMemo(() => {
15740
- const state = store.getState();
15741
- const wrappedState = {
15742
- ...state,
15743
- uniforms: createScopedStore(state.uniforms),
15744
- nodes: createScopedStore(state.nodes)
15745
- };
15611
+ const wrappedState = createLazyCreatorState(store.getState());
15746
15612
  return creator(wrappedState);
15747
- }, [store, creator, uniforms, nodes, textures]);
15613
+ }, [store, creator, uniforms, nodes, textures, hmrVersion]);
15614
+ }
15615
+
15616
+ const isBufferLike = (value) => {
15617
+ if (value === null || typeof value !== "object") return false;
15618
+ if (ArrayBuffer.isView(value)) return true;
15619
+ if ("isBufferAttribute" in value) return true;
15620
+ if ("uuid" in value || "nodeType" in value) return true;
15621
+ return false;
15622
+ };
15623
+ const disposeBuffer = (buffer) => {
15624
+ if (buffer === null || typeof buffer !== "object") return;
15625
+ if ("dispose" in buffer && typeof buffer.dispose === "function") {
15626
+ buffer.dispose();
15627
+ }
15628
+ };
15629
+ function useBuffers(creatorOrScope, scope) {
15630
+ const store = usePrimaryStore();
15631
+ const removeBuffers = useCallback(
15632
+ (names, targetScope) => {
15633
+ const nameArray = Array.isArray(names) ? names : [names];
15634
+ store.setState((state) => {
15635
+ if (targetScope) {
15636
+ const currentScope = { ...state.buffers[targetScope] };
15637
+ for (const name of nameArray) delete currentScope[name];
15638
+ return { buffers: { ...state.buffers, [targetScope]: currentScope } };
15639
+ }
15640
+ const buffers2 = { ...state.buffers };
15641
+ for (const name of nameArray) if (isBufferLike(buffers2[name])) delete buffers2[name];
15642
+ return { buffers: buffers2 };
15643
+ });
15644
+ },
15645
+ [store]
15646
+ );
15647
+ const clearBuffers = useCallback(
15648
+ (targetScope) => {
15649
+ store.setState((state) => {
15650
+ if (targetScope && targetScope !== "root") {
15651
+ const { [targetScope]: _, ...rest } = state.buffers;
15652
+ return { buffers: rest };
15653
+ }
15654
+ if (targetScope === "root") {
15655
+ const buffers2 = {};
15656
+ for (const [key, value] of Object.entries(state.buffers)) {
15657
+ if (!isBufferLike(value)) buffers2[key] = value;
15658
+ }
15659
+ return { buffers: buffers2 };
15660
+ }
15661
+ return { buffers: {} };
15662
+ });
15663
+ },
15664
+ [store]
15665
+ );
15666
+ const rebuildBuffers = useCallback(
15667
+ (targetScope) => {
15668
+ store.setState((state) => {
15669
+ let newBuffers = state.buffers;
15670
+ if (targetScope && targetScope !== "root") {
15671
+ const { [targetScope]: _, ...rest } = state.buffers;
15672
+ newBuffers = rest;
15673
+ } else if (targetScope === "root") {
15674
+ newBuffers = {};
15675
+ for (const [key, value] of Object.entries(state.buffers)) {
15676
+ if (!isBufferLike(value)) newBuffers[key] = value;
15677
+ }
15678
+ } else {
15679
+ newBuffers = {};
15680
+ }
15681
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15682
+ });
15683
+ },
15684
+ [store]
15685
+ );
15686
+ const disposeBuffers = useCallback(
15687
+ (names, targetScope) => {
15688
+ const nameArray = Array.isArray(names) ? names : [names];
15689
+ const state = store.getState();
15690
+ for (const name of nameArray) {
15691
+ const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
15692
+ if (buffer && isBufferLike(buffer)) {
15693
+ disposeBuffer(buffer);
15694
+ }
15695
+ }
15696
+ removeBuffers(names, targetScope);
15697
+ },
15698
+ [store, removeBuffers]
15699
+ );
15700
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15701
+ const storeBuffers = usePrimaryThree((s) => s.buffers);
15702
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15703
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15704
+ const readerDep = isReader ? storeBuffers : null;
15705
+ const creatorDep = isReader ? null : hmrVersion;
15706
+ const buffers = useMemo(() => {
15707
+ if (creatorOrScope === void 0) {
15708
+ return storeBuffers;
15709
+ }
15710
+ if (typeof creatorOrScope === "string") {
15711
+ const scopeData = storeBuffers[creatorOrScope];
15712
+ if (scopeData && !isBufferLike(scopeData)) return scopeData;
15713
+ return {};
15714
+ }
15715
+ const state = store.getState();
15716
+ const set = store.setState;
15717
+ const creator = creatorOrScope;
15718
+ const wrappedState = createLazyCreatorState(state);
15719
+ const created = creator(wrappedState);
15720
+ const result = {};
15721
+ let hasNewBuffers = false;
15722
+ if (scope) {
15723
+ const currentScope = state.buffers[scope] ?? {};
15724
+ for (const [name, buffer] of Object.entries(created)) {
15725
+ if (currentScope[name]) {
15726
+ result[name] = currentScope[name];
15727
+ } else {
15728
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15729
+ buffer.setName(`${scope}.${name}`);
15730
+ }
15731
+ result[name] = buffer;
15732
+ hasNewBuffers = true;
15733
+ }
15734
+ }
15735
+ if (hasNewBuffers) {
15736
+ set((s) => ({
15737
+ buffers: {
15738
+ ...s.buffers,
15739
+ [scope]: { ...s.buffers[scope], ...result }
15740
+ }
15741
+ }));
15742
+ }
15743
+ return result;
15744
+ }
15745
+ for (const [name, buffer] of Object.entries(created)) {
15746
+ const existing = state.buffers[name];
15747
+ if (existing && isBufferLike(existing)) {
15748
+ result[name] = existing;
15749
+ } else {
15750
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15751
+ buffer.setName(name);
15752
+ }
15753
+ result[name] = buffer;
15754
+ hasNewBuffers = true;
15755
+ }
15756
+ }
15757
+ if (hasNewBuffers) {
15758
+ set((s) => ({ buffers: { ...s.buffers, ...result } }));
15759
+ }
15760
+ return result;
15761
+ }, [store, scopeDep, readerDep, creatorDep]);
15762
+ return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
15763
+ }
15764
+ function rebuildAllBuffers(store, scope) {
15765
+ store = store.getState().primaryStore ?? store;
15766
+ store.setState((state) => {
15767
+ let newBuffers = state.buffers;
15768
+ if (scope && scope !== "root") {
15769
+ const { [scope]: _, ...rest } = state.buffers;
15770
+ newBuffers = rest;
15771
+ } else if (scope === "root") {
15772
+ newBuffers = {};
15773
+ for (const [key, value] of Object.entries(state.buffers)) {
15774
+ if (!isBufferLike(value)) newBuffers[key] = value;
15775
+ }
15776
+ } else {
15777
+ newBuffers = {};
15778
+ }
15779
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15780
+ });
15781
+ }
15782
+
15783
+ const isStorageLike = (value) => {
15784
+ if (value === null || typeof value !== "object") return false;
15785
+ if ("isTexture" in value) return true;
15786
+ if ("isData3DTexture" in value) return true;
15787
+ if ("uuid" in value || "nodeType" in value) return true;
15788
+ return false;
15789
+ };
15790
+ const disposeStorage = (storage) => {
15791
+ if (storage === null || typeof storage !== "object") return;
15792
+ if ("dispose" in storage && typeof storage.dispose === "function") {
15793
+ storage.dispose();
15794
+ }
15795
+ };
15796
+ function useGPUStorage(creatorOrScope, scope) {
15797
+ const store = usePrimaryStore();
15798
+ const removeStorage = useCallback(
15799
+ (names, targetScope) => {
15800
+ const nameArray = Array.isArray(names) ? names : [names];
15801
+ store.setState((state) => {
15802
+ if (targetScope) {
15803
+ const currentScope = { ...state.gpuStorage[targetScope] };
15804
+ for (const name of nameArray) delete currentScope[name];
15805
+ return { gpuStorage: { ...state.gpuStorage, [targetScope]: currentScope } };
15806
+ }
15807
+ const gpuStorage2 = { ...state.gpuStorage };
15808
+ for (const name of nameArray) if (isStorageLike(gpuStorage2[name])) delete gpuStorage2[name];
15809
+ return { gpuStorage: gpuStorage2 };
15810
+ });
15811
+ },
15812
+ [store]
15813
+ );
15814
+ const clearStorage = useCallback(
15815
+ (targetScope) => {
15816
+ store.setState((state) => {
15817
+ if (targetScope && targetScope !== "root") {
15818
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15819
+ return { gpuStorage: rest };
15820
+ }
15821
+ if (targetScope === "root") {
15822
+ const gpuStorage2 = {};
15823
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15824
+ if (!isStorageLike(value)) gpuStorage2[key] = value;
15825
+ }
15826
+ return { gpuStorage: gpuStorage2 };
15827
+ }
15828
+ return { gpuStorage: {} };
15829
+ });
15830
+ },
15831
+ [store]
15832
+ );
15833
+ const rebuildStorage = useCallback(
15834
+ (targetScope) => {
15835
+ store.setState((state) => {
15836
+ let newStorage = state.gpuStorage;
15837
+ if (targetScope && targetScope !== "root") {
15838
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15839
+ newStorage = rest;
15840
+ } else if (targetScope === "root") {
15841
+ newStorage = {};
15842
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15843
+ if (!isStorageLike(value)) newStorage[key] = value;
15844
+ }
15845
+ } else {
15846
+ newStorage = {};
15847
+ }
15848
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
15849
+ });
15850
+ },
15851
+ [store]
15852
+ );
15853
+ const disposeStorageFn = useCallback(
15854
+ (names, targetScope) => {
15855
+ const nameArray = Array.isArray(names) ? names : [names];
15856
+ const state = store.getState();
15857
+ for (const name of nameArray) {
15858
+ const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
15859
+ if (storage && isStorageLike(storage)) {
15860
+ disposeStorage(storage);
15861
+ }
15862
+ }
15863
+ removeStorage(names, targetScope);
15864
+ },
15865
+ [store, removeStorage]
15866
+ );
15867
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15868
+ const storeStorage = usePrimaryThree((s) => s.gpuStorage);
15869
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15870
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15871
+ const readerDep = isReader ? storeStorage : null;
15872
+ const creatorDep = isReader ? null : hmrVersion;
15873
+ const gpuStorage = useMemo(() => {
15874
+ if (creatorOrScope === void 0) {
15875
+ return storeStorage;
15876
+ }
15877
+ if (typeof creatorOrScope === "string") {
15878
+ const scopeData = storeStorage[creatorOrScope];
15879
+ if (scopeData && !isStorageLike(scopeData)) return scopeData;
15880
+ return {};
15881
+ }
15882
+ const state = store.getState();
15883
+ const set = store.setState;
15884
+ const creator = creatorOrScope;
15885
+ const wrappedState = createLazyCreatorState(state);
15886
+ const created = creator(wrappedState);
15887
+ const result = {};
15888
+ let hasNewStorage = false;
15889
+ if (scope) {
15890
+ const currentScope = state.gpuStorage[scope] ?? {};
15891
+ for (const [name, storage] of Object.entries(created)) {
15892
+ if (currentScope[name]) {
15893
+ result[name] = currentScope[name];
15894
+ } else {
15895
+ if ("setName" in storage && typeof storage.setName === "function") {
15896
+ storage.setName(`${scope}.${name}`);
15897
+ }
15898
+ if ("name" in storage && typeof storage.name === "string") {
15899
+ storage.name = `${scope}.${name}`;
15900
+ }
15901
+ result[name] = storage;
15902
+ hasNewStorage = true;
15903
+ }
15904
+ }
15905
+ if (hasNewStorage) {
15906
+ set((s) => ({
15907
+ gpuStorage: {
15908
+ ...s.gpuStorage,
15909
+ [scope]: { ...s.gpuStorage[scope], ...result }
15910
+ }
15911
+ }));
15912
+ }
15913
+ return result;
15914
+ }
15915
+ for (const [name, storage] of Object.entries(created)) {
15916
+ const existing = state.gpuStorage[name];
15917
+ if (existing && isStorageLike(existing)) {
15918
+ result[name] = existing;
15919
+ } else {
15920
+ if ("setName" in storage && typeof storage.setName === "function") {
15921
+ storage.setName(name);
15922
+ }
15923
+ if ("name" in storage && typeof storage.name === "string") {
15924
+ storage.name = name;
15925
+ }
15926
+ result[name] = storage;
15927
+ hasNewStorage = true;
15928
+ }
15929
+ }
15930
+ if (hasNewStorage) {
15931
+ set((s) => ({ gpuStorage: { ...s.gpuStorage, ...result } }));
15932
+ }
15933
+ return result;
15934
+ }, [store, scopeDep, readerDep, creatorDep]);
15935
+ return {
15936
+ ...gpuStorage,
15937
+ removeStorage,
15938
+ clearStorage,
15939
+ rebuildStorage,
15940
+ disposeStorage: disposeStorageFn
15941
+ };
15942
+ }
15943
+ function rebuildAllStorage(store, scope) {
15944
+ store = store.getState().primaryStore ?? store;
15945
+ store.setState((state) => {
15946
+ let newStorage = state.gpuStorage;
15947
+ if (scope && scope !== "root") {
15948
+ const { [scope]: _, ...rest } = state.gpuStorage;
15949
+ newStorage = rest;
15950
+ } else if (scope === "root") {
15951
+ newStorage = {};
15952
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15953
+ if (!isStorageLike(value)) newStorage[key] = value;
15954
+ }
15955
+ } else {
15956
+ newStorage = {};
15957
+ }
15958
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
15959
+ });
15748
15960
  }
15749
15961
 
15750
- function usePostProcessing(mainCB, setupCB) {
15962
+ function useRenderPipeline(mainCB, setupCB) {
15751
15963
  const store = useStore();
15752
15964
  const { scene, camera, renderer, isLegacy } = useThree();
15753
15965
  const callbacksRanRef = useRef(false);
@@ -15766,7 +15978,7 @@ function usePostProcessing(mainCB, setupCB) {
15766
15978
  }, [store]);
15767
15979
  const reset = useCallback(() => {
15768
15980
  store.setState({
15769
- postProcessing: null,
15981
+ renderPipeline: null,
15770
15982
  passes: {}
15771
15983
  });
15772
15984
  callbacksRanRef.current = false;
@@ -15779,13 +15991,13 @@ function usePostProcessing(mainCB, setupCB) {
15779
15991
  }, []);
15780
15992
  useLayoutEffect(() => {
15781
15993
  if (isLegacy) {
15782
- throw new Error("usePostProcessing is only available with WebGPU renderer. Set renderer prop on Canvas.");
15994
+ throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
15783
15995
  }
15784
15996
  if (!renderer || !scene || !camera) return;
15785
15997
  const state = store.getState();
15786
15998
  const set = store.setState;
15787
15999
  try {
15788
- let pp = state.postProcessing;
16000
+ let pp = state.renderPipeline;
15789
16001
  let currentPasses = { ...state.passes };
15790
16002
  let justCreatedPP = false;
15791
16003
  if (!pp) {
@@ -15802,7 +16014,7 @@ function usePostProcessing(mainCB, setupCB) {
15802
16014
  }
15803
16015
  currentPasses.scenePass = scenePass;
15804
16016
  if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
15805
- set({ postProcessing: pp, passes: currentPasses });
16017
+ set({ renderPipeline: pp, passes: currentPasses });
15806
16018
  const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
15807
16019
  if (shouldRunCallbacks) {
15808
16020
  if (setupCBRef.current) {
@@ -15824,22 +16036,22 @@ function usePostProcessing(mainCB, setupCB) {
15824
16036
  callbacksRanRef.current = true;
15825
16037
  }
15826
16038
  } catch (error) {
15827
- console.error("[usePostProcessing] Setup error:", error);
16039
+ console.error("[useRenderPipeline] Setup error:", error);
15828
16040
  }
15829
16041
  }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
15830
16042
  const passes = useThree((s) => s.passes);
15831
- const postProcessing = useThree((s) => s.postProcessing);
16043
+ const renderPipeline = useThree((s) => s.renderPipeline);
15832
16044
  return {
15833
16045
  passes,
15834
- postProcessing,
16046
+ renderPipeline,
15835
16047
  clearPasses,
15836
16048
  reset,
15837
16049
  rebuild,
15838
- // isReady indicates if PostProcessing is configured and ready for rendering
15839
- isReady: postProcessing !== null
16050
+ // isReady indicates if RenderPipeline is configured and ready for rendering
16051
+ isReady: renderPipeline !== null
15840
16052
  };
15841
16053
  }
15842
16054
 
15843
16055
  extend(THREE);
15844
16056
 
15845
- export { Block, Canvas, ErrorBoundary, IsObject, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, clearNodeScope, clearRootNodes, clearRootUniforms, clearScope, context, createEvents, createPointerEvents, createPortal, createRoot, createScopedStore, createStore, createTextureOperations, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, getInstanceProps, getRootState, getScheduler, getUuidPrefix, hasConstructor, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isObject3D, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, prepare, rebuildAllNodes, rebuildAllUniforms, reconciler, removeInteractivity, removeNodes, removeUniforms, resolve, unmountComponentAtNode, updateCamera, updateFrustum, useBridge, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useLocalNodes, useMutableCallback, useNodes, usePostProcessing, useRenderTarget, useStore, useTexture, useTextures, useThree, useUniform, useUniforms };
16057
+ export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, clearNodeScope, clearRootNodes, clearRootUniforms, clearScope, context, createEvents, createPointerEvents, createPortal, createRoot, createScopedStore, createStore, createTextureOperations, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, rebuildAllBuffers, rebuildAllNodes, rebuildAllStorage, rebuildAllUniforms, reconciler, registerPrimary, removeInteractivity, removeNodes, removeUniforms, resolve, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useBuffers, useEnvironment, useFrame, useGPUStorage, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useLocalNodes, useMutableCallback, useNodes, useRenderPipeline, useRenderTarget, useStore, useTexture, useTextures, useThree, useUniform, useUniforms, waitForPrimary };