@react-three/fiber 10.0.0-alpha.2 → 10.0.0-canary.0706a92

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.
@@ -6,8 +6,15 @@ const jsxRuntime = require('react/jsx-runtime');
6
6
  const React = require('react');
7
7
  const useMeasure = require('react-use-measure');
8
8
  const itsFine = require('its-fine');
9
+ const fiber = require('@react-three/fiber');
10
+ const GroundedSkybox_js = require('three/examples/jsm/objects/GroundedSkybox.js');
11
+ const HDRLoader_js = require('three/examples/jsm/loaders/HDRLoader.js');
12
+ const EXRLoader_js = require('three/examples/jsm/loaders/EXRLoader.js');
13
+ const UltraHDRLoader_js = require('three/examples/jsm/loaders/UltraHDRLoader.js');
14
+ const gainmapJs = require('@monogrid/gainmap-js');
9
15
  const Tb = require('scheduler');
10
16
  const traditional = require('zustand/traditional');
17
+ const scheduler = require('@pmndrs/scheduler');
11
18
  const suspendReact = require('suspend-react');
12
19
  const lite = require('dequal/lite');
13
20
  require('zustand/shallow');
@@ -67,9 +74,392 @@ const THREE = /*#__PURE__*/_mergeNamespaces({
67
74
  WebGLRenderer: WebGLRenderer
68
75
  }, [webgpu__namespace]);
69
76
 
70
- var __defProp$3 = Object.defineProperty;
71
- var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
72
- var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
77
+ const primaryRegistry = /* @__PURE__ */ new Map();
78
+ const pendingSubscribers = /* @__PURE__ */ new Map();
79
+ function registerPrimary(id, renderer, store) {
80
+ if (primaryRegistry.has(id)) {
81
+ console.warn(`Canvas with id="${id}" already registered. Overwriting.`);
82
+ }
83
+ const entry = { renderer, store };
84
+ primaryRegistry.set(id, entry);
85
+ const subscribers = pendingSubscribers.get(id);
86
+ if (subscribers) {
87
+ subscribers.forEach((callback) => callback(entry));
88
+ pendingSubscribers.delete(id);
89
+ }
90
+ return () => {
91
+ const currentEntry = primaryRegistry.get(id);
92
+ if (currentEntry?.renderer === renderer) {
93
+ primaryRegistry.delete(id);
94
+ }
95
+ };
96
+ }
97
+ function getPrimary(id) {
98
+ return primaryRegistry.get(id);
99
+ }
100
+ function waitForPrimary(id, timeout = 5e3) {
101
+ const existing = primaryRegistry.get(id);
102
+ if (existing) {
103
+ return Promise.resolve(existing);
104
+ }
105
+ return new Promise((resolve, reject) => {
106
+ const timeoutId = setTimeout(() => {
107
+ const subscribers = pendingSubscribers.get(id);
108
+ if (subscribers) {
109
+ const index = subscribers.indexOf(callback);
110
+ if (index !== -1) subscribers.splice(index, 1);
111
+ if (subscribers.length === 0) pendingSubscribers.delete(id);
112
+ }
113
+ reject(new Error(`Timeout waiting for canvas with id="${id}". Make sure a <Canvas id="${id}"> is mounted.`));
114
+ }, timeout);
115
+ const callback = (entry) => {
116
+ clearTimeout(timeoutId);
117
+ resolve(entry);
118
+ };
119
+ if (!pendingSubscribers.has(id)) {
120
+ pendingSubscribers.set(id, []);
121
+ }
122
+ pendingSubscribers.get(id).push(callback);
123
+ });
124
+ }
125
+ function hasPrimary(id) {
126
+ return primaryRegistry.has(id);
127
+ }
128
+ function unregisterPrimary(id) {
129
+ primaryRegistry.delete(id);
130
+ }
131
+ function getPrimaryIds() {
132
+ return Array.from(primaryRegistry.keys());
133
+ }
134
+
135
+ const presetsObj = {
136
+ apartment: "lebombo_1k.hdr",
137
+ city: "potsdamer_platz_1k.hdr",
138
+ dawn: "kiara_1_dawn_1k.hdr",
139
+ forest: "forest_slope_1k.hdr",
140
+ lobby: "st_fagans_interior_1k.hdr",
141
+ night: "dikhololo_night_1k.hdr",
142
+ park: "rooitou_park_1k.hdr",
143
+ studio: "studio_small_03_1k.hdr",
144
+ sunset: "venice_sunset_1k.hdr",
145
+ warehouse: "empty_warehouse_01_1k.hdr"
146
+ };
147
+
148
+ const CUBEMAP_ROOT = "https://raw.githack.com/pmndrs/drei-assets/456060a26bbeb8fdf79326f224b6d99b8bcce736/hdri/";
149
+ const isArray = (arr) => Array.isArray(arr);
150
+ const defaultFiles = ["/px.png", "/nx.png", "/py.png", "/ny.png", "/pz.png", "/nz.png"];
151
+ function useEnvironment({
152
+ files = defaultFiles,
153
+ path = "",
154
+ preset = void 0,
155
+ colorSpace = void 0,
156
+ extensions
157
+ } = {}) {
158
+ if (preset) {
159
+ validatePreset(preset);
160
+ files = presetsObj[preset];
161
+ path = CUBEMAP_ROOT;
162
+ }
163
+ const multiFile = isArray(files);
164
+ const { extension, isCubemap } = getExtension(files);
165
+ const loader = getLoader$1(extension);
166
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
167
+ const renderer = fiber.useThree((state) => state.renderer);
168
+ React.useLayoutEffect(() => {
169
+ if (extension !== "webp" && extension !== "jpg" && extension !== "jpeg") return;
170
+ function clearGainmapTexture() {
171
+ fiber.useLoader.clear(loader, multiFile ? [files] : files);
172
+ }
173
+ renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
174
+ }, [extension, files, loader, multiFile, renderer.domElement]);
175
+ const loaderResult = fiber.useLoader(
176
+ loader,
177
+ multiFile ? [files] : files,
178
+ (loader2) => {
179
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
180
+ loader2.setRenderer?.(renderer);
181
+ }
182
+ loader2.setPath?.(path);
183
+ if (extensions) extensions(loader2);
184
+ }
185
+ );
186
+ let texture = multiFile ? (
187
+ // @ts-ignore
188
+ loaderResult[0]
189
+ ) : loaderResult;
190
+ if (extension === "jpg" || extension === "jpeg" || extension === "webp") {
191
+ texture = texture.renderTarget?.texture;
192
+ }
193
+ texture.mapping = isCubemap ? webgpu.CubeReflectionMapping : webgpu.EquirectangularReflectionMapping;
194
+ texture.colorSpace = colorSpace ?? (isCubemap ? "srgb" : "srgb-linear");
195
+ return texture;
196
+ }
197
+ const preloadDefaultOptions = {
198
+ files: defaultFiles,
199
+ path: "",
200
+ preset: void 0,
201
+ extensions: void 0
202
+ };
203
+ useEnvironment.preload = (preloadOptions) => {
204
+ const options = { ...preloadDefaultOptions, ...preloadOptions };
205
+ let { files, path = "" } = options;
206
+ const { preset, extensions } = options;
207
+ if (preset) {
208
+ validatePreset(preset);
209
+ files = presetsObj[preset];
210
+ path = CUBEMAP_ROOT;
211
+ }
212
+ const { extension } = getExtension(files);
213
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
214
+ throw new Error("useEnvironment: Preloading gainmaps is not supported");
215
+ }
216
+ const loader = getLoader$1(extension);
217
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
218
+ fiber.useLoader.preload(loader, isArray(files) ? [files] : files, (loader2) => {
219
+ loader2.setPath?.(path);
220
+ if (extensions) extensions(loader2);
221
+ });
222
+ };
223
+ const clearDefaultOptins = {
224
+ files: defaultFiles,
225
+ preset: void 0
226
+ };
227
+ useEnvironment.clear = (clearOptions) => {
228
+ const options = { ...clearDefaultOptins, ...clearOptions };
229
+ let { files } = options;
230
+ const { preset } = options;
231
+ if (preset) {
232
+ validatePreset(preset);
233
+ files = presetsObj[preset];
234
+ }
235
+ const { extension } = getExtension(files);
236
+ const loader = getLoader$1(extension);
237
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
238
+ fiber.useLoader.clear(loader, isArray(files) ? [files] : files);
239
+ };
240
+ function validatePreset(preset) {
241
+ if (!(preset in presetsObj)) throw new Error("Preset must be one of: " + Object.keys(presetsObj).join(", "));
242
+ }
243
+ function getExtension(files) {
244
+ const isCubemap = isArray(files) && files.length === 6;
245
+ const isGainmap = isArray(files) && files.length === 3 && files.some((file) => file.endsWith("json"));
246
+ const firstEntry = isArray(files) ? files[0] : files;
247
+ 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();
248
+ return { extension, isCubemap, isGainmap };
249
+ }
250
+ function getLoader$1(extension) {
251
+ const loader = extension === "cube" ? webgpu.CubeTextureLoader : extension === "hdr" ? HDRLoader_js.HDRLoader : extension === "exr" ? EXRLoader_js.EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader_js.UltraHDRLoader : extension === "webp" ? gainmapJs.GainMapLoader : null;
252
+ return loader;
253
+ }
254
+
255
+ const isRef$1 = (obj) => obj.current && obj.current.isScene;
256
+ const resolveScene = (scene) => isRef$1(scene) ? scene.current : scene;
257
+ function setEnvProps(background, scene, defaultScene, texture, sceneProps = {}) {
258
+ sceneProps = {
259
+ backgroundBlurriness: 0,
260
+ backgroundIntensity: 1,
261
+ backgroundRotation: [0, 0, 0],
262
+ environmentIntensity: 1,
263
+ environmentRotation: [0, 0, 0],
264
+ ...sceneProps
265
+ };
266
+ const target = resolveScene(scene || defaultScene);
267
+ const oldbg = target.background;
268
+ const oldenv = target.environment;
269
+ const oldSceneProps = {
270
+ // @ts-ignore
271
+ backgroundBlurriness: target.backgroundBlurriness,
272
+ // @ts-ignore
273
+ backgroundIntensity: target.backgroundIntensity,
274
+ // @ts-ignore
275
+ backgroundRotation: target.backgroundRotation?.clone?.() ?? [0, 0, 0],
276
+ // @ts-ignore
277
+ environmentIntensity: target.environmentIntensity,
278
+ // @ts-ignore
279
+ environmentRotation: target.environmentRotation?.clone?.() ?? [0, 0, 0]
280
+ };
281
+ if (background !== "only") target.environment = texture;
282
+ if (background) target.background = texture;
283
+ fiber.applyProps(target, sceneProps);
284
+ return () => {
285
+ if (background !== "only") target.environment = oldenv;
286
+ if (background) target.background = oldbg;
287
+ fiber.applyProps(target, oldSceneProps);
288
+ };
289
+ }
290
+ function EnvironmentMap({ scene, background = false, map, ...config }) {
291
+ const defaultScene = fiber.useThree((state) => state.scene);
292
+ React__namespace.useLayoutEffect(() => {
293
+ if (map) return setEnvProps(background, scene, defaultScene, map, config);
294
+ });
295
+ return null;
296
+ }
297
+ function EnvironmentCube({
298
+ background = false,
299
+ scene,
300
+ blur,
301
+ backgroundBlurriness,
302
+ backgroundIntensity,
303
+ backgroundRotation,
304
+ environmentIntensity,
305
+ environmentRotation,
306
+ ...rest
307
+ }) {
308
+ const texture = useEnvironment(rest);
309
+ const defaultScene = fiber.useThree((state) => state.scene);
310
+ React__namespace.useLayoutEffect(() => {
311
+ return setEnvProps(background, scene, defaultScene, texture, {
312
+ backgroundBlurriness: blur ?? backgroundBlurriness,
313
+ backgroundIntensity,
314
+ backgroundRotation,
315
+ environmentIntensity,
316
+ environmentRotation
317
+ });
318
+ });
319
+ React__namespace.useEffect(() => {
320
+ return () => {
321
+ texture.dispose();
322
+ };
323
+ }, [texture]);
324
+ return null;
325
+ }
326
+ function EnvironmentPortal({
327
+ children,
328
+ near = 0.1,
329
+ far = 1e3,
330
+ resolution = 256,
331
+ frames = 1,
332
+ map,
333
+ background = false,
334
+ blur,
335
+ backgroundBlurriness,
336
+ backgroundIntensity,
337
+ backgroundRotation,
338
+ environmentIntensity,
339
+ environmentRotation,
340
+ scene,
341
+ files,
342
+ path,
343
+ preset = void 0,
344
+ extensions
345
+ }) {
346
+ const gl = fiber.useThree((state) => state.gl);
347
+ const defaultScene = fiber.useThree((state) => state.scene);
348
+ const camera = React__namespace.useRef(null);
349
+ const [virtualScene] = React__namespace.useState(() => new webgpu.Scene());
350
+ const fbo = React__namespace.useMemo(() => {
351
+ const fbo2 = new webgpu.WebGLCubeRenderTarget(resolution);
352
+ fbo2.texture.type = webgpu.HalfFloatType;
353
+ return fbo2;
354
+ }, [resolution]);
355
+ React__namespace.useEffect(() => {
356
+ return () => {
357
+ fbo.dispose();
358
+ };
359
+ }, [fbo]);
360
+ React__namespace.useLayoutEffect(() => {
361
+ if (frames === 1) {
362
+ const autoClear = gl.autoClear;
363
+ gl.autoClear = true;
364
+ camera.current.update(gl, virtualScene);
365
+ gl.autoClear = autoClear;
366
+ }
367
+ return setEnvProps(background, scene, defaultScene, fbo.texture, {
368
+ backgroundBlurriness: blur ?? backgroundBlurriness,
369
+ backgroundIntensity,
370
+ backgroundRotation,
371
+ environmentIntensity,
372
+ environmentRotation
373
+ });
374
+ }, [
375
+ children,
376
+ virtualScene,
377
+ fbo.texture,
378
+ scene,
379
+ defaultScene,
380
+ background,
381
+ frames,
382
+ gl,
383
+ blur,
384
+ backgroundBlurriness,
385
+ backgroundIntensity,
386
+ backgroundRotation,
387
+ environmentIntensity,
388
+ environmentRotation
389
+ ]);
390
+ let count = 1;
391
+ fiber.useFrame(() => {
392
+ if (frames === Infinity || count < frames) {
393
+ const autoClear = gl.autoClear;
394
+ gl.autoClear = true;
395
+ camera.current.update(gl, virtualScene);
396
+ gl.autoClear = autoClear;
397
+ count++;
398
+ }
399
+ });
400
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fiber.createPortal(
401
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
402
+ children,
403
+ /* @__PURE__ */ jsxRuntime.jsx("cubeCamera", { ref: camera, args: [near, far, fbo] }),
404
+ files || preset ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentCube, { background: true, files, preset, path, extensions }) : map ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentMap, { background: true, map, extensions }) : null
405
+ ] }),
406
+ virtualScene
407
+ ) });
408
+ }
409
+ function EnvironmentGround(props) {
410
+ const textureDefault = useEnvironment(props);
411
+ const texture = props.map || textureDefault;
412
+ React__namespace.useMemo(() => fiber.extend({ GroundProjectedEnvImpl: GroundedSkybox_js.GroundedSkybox }), []);
413
+ React__namespace.useEffect(() => {
414
+ return () => {
415
+ textureDefault.dispose();
416
+ };
417
+ }, [textureDefault]);
418
+ const height = props.ground?.height ?? 15;
419
+ const radius = props.ground?.radius ?? 60;
420
+ const scale = props.ground?.scale ?? 1e3;
421
+ const args = React__namespace.useMemo(
422
+ () => [texture, height, radius],
423
+ [texture, height, radius]
424
+ );
425
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
426
+ /* @__PURE__ */ jsxRuntime.jsx(EnvironmentMap, { ...props, map: texture }),
427
+ /* @__PURE__ */ jsxRuntime.jsx("groundProjectedEnvImpl", { args, scale })
428
+ ] });
429
+ }
430
+ function EnvironmentColor({ color, scene }) {
431
+ const defaultScene = fiber.useThree((state) => state.scene);
432
+ React__namespace.useLayoutEffect(() => {
433
+ if (color === void 0) return;
434
+ const target = resolveScene(scene || defaultScene);
435
+ const oldBg = target.background;
436
+ target.background = new webgpu.Color(color);
437
+ return () => {
438
+ target.background = oldBg;
439
+ };
440
+ });
441
+ return null;
442
+ }
443
+ function EnvironmentDualSource(props) {
444
+ const { backgroundFiles, ...envProps } = props;
445
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
446
+ /* @__PURE__ */ jsxRuntime.jsx(EnvironmentCube, { ...envProps, background: false }),
447
+ /* @__PURE__ */ jsxRuntime.jsx(EnvironmentCube, { ...props, files: backgroundFiles, background: "only" })
448
+ ] });
449
+ }
450
+ function Environment(props) {
451
+ if (props.color && !props.files && !props.preset && !props.map) {
452
+ return /* @__PURE__ */ jsxRuntime.jsx(EnvironmentColor, { ...props });
453
+ }
454
+ if (props.backgroundFiles && props.backgroundFiles !== props.files) {
455
+ return /* @__PURE__ */ jsxRuntime.jsx(EnvironmentDualSource, { ...props });
456
+ }
457
+ return props.ground ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(EnvironmentCube, { ...props });
458
+ }
459
+
460
+ var __defProp$1 = Object.defineProperty;
461
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
462
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
73
463
  const act = React__namespace["act"];
74
464
  const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React__namespace.useLayoutEffect : React__namespace.useEffect;
75
465
  function useMutableCallback(fn) {
@@ -101,7 +491,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
101
491
  return _a = class extends React__namespace.Component {
102
492
  constructor() {
103
493
  super(...arguments);
104
- __publicField$3(this, "state", { error: false });
494
+ __publicField$1(this, "state", { error: false });
105
495
  }
106
496
  componentDidCatch(err) {
107
497
  this.props.set(err);
@@ -109,7 +499,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
109
499
  render() {
110
500
  return this.state.error ? null : this.props.children;
111
501
  }
112
- }, __publicField$3(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
502
+ }, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
113
503
  })();
114
504
 
115
505
  const is = {
@@ -295,7 +685,7 @@ function createOcclusionObserverNode(store, uniform) {
295
685
  let occlusionSetupPromise = null;
296
686
  function enableOcclusion(store) {
297
687
  const state = store.getState();
298
- const { internal, renderer, rootScene } = state;
688
+ const { internal, renderer } = state;
299
689
  if (internal.occlusionEnabled || occlusionSetupPromise) return;
300
690
  const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
301
691
  if (!hasOcclusionSupport) {
@@ -403,6 +793,17 @@ function unregisterVisibility(store, object) {
403
793
  internal.visibilityRegistry.delete(object.uuid);
404
794
  internal.occlusionCache.delete(object);
405
795
  }
796
+ function updateVisibilityHandlers(store, object, handlers) {
797
+ const { internal } = store.getState();
798
+ const entry = internal.visibilityRegistry.get(object.uuid);
799
+ if (!entry) return false;
800
+ entry.handlers = handlers;
801
+ if ((handlers.onOccluded || handlers.onVisible) && !internal.occlusionEnabled) {
802
+ object.occlusionTest = true;
803
+ enableOcclusion(store);
804
+ }
805
+ return true;
806
+ }
406
807
  function checkVisibility(state) {
407
808
  const { internal, camera } = state;
408
809
  const registry = internal.visibilityRegistry;
@@ -458,6 +859,22 @@ function hasVisibilityHandlers(handlers) {
458
859
  return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
459
860
  }
460
861
 
862
+ const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
863
+ function fromRef(ref) {
864
+ return { [FROM_REF]: ref };
865
+ }
866
+ function isFromRef(value) {
867
+ return value !== null && typeof value === "object" && FROM_REF in value;
868
+ }
869
+
870
+ const ONCE = Symbol.for("@react-three/fiber.once");
871
+ function once(...args) {
872
+ return { [ONCE]: args.length ? args : true };
873
+ }
874
+ function isOnce(value) {
875
+ return value !== null && typeof value === "object" && ONCE in value;
876
+ }
877
+
461
878
  const RESERVED_PROPS = [
462
879
  "children",
463
880
  "key",
@@ -528,7 +945,7 @@ function getMemoizedPrototype(root) {
528
945
  ctor = new root.constructor();
529
946
  MEMOIZED_PROTOTYPES.set(root.constructor, ctor);
530
947
  }
531
- } catch (e) {
948
+ } catch {
532
949
  }
533
950
  return ctor;
534
951
  }
@@ -574,6 +991,22 @@ function applyProps(object, props) {
574
991
  continue;
575
992
  }
576
993
  if (value === void 0) continue;
994
+ if (isFromRef(value)) continue;
995
+ if (isOnce(value)) {
996
+ if (instance?.appliedOnce?.has(prop)) continue;
997
+ if (instance) {
998
+ instance.appliedOnce ?? (instance.appliedOnce = /* @__PURE__ */ new Set());
999
+ instance.appliedOnce.add(prop);
1000
+ }
1001
+ const { root: targetRoot, key: targetKey } = resolve(object, prop);
1002
+ const args = value[ONCE];
1003
+ if (typeof targetRoot[targetKey] === "function") {
1004
+ targetRoot[targetKey](...args === true ? [] : args);
1005
+ } else if (args !== true && args.length > 0) {
1006
+ targetRoot[targetKey] = args[0];
1007
+ }
1008
+ continue;
1009
+ }
577
1010
  let { root, key, target } = resolve(object, prop);
578
1011
  if (target === void 0 && (typeof root !== "object" || root === null)) {
579
1012
  throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`);
@@ -594,9 +1027,25 @@ function applyProps(object, props) {
594
1027
  } else if (isVectorLike(target) && is.num(value)) {
595
1028
  if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
596
1029
  else target.set(value);
1030
+ } else if (root instanceof webgpu.ShaderMaterial && key === "uniforms" && is.obj(value)) {
1031
+ if (!is.obj(root.uniforms)) root.uniforms = {};
1032
+ const uniforms = root.uniforms;
1033
+ const nextUniforms = value;
1034
+ for (const name in nextUniforms) {
1035
+ const uniform = nextUniforms[name];
1036
+ const targetUniform = uniforms[name];
1037
+ if (targetUniform) Object.assign(targetUniform, uniform);
1038
+ else {
1039
+ const nextUniform = uniform instanceof webgpu.Uniform ? uniform.clone() : new webgpu.Uniform(uniform.value);
1040
+ uniforms[name] = nextUniform;
1041
+ }
1042
+ }
597
1043
  } else {
598
1044
  root[key] = value;
599
- 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
1045
+ if (key.endsWith("Node") && root.isMaterial) {
1046
+ root.needsUpdate = true;
1047
+ }
1048
+ if (rootState && rootState.renderer?.outputColorSpace === webgpu.SRGBColorSpace && colorMaps.includes(key) && isTexture(value) && root[key]?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
600
1049
  root[key].format === webgpu.RGBAFormat && root[key].type === webgpu.UnsignedByteType) {
601
1050
  root[key].colorSpace = rootState.textureColorSpace;
602
1051
  }
@@ -609,6 +1058,9 @@ function applyProps(object, props) {
609
1058
  if (instance.eventCount && object2.raycast !== null) {
610
1059
  rootState.internal.interaction.push(object2);
611
1060
  }
1061
+ }
1062
+ if (instance?.parent && rootState?.internal && instance.object?.isObject3D) {
1063
+ const object2 = instance.object;
612
1064
  const root = findInitialRoot(instance);
613
1065
  const visibilityHandlers = {
614
1066
  onFramed: instance.handlers.onFramed,
@@ -616,7 +1068,9 @@ function applyProps(object, props) {
616
1068
  onVisible: instance.handlers.onVisible
617
1069
  };
618
1070
  if (hasVisibilityHandlers(visibilityHandlers)) {
619
- registerVisibility(root, object2, visibilityHandlers);
1071
+ if (!updateVisibilityHandlers(root, object2, visibilityHandlers)) {
1072
+ registerVisibility(root, object2, visibilityHandlers);
1073
+ }
620
1074
  } else {
621
1075
  unregisterVisibility(root, object2);
622
1076
  }
@@ -629,38 +1083,60 @@ function applyProps(object, props) {
629
1083
  return object;
630
1084
  }
631
1085
 
1086
+ const DEFAULT_POINTER_ID = 0;
1087
+ const XR_POINTER_ID_START = 1e3;
1088
+ function getPointerState(internal, pointerId) {
1089
+ let state = internal.pointerMap.get(pointerId);
1090
+ if (!state) {
1091
+ state = {
1092
+ hovered: /* @__PURE__ */ new Map(),
1093
+ captured: /* @__PURE__ */ new Map(),
1094
+ initialClick: [0, 0],
1095
+ initialHits: []
1096
+ };
1097
+ internal.pointerMap.set(pointerId, state);
1098
+ }
1099
+ return state;
1100
+ }
1101
+ function getPointerId(event) {
1102
+ return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
1103
+ }
632
1104
  function makeId(event) {
633
1105
  return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
634
1106
  }
635
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
636
- const captureData = captures.get(obj);
1107
+ function releaseInternalPointerCapture(internal, obj, pointerId) {
1108
+ const pointerState = internal.pointerMap.get(pointerId);
1109
+ if (!pointerState) return;
1110
+ const captureData = pointerState.captured.get(obj);
637
1111
  if (captureData) {
638
- captures.delete(obj);
639
- if (captures.size === 0) {
640
- capturedMap.delete(pointerId);
641
- captureData.target.releasePointerCapture(pointerId);
642
- }
1112
+ pointerState.captured.delete(obj);
1113
+ captureData.target.releasePointerCapture(pointerId);
643
1114
  }
644
1115
  }
645
1116
  function removeInteractivity(store, object) {
646
1117
  const { internal } = store.getState();
647
1118
  internal.interaction = internal.interaction.filter((o) => o !== object);
648
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
649
- internal.hovered.forEach((value, key) => {
650
- if (value.eventObject === object || value.object === object) {
651
- internal.hovered.delete(key);
1119
+ for (const [pointerId, pointerState] of internal.pointerMap) {
1120
+ pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
1121
+ pointerState.hovered.forEach((value, key) => {
1122
+ if (value.eventObject === object || value.object === object) {
1123
+ pointerState.hovered.delete(key);
1124
+ }
1125
+ });
1126
+ if (pointerState.captured.has(object)) {
1127
+ releaseInternalPointerCapture(internal, object, pointerId);
652
1128
  }
653
- });
654
- internal.capturedMap.forEach((captures, pointerId) => {
655
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
656
- });
1129
+ }
657
1130
  unregisterVisibility(store, object);
658
1131
  }
659
1132
  function createEvents(store) {
660
- function calculateDistance(event) {
1133
+ function calculateDistance(event, pointerId) {
661
1134
  const { internal } = store.getState();
662
- const dx = event.offsetX - internal.initialClick[0];
663
- const dy = event.offsetY - internal.initialClick[1];
1135
+ const pointerState = internal.pointerMap.get(pointerId);
1136
+ if (!pointerState) return 0;
1137
+ const [initialX, initialY] = pointerState.initialClick;
1138
+ const dx = event.offsetX - initialX;
1139
+ const dy = event.offsetY - initialY;
664
1140
  return Math.round(Math.sqrt(dx * dx + dy * dy));
665
1141
  }
666
1142
  function filterPointerEvents(objects) {
@@ -696,6 +1172,15 @@ function createEvents(store) {
696
1172
  return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
697
1173
  }
698
1174
  let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
1175
+ const aInteractivePriority = a.object.userData?.interactivePriority;
1176
+ const bInteractivePriority = b.object.userData?.interactivePriority;
1177
+ if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
1178
+ if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
1179
+ if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
1180
+ if (aInteractivePriority !== bInteractivePriority) {
1181
+ return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
1182
+ }
1183
+ }
699
1184
  const aState = getRootState(a.object);
700
1185
  const bState = getRootState(b.object);
701
1186
  const aPriority = aState?.events?.priority ?? 1;
@@ -717,9 +1202,13 @@ function createEvents(store) {
717
1202
  eventObject = eventObject.parent;
718
1203
  }
719
1204
  }
720
- if ("pointerId" in event && state.internal.capturedMap.has(event.pointerId)) {
721
- for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
722
- if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1205
+ if ("pointerId" in event) {
1206
+ const pointerId = event.pointerId;
1207
+ const pointerState = state.internal.pointerMap.get(pointerId);
1208
+ if (pointerState?.captured.size) {
1209
+ for (const captureData of pointerState.captured.values()) {
1210
+ if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1211
+ }
723
1212
  }
724
1213
  }
725
1214
  return intersections;
@@ -732,27 +1221,25 @@ function createEvents(store) {
732
1221
  if (state) {
733
1222
  const { raycaster, pointer, camera, internal } = state;
734
1223
  const unprojectedPoint = new webgpu.Vector3(pointer.x, pointer.y, 0).unproject(camera);
735
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1224
+ const hasPointerCapture = (id) => {
1225
+ const pointerState = internal.pointerMap.get(id);
1226
+ return pointerState?.captured.has(hit.eventObject) ?? false;
1227
+ };
736
1228
  const setPointerCapture = (id) => {
737
1229
  const captureData = { intersection: hit, target: event.target };
738
- if (internal.capturedMap.has(id)) {
739
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
740
- } else {
741
- internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
742
- }
1230
+ const pointerState = getPointerState(internal, id);
1231
+ pointerState.captured.set(hit.eventObject, captureData);
743
1232
  event.target.setPointerCapture(id);
744
1233
  };
745
1234
  const releasePointerCapture = (id) => {
746
- const captures = internal.capturedMap.get(id);
747
- if (captures) {
748
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
749
- }
1235
+ releaseInternalPointerCapture(internal, hit.eventObject, id);
750
1236
  };
751
1237
  const extractEventProps = {};
752
1238
  for (const prop in event) {
753
1239
  const property = event[prop];
754
1240
  if (typeof property !== "function") extractEventProps[prop] = property;
755
1241
  }
1242
+ const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
756
1243
  const raycastEvent = {
757
1244
  ...hit,
758
1245
  ...extractEventProps,
@@ -763,18 +1250,19 @@ function createEvents(store) {
763
1250
  unprojectedPoint,
764
1251
  ray: raycaster.ray,
765
1252
  camera,
1253
+ pointerId: eventPointerId,
766
1254
  // Hijack stopPropagation, which just sets a flag
767
1255
  stopPropagation() {
768
- const capturesForPointer = "pointerId" in event && internal.capturedMap.get(event.pointerId);
1256
+ const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
769
1257
  if (
770
1258
  // ...if this pointer hasn't been captured
771
- !capturesForPointer || // ... or if the hit object is capturing the pointer
772
- capturesForPointer.has(hit.eventObject)
1259
+ !pointerState?.captured.size || // ... or if the hit object is capturing the pointer
1260
+ pointerState.captured.has(hit.eventObject)
773
1261
  ) {
774
1262
  raycastEvent.stopped = localState.stopped = true;
775
- if (internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1263
+ if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
776
1264
  const higher = intersections.slice(0, intersections.indexOf(hit));
777
- cancelPointer([...higher, hit]);
1265
+ cancelPointer([...higher, hit], eventPointerId);
778
1266
  }
779
1267
  }
780
1268
  },
@@ -790,15 +1278,18 @@ function createEvents(store) {
790
1278
  }
791
1279
  return intersections;
792
1280
  }
793
- function cancelPointer(intersections) {
1281
+ function cancelPointer(intersections, pointerId) {
794
1282
  const { internal } = store.getState();
795
- for (const hoveredObj of internal.hovered.values()) {
1283
+ const pid = pointerId ?? DEFAULT_POINTER_ID;
1284
+ const pointerState = internal.pointerMap.get(pid);
1285
+ if (!pointerState) return;
1286
+ for (const [hoveredId, hoveredObj] of pointerState.hovered) {
796
1287
  if (!intersections.length || !intersections.find(
797
1288
  (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
798
1289
  )) {
799
1290
  const eventObject = hoveredObj.eventObject;
800
1291
  const instance = eventObject.__r3f;
801
- internal.hovered.delete(makeId(hoveredObj));
1292
+ pointerState.hovered.delete(hoveredId);
802
1293
  if (instance?.eventCount) {
803
1294
  const handlers = instance.handlers;
804
1295
  const data = { ...hoveredObj, intersections };
@@ -827,41 +1318,118 @@ function createEvents(store) {
827
1318
  instance?.handlers.onDropMissed?.(event);
828
1319
  }
829
1320
  }
1321
+ function cleanupPointer(pointerId) {
1322
+ const { internal } = store.getState();
1323
+ const pointerState = internal.pointerMap.get(pointerId);
1324
+ if (pointerState) {
1325
+ for (const [, hoveredObj] of pointerState.hovered) {
1326
+ const eventObject = hoveredObj.eventObject;
1327
+ const instance = eventObject.__r3f;
1328
+ if (instance?.eventCount) {
1329
+ const handlers = instance.handlers;
1330
+ const data = { ...hoveredObj, intersections: [] };
1331
+ handlers.onPointerOut?.(data);
1332
+ handlers.onPointerLeave?.(data);
1333
+ }
1334
+ }
1335
+ internal.pointerMap.delete(pointerId);
1336
+ }
1337
+ internal.pointerDirty.delete(pointerId);
1338
+ }
1339
+ function processDeferredPointer(event, pointerId) {
1340
+ const state = store.getState();
1341
+ const { internal } = state;
1342
+ if (!state.events.enabled) return;
1343
+ const filter = filterPointerEvents;
1344
+ const hits = intersect(event, filter);
1345
+ cancelPointer(hits, pointerId);
1346
+ function onIntersect(data) {
1347
+ const eventObject = data.eventObject;
1348
+ const instance = eventObject.__r3f;
1349
+ if (!instance?.eventCount) return;
1350
+ const handlers = instance.handlers;
1351
+ if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1352
+ const id = makeId(data);
1353
+ const pointerState = getPointerState(internal, pointerId);
1354
+ const hoveredItem = pointerState.hovered.get(id);
1355
+ if (!hoveredItem) {
1356
+ pointerState.hovered.set(id, data);
1357
+ handlers.onPointerOver?.(data);
1358
+ handlers.onPointerEnter?.(data);
1359
+ } else if (hoveredItem.stopped) {
1360
+ data.stopPropagation();
1361
+ }
1362
+ }
1363
+ handlers.onPointerMove?.(data);
1364
+ }
1365
+ handleIntersects(hits, event, 0, onIntersect);
1366
+ }
830
1367
  function handlePointer(name) {
831
1368
  switch (name) {
832
1369
  case "onPointerLeave":
833
- case "onPointerCancel":
834
1370
  case "onDragLeave":
835
1371
  return () => cancelPointer([]);
1372
+ // Global cancel of these events
1373
+ case "onPointerCancel":
1374
+ return (event) => {
1375
+ const pointerId = getPointerId(event);
1376
+ cleanupPointer(pointerId);
1377
+ };
836
1378
  case "onLostPointerCapture":
837
1379
  return (event) => {
838
1380
  const { internal } = store.getState();
839
- if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) {
1381
+ const pointerId = getPointerId(event);
1382
+ const pointerState = internal.pointerMap.get(pointerId);
1383
+ if (pointerState?.captured.size) {
840
1384
  requestAnimationFrame(() => {
841
- if (internal.capturedMap.has(event.pointerId)) {
842
- internal.capturedMap.delete(event.pointerId);
843
- cancelPointer([]);
1385
+ const pointerState2 = internal.pointerMap.get(pointerId);
1386
+ if (pointerState2?.captured.size) {
1387
+ pointerState2.captured.clear();
844
1388
  }
1389
+ cancelPointer([], pointerId);
845
1390
  });
846
1391
  }
847
1392
  };
848
1393
  }
849
1394
  return function handleEvent(event) {
850
1395
  const state = store.getState();
851
- const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
1396
+ const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
1397
+ const pointerId = getPointerId(event);
852
1398
  internal.lastEvent.current = event;
853
- if (!state.events.enabled) return;
1399
+ if (!events.enabled) return;
854
1400
  const isPointerMove = name === "onPointerMove";
855
1401
  const isDragOver = name === "onDragOver";
856
1402
  const isDrop = name === "onDrop";
857
1403
  const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
1404
+ const isPointerDown = name === "onPointerDown";
1405
+ const isPointerUp = name === "onPointerUp";
1406
+ const isWheel = name === "onWheel";
1407
+ const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
1408
+ if (isPointerMove && canDeferRaycasts) {
1409
+ events.compute?.(event, state);
1410
+ internal.pointerDirty.set(pointerId, event);
1411
+ return;
1412
+ }
1413
+ if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
1414
+ events.compute?.(event, state);
1415
+ internal.pointerDirty.set(pointerId, event);
1416
+ return;
1417
+ }
1418
+ if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
1419
+ const deferredEvent = internal.pointerDirty.get(pointerId);
1420
+ internal.pointerDirty.delete(pointerId);
1421
+ processDeferredPointer(deferredEvent, pointerId);
1422
+ }
858
1423
  const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
859
1424
  const hits = intersect(event, filter);
860
- const delta = isClickEvent ? calculateDistance(event) : 0;
861
- if (name === "onPointerDown") {
862
- internal.initialClick = [event.offsetX, event.offsetY];
863
- internal.initialHits = hits.map((hit) => hit.eventObject);
864
- }
1425
+ const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
1426
+ if (isPointerDown) {
1427
+ const pointerState2 = getPointerState(internal, pointerId);
1428
+ pointerState2.initialClick = [event.offsetX, event.offsetY];
1429
+ pointerState2.initialHits = hits.map((hit) => hit.eventObject);
1430
+ }
1431
+ const pointerState = internal.pointerMap.get(pointerId);
1432
+ const initialHits = pointerState?.initialHits ?? [];
865
1433
  if (isClickEvent && !hits.length) {
866
1434
  if (delta <= 2) {
867
1435
  pointerMissed(event, internal.interaction);
@@ -876,7 +1444,9 @@ function createEvents(store) {
876
1444
  dropMissed(event, internal.interaction);
877
1445
  if (onDropMissed) onDropMissed(event);
878
1446
  }
879
- if (isPointerMove || isDragOver) cancelPointer(hits);
1447
+ if (isPointerMove || isDragOver) {
1448
+ cancelPointer(hits, pointerId);
1449
+ }
880
1450
  function onIntersect(data) {
881
1451
  const eventObject = data.eventObject;
882
1452
  const instance = eventObject.__r3f;
@@ -885,9 +1455,10 @@ function createEvents(store) {
885
1455
  if (isPointerMove) {
886
1456
  if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
887
1457
  const id = makeId(data);
888
- const hoveredItem = internal.hovered.get(id);
1458
+ const pointerState2 = getPointerState(internal, pointerId);
1459
+ const hoveredItem = pointerState2.hovered.get(id);
889
1460
  if (!hoveredItem) {
890
- internal.hovered.set(id, data);
1461
+ pointerState2.hovered.set(id, data);
891
1462
  handlers.onPointerOver?.(data);
892
1463
  handlers.onPointerEnter?.(data);
893
1464
  } else if (hoveredItem.stopped) {
@@ -897,9 +1468,10 @@ function createEvents(store) {
897
1468
  handlers.onPointerMove?.(data);
898
1469
  } else if (isDragOver) {
899
1470
  const id = makeId(data);
900
- const hoveredItem = internal.hovered.get(id);
1471
+ const pointerState2 = getPointerState(internal, pointerId);
1472
+ const hoveredItem = pointerState2.hovered.get(id);
901
1473
  if (!hoveredItem) {
902
- internal.hovered.set(id, data);
1474
+ pointerState2.hovered.set(id, data);
903
1475
  handlers.onDragOverEnter?.(data);
904
1476
  } else if (hoveredItem.stopped) {
905
1477
  data.stopPropagation();
@@ -910,18 +1482,18 @@ function createEvents(store) {
910
1482
  } else {
911
1483
  const handler = handlers[name];
912
1484
  if (handler) {
913
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1485
+ if (!isClickEvent || initialHits.includes(eventObject)) {
914
1486
  pointerMissed(
915
1487
  event,
916
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1488
+ internal.interaction.filter((object) => !initialHits.includes(object))
917
1489
  );
918
1490
  handler(data);
919
1491
  }
920
1492
  } else {
921
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1493
+ if (isClickEvent && initialHits.includes(eventObject)) {
922
1494
  pointerMissed(
923
1495
  event,
924
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1496
+ internal.interaction.filter((object) => !initialHits.includes(object))
925
1497
  );
926
1498
  }
927
1499
  }
@@ -930,7 +1502,15 @@ function createEvents(store) {
930
1502
  handleIntersects(hits, event, delta, onIntersect);
931
1503
  };
932
1504
  }
933
- return { handlePointer };
1505
+ function flushDeferredPointers() {
1506
+ const { internal, events } = store.getState();
1507
+ if (!events.frameTimedRaycasts) return;
1508
+ for (const [pointerId, event] of internal.pointerDirty) {
1509
+ processDeferredPointer(event, pointerId);
1510
+ }
1511
+ internal.pointerDirty.clear();
1512
+ }
1513
+ return { handlePointer, flushDeferredPointers, processDeferredPointer };
934
1514
  }
935
1515
  const DOM_EVENTS = {
936
1516
  onClick: ["click", false],
@@ -949,11 +1529,16 @@ const DOM_EVENTS = {
949
1529
  onLostPointerCapture: ["lostpointercapture", true]
950
1530
  };
951
1531
  function createPointerEvents(store) {
952
- const { handlePointer } = createEvents(store);
1532
+ const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
1533
+ let nextXRPointerId = XR_POINTER_ID_START;
1534
+ const xrPointers = /* @__PURE__ */ new Map();
953
1535
  return {
954
1536
  priority: 1,
955
1537
  enabled: true,
956
- compute(event, state, previous) {
1538
+ frameTimedRaycasts: true,
1539
+ alwaysFireOnScroll: true,
1540
+ updateOnFrame: false,
1541
+ compute(event, state) {
957
1542
  state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
958
1543
  state.raycaster.setFromCamera(state.pointer, state.camera);
959
1544
  },
@@ -962,11 +1547,33 @@ function createPointerEvents(store) {
962
1547
  (acc, key) => ({ ...acc, [key]: handlePointer(key) }),
963
1548
  {}
964
1549
  ),
965
- update: () => {
1550
+ update: (pointerId) => {
966
1551
  const { events, internal } = store.getState();
967
- if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current);
1552
+ if (!events.handlers) return;
1553
+ if (pointerId !== void 0) {
1554
+ const event = internal.pointerDirty.get(pointerId);
1555
+ if (event) {
1556
+ internal.pointerDirty.delete(pointerId);
1557
+ processDeferredPointer(event, pointerId);
1558
+ } else if (internal.lastEvent?.current) {
1559
+ processDeferredPointer(internal.lastEvent.current, pointerId);
1560
+ }
1561
+ } else {
1562
+ flushDeferredPointers();
1563
+ if (internal.lastEvent?.current) {
1564
+ events.handlers.onPointerMove(internal.lastEvent.current);
1565
+ }
1566
+ }
1567
+ },
1568
+ flush: () => {
1569
+ const { events, internal } = store.getState();
1570
+ flushDeferredPointers();
1571
+ if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
1572
+ events.handlers.onPointerMove(internal.lastEvent.current);
1573
+ }
968
1574
  },
969
1575
  connect: (target) => {
1576
+ if (!target) return;
970
1577
  const { set, events } = store.getState();
971
1578
  events.disconnect?.();
972
1579
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -990,6 +1597,33 @@ function createPointerEvents(store) {
990
1597
  }
991
1598
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
992
1599
  }
1600
+ },
1601
+ // PREVIEW (A8): the XR pointer registration API may change before stable.
1602
+ registerPointer: (config) => {
1603
+ const pointerId = nextXRPointerId++;
1604
+ xrPointers.set(pointerId, config);
1605
+ const { internal } = store.getState();
1606
+ getPointerState(internal, pointerId);
1607
+ return pointerId;
1608
+ },
1609
+ unregisterPointer: (pointerId) => {
1610
+ xrPointers.delete(pointerId);
1611
+ const { internal } = store.getState();
1612
+ const pointerState = internal.pointerMap.get(pointerId);
1613
+ if (pointerState) {
1614
+ for (const [, hoveredObj] of pointerState.hovered) {
1615
+ const eventObject = hoveredObj.eventObject;
1616
+ const instance = eventObject.__r3f;
1617
+ if (instance?.eventCount) {
1618
+ const handlers = instance.handlers;
1619
+ const data = { ...hoveredObj, intersections: [] };
1620
+ handlers.onPointerOut?.(data);
1621
+ handlers.onPointerLeave?.(data);
1622
+ }
1623
+ }
1624
+ internal.pointerMap.delete(pointerId);
1625
+ }
1626
+ internal.pointerDirty.delete(pointerId);
993
1627
  }
994
1628
  };
995
1629
  }
@@ -1051,6 +1685,14 @@ function notifyAlpha({ message, link }) {
1051
1685
  }
1052
1686
  }
1053
1687
 
1688
+ const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
1689
+ const INTERNAL_FIBER_MARKERS = ["packages/fiber/src", "packages/fiber/dist", "@react-three/fiber", "@react-three_fiber"];
1690
+ function isInternalRendererAccess(stack, callerDepth = 2) {
1691
+ if (!stack) return false;
1692
+ const caller = stack.split("\n")[callerDepth] ?? "";
1693
+ return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m)) || INTERNAL_FIBER_MARKERS.some((m) => caller.includes(m));
1694
+ }
1695
+
1054
1696
  const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
1055
1697
  const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React__namespace.createContext(null));
1056
1698
  const createStore = (invalidate, advance) => {
@@ -1080,23 +1722,29 @@ const createStore = (invalidate, advance) => {
1080
1722
  set,
1081
1723
  get,
1082
1724
  // Mock objects that have to be configured
1725
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
1726
+ primaryStore: null,
1083
1727
  gl: null,
1084
1728
  renderer: null,
1085
1729
  camera: null,
1086
1730
  frustum: new webgpu.Frustum(),
1087
1731
  autoUpdateFrustum: true,
1088
1732
  raycaster: null,
1089
- events: { priority: 1, enabled: true, connected: false },
1733
+ events: {
1734
+ priority: 1,
1735
+ enabled: true,
1736
+ connected: false,
1737
+ frameTimedRaycasts: true,
1738
+ alwaysFireOnScroll: true,
1739
+ updateOnFrame: false
1740
+ },
1090
1741
  scene: null,
1091
1742
  rootScene: null,
1092
1743
  xr: null,
1093
1744
  inspector: null,
1094
1745
  invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
1095
1746
  advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
1096
- legacy: false,
1097
- linear: false,
1098
- flat: false,
1099
- textureColorSpace: "srgb",
1747
+ textureColorSpace: webgpu.SRGBColorSpace,
1100
1748
  isLegacy: false,
1101
1749
  webGPUSupported: false,
1102
1750
  isNative: false,
@@ -1154,6 +1802,7 @@ const createStore = (invalidate, advance) => {
1154
1802
  size: newSize,
1155
1803
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
1156
1804
  }));
1805
+ scheduler.getScheduler().invalidate();
1157
1806
  }
1158
1807
  }
1159
1808
  return;
@@ -1168,6 +1817,7 @@ const createStore = (invalidate, advance) => {
1168
1817
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
1169
1818
  _sizeImperative: true
1170
1819
  }));
1820
+ scheduler.getScheduler().invalidate();
1171
1821
  },
1172
1822
  setDpr: (dpr) => set((state2) => {
1173
1823
  const resolved = calculateDpr(dpr);
@@ -1175,14 +1825,18 @@ const createStore = (invalidate, advance) => {
1175
1825
  }),
1176
1826
  setFrameloop: (frameloop = "always") => {
1177
1827
  set(() => ({ frameloop }));
1828
+ scheduler.getScheduler().frameloop = frameloop;
1178
1829
  },
1179
1830
  setError: (error) => set(() => ({ error })),
1180
1831
  error: null,
1181
- //* TSL State (managed via hooks: useUniforms, useNodes, useTextures, usePostProcessing) ==============================
1832
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
1182
1833
  uniforms: {},
1183
1834
  nodes: {},
1835
+ buffers: {},
1836
+ gpuStorage: {},
1184
1837
  textures: /* @__PURE__ */ new Map(),
1185
- postProcessing: null,
1838
+ _textureRefs: /* @__PURE__ */ new Map(),
1839
+ renderPipeline: null,
1186
1840
  passes: {},
1187
1841
  _hmrVersion: 0,
1188
1842
  _sizeImperative: false,
@@ -1191,12 +1845,16 @@ const createStore = (invalidate, advance) => {
1191
1845
  internal: {
1192
1846
  // Events
1193
1847
  interaction: [],
1194
- hovered: /* @__PURE__ */ new Map(),
1195
1848
  subscribers: [],
1849
+ // Per-pointer state (new unified structure)
1850
+ pointerMap: /* @__PURE__ */ new Map(),
1851
+ pointerDirty: /* @__PURE__ */ new Map(),
1852
+ lastEvent: React__namespace.createRef(),
1853
+ // Deprecated but kept for backwards compatibility
1854
+ hovered: /* @__PURE__ */ new Map(),
1196
1855
  initialClick: [0, 0],
1197
1856
  initialHits: [],
1198
1857
  capturedMap: /* @__PURE__ */ new Map(),
1199
- lastEvent: React__namespace.createRef(),
1200
1858
  // Visibility tracking (onFramed, onOccluded, onVisible)
1201
1859
  visibilityRegistry: /* @__PURE__ */ new Map(),
1202
1860
  // Occlusion system (WebGPU only)
@@ -1235,8 +1893,7 @@ const createStore = (invalidate, advance) => {
1235
1893
  const currentState = rootStore.getState();
1236
1894
  if (!currentState.isLegacy && currentState.internal.actualRenderer) {
1237
1895
  const stack = new Error().stack || "";
1238
- const isInternalAccess = stack.includes("zustand") || stack.includes("setState") || stack.includes("Object.assign") || stack.includes("react-three-fiber/packages/fiber/src/core");
1239
- if (!isInternalAccess) {
1896
+ if (!isInternalRendererAccess(stack)) {
1240
1897
  const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
1241
1898
  notifyDepreciated({
1242
1899
  heading: "Accessing state.gl in WebGPU mode",
@@ -1279,13 +1936,22 @@ const createStore = (invalidate, advance) => {
1279
1936
  rootStore.subscribe(() => {
1280
1937
  const { camera, size, viewport, set, internal } = rootStore.getState();
1281
1938
  const actualRenderer = internal.actualRenderer;
1939
+ const canvasTarget = internal.canvasTarget;
1282
1940
  if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1283
1941
  oldSize = size;
1284
1942
  oldDpr = viewport.dpr;
1285
1943
  updateCamera(camera, size);
1286
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1287
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && actualRenderer.domElement instanceof HTMLCanvasElement;
1288
- actualRenderer.setSize(size.width, size.height, updateStyle);
1944
+ if (internal.isSecondary && canvasTarget) {
1945
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1946
+ canvasTarget.setSize(size.width, size.height, false);
1947
+ } else {
1948
+ if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1949
+ actualRenderer.setSize(size.width, size.height, false);
1950
+ if (canvasTarget) {
1951
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1952
+ canvasTarget.setSize(size.width, size.height, false);
1953
+ }
1954
+ }
1289
1955
  }
1290
1956
  if (camera !== oldCamera) {
1291
1957
  oldCamera = camera;
@@ -1340,1157 +2006,125 @@ function loadingFn(extensions, onProgress) {
1340
2006
  );
1341
2007
  };
1342
2008
  }
1343
- function useLoader(loader, input, extensions, onProgress) {
1344
- const keys = Array.isArray(input) ? input : [input];
2009
+ function useLoader(loader, input, extensions, onProgress, cacheKey) {
2010
+ const inputs = Array.isArray(input) ? input : [input];
2011
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1345
2012
  const fn = loadingFn(extensions, onProgress);
1346
- const results = keys.map((key) => suspendReact.suspend(fn, [loader, key], { equal: is.equ }));
2013
+ const results = cacheKeys.map(
2014
+ (key, index) => suspendReact.suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
2015
+ );
1347
2016
  return Array.isArray(input) ? results : results[0];
1348
2017
  }
1349
- useLoader.preload = function(loader, input, extensions, onProgress) {
1350
- const keys = Array.isArray(input) ? input : [input];
1351
- keys.forEach((key) => suspendReact.preload(loadingFn(extensions, onProgress), [loader, key]));
2018
+ useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
2019
+ const inputs = Array.isArray(input) ? input : [input];
2020
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2021
+ cacheKeys.forEach(
2022
+ (key, index) => suspendReact.preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
2023
+ );
1352
2024
  };
1353
- useLoader.clear = function(loader, input) {
1354
- const keys = Array.isArray(input) ? input : [input];
1355
- keys.forEach((key) => suspendReact.clear([loader, key]));
2025
+ useLoader.clear = function(loader, input, cacheKey) {
2026
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2027
+ cacheKeys.forEach((key) => suspendReact.clear([loader, key]));
1356
2028
  };
1357
2029
  useLoader.loader = getLoader;
1358
2030
 
1359
- var __defProp$2 = Object.defineProperty;
1360
- var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1361
- var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
1362
- const DEFAULT_PHASES = ["start", "input", "physics", "update", "render", "finish"];
1363
- class PhaseGraph {
1364
- constructor() {
1365
- /** Ordered list of phase nodes */
1366
- __publicField$2(this, "phases", []);
1367
- /** Quick lookup by name */
1368
- __publicField$2(this, "phaseMap", /* @__PURE__ */ new Map());
1369
- /** Cached ordered names (invalidated on changes) */
1370
- __publicField$2(this, "orderedNamesCache", null);
1371
- this.initializeDefaultPhases();
1372
- }
1373
- //* Initialization --------------------------------
1374
- initializeDefaultPhases() {
1375
- for (const name of DEFAULT_PHASES) {
1376
- const node = { name, isAutoGenerated: false };
1377
- this.phases.push(node);
1378
- this.phaseMap.set(name, node);
1379
- }
1380
- this.invalidateCache();
1381
- }
1382
- //* Public API --------------------------------
1383
- /**
1384
- * Add a named phase to the graph
1385
- * @param name - Phase name (must be unique)
1386
- * @param options - Position options (before or after another phase)
1387
- */
1388
- addPhase(name, options = {}) {
1389
- if (this.phaseMap.has(name)) {
1390
- console.warn(`[useFrame] Phase "${name}" already exists`);
1391
- return;
1392
- }
1393
- const { before, after } = options;
1394
- const node = { name, isAutoGenerated: false };
1395
- let insertIndex = this.phases.length;
1396
- const targetIndex = this.getPhaseIndex(before ?? after);
1397
- if (targetIndex !== -1) {
1398
- insertIndex = before ? targetIndex : targetIndex + 1;
2031
+ function useFrame(callback, priorityOrOptions) {
2032
+ const store = React__namespace.useContext(context);
2033
+ const isInsideCanvas = store !== null;
2034
+ const scheduler$1 = scheduler.getScheduler();
2035
+ const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
2036
+ id: priorityOrOptions.id,
2037
+ phase: priorityOrOptions.phase,
2038
+ priority: priorityOrOptions.priority,
2039
+ fps: priorityOrOptions.fps,
2040
+ drop: priorityOrOptions.drop,
2041
+ enabled: priorityOrOptions.enabled,
2042
+ before: priorityOrOptions.before,
2043
+ after: priorityOrOptions.after
2044
+ }) : "";
2045
+ const options = React__namespace.useMemo(() => {
2046
+ return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
2047
+ }, [optionsKey]);
2048
+ const reactId = React__namespace.useId();
2049
+ const id = options.id ?? reactId;
2050
+ const callbackRef = useMutableCallback(callback);
2051
+ const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
2052
+ useIsomorphicLayoutEffect(() => {
2053
+ if (!callback) return;
2054
+ if (isInsideCanvas) {
2055
+ const state = store.getState();
2056
+ const rootId = state.internal.rootId;
2057
+ if (isLegacyPriority) {
2058
+ state.internal.priority++;
2059
+ let parentRoot = state.previousRoot;
2060
+ while (parentRoot) {
2061
+ const parentState = parentRoot.getState();
2062
+ if (parentState?.internal) parentState.internal.priority++;
2063
+ parentRoot = parentState?.previousRoot;
2064
+ }
2065
+ notifyDepreciated({
2066
+ heading: "useFrame with numeric priority is deprecated",
2067
+ 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 })',
2068
+ link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2069
+ });
2070
+ }
2071
+ const wrappedCallback = (frameState, delta) => {
2072
+ const localState = store.getState();
2073
+ const mergedState = {
2074
+ ...localState,
2075
+ time: frameState.time,
2076
+ delta: frameState.delta,
2077
+ elapsed: frameState.elapsed,
2078
+ frame: frameState.frame
2079
+ };
2080
+ callbackRef.current?.(mergedState, delta);
2081
+ };
2082
+ const unregister = scheduler$1.register(wrappedCallback, {
2083
+ id,
2084
+ rootId,
2085
+ ...options
2086
+ });
2087
+ return () => {
2088
+ unregister();
2089
+ if (isLegacyPriority) {
2090
+ const currentState = store.getState();
2091
+ if (currentState.internal) {
2092
+ currentState.internal.priority--;
2093
+ let parentRoot = currentState.previousRoot;
2094
+ while (parentRoot) {
2095
+ const parentState = parentRoot.getState();
2096
+ if (parentState?.internal) parentState.internal.priority--;
2097
+ parentRoot = parentState?.previousRoot;
2098
+ }
2099
+ }
2100
+ }
2101
+ };
1399
2102
  } else {
1400
- const constraintType = before ? "before" : "after";
1401
- console.warn(`[useFrame] Phase "${before ?? after}" not found for '${constraintType}' constraint`);
1402
- }
1403
- this.phases.splice(insertIndex, 0, node);
1404
- this.phaseMap.set(name, node);
1405
- this.invalidateCache();
1406
- }
1407
- /**
1408
- * Get ordered list of phase names
1409
- */
1410
- getOrderedPhases() {
1411
- if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
1412
- return this.orderedNamesCache;
1413
- }
1414
- /**
1415
- * Check if a phase exists
1416
- */
1417
- hasPhase(name) {
1418
- return this.phaseMap.has(name);
1419
- }
1420
- /**
1421
- * Get the index of a phase (-1 if not found)
1422
- */
1423
- getPhaseIndex(name) {
1424
- if (!name) return -1;
1425
- return this.phases.findIndex((p) => p.name === name);
1426
- }
1427
- /**
1428
- * Ensure a phase exists, creating an auto-generated one if needed.
1429
- * Used for resolving before/after constraints.
1430
- *
1431
- * @param name - The phase name to ensure exists
1432
- * @returns The phase name (may be auto-generated like 'before:render')
1433
- */
1434
- ensurePhase(name) {
1435
- if (this.phaseMap.has(name)) return name;
1436
- const node = { name, isAutoGenerated: true };
1437
- this.phases.push(node);
1438
- this.phaseMap.set(name, node);
1439
- this.invalidateCache();
1440
- return name;
1441
- }
1442
- /**
1443
- * Resolve where a job with before/after constraints should go.
1444
- * Creates auto-generated phases if needed.
1445
- *
1446
- * @param before - Phase(s) to run before
1447
- * @param after - Phase(s) to run after
1448
- * @returns The resolved phase name
1449
- */
1450
- resolveConstraintPhase(before, after) {
1451
- const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
1452
- const afterArr = after ? Array.isArray(after) ? after : [after] : [];
1453
- if (beforeArr.length > 0) {
1454
- return this.ensureAutoPhase(beforeArr[0], "before", 0);
1455
- }
1456
- if (afterArr.length > 0) {
1457
- return this.ensureAutoPhase(afterArr[0], "after", 1);
2103
+ return scheduler$1.register(
2104
+ (state, delta) => {
2105
+ const frameState = state;
2106
+ if (!frameState.renderer) return;
2107
+ callbackRef.current?.(frameState, delta);
2108
+ },
2109
+ { id, ...options }
2110
+ );
1458
2111
  }
1459
- return "update";
1460
- }
1461
- /**
1462
- * Ensure an auto-generated phase exists relative to a target phase.
1463
- * Creates the phase if it doesn't exist, inserting it at the correct position.
1464
- *
1465
- * @param target - The target phase name to position relative to
1466
- * @param prefix - Prefix for auto-generated phase name ('before' or 'after')
1467
- * @param offset - Insertion offset (0 for before, 1 for after)
1468
- * @returns The auto-generated phase name
1469
- */
1470
- ensureAutoPhase(target, prefix, offset) {
1471
- const autoName = `${prefix}:${target}`;
1472
- if (this.phaseMap.has(autoName)) return autoName;
1473
- const node = { name: autoName, isAutoGenerated: true };
1474
- const targetIndex = this.getPhaseIndex(target);
1475
- if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
1476
- else this.phases.push(node);
1477
- this.phaseMap.set(autoName, node);
1478
- this.invalidateCache();
1479
- return autoName;
1480
- }
1481
- // Internal --------------------------------
1482
- invalidateCache() {
1483
- this.orderedNamesCache = null;
1484
- }
1485
- }
1486
-
1487
- function rebuildSortedJobs(jobs, phaseGraph) {
1488
- const orderedPhases = phaseGraph.getOrderedPhases();
1489
- const buckets = /* @__PURE__ */ new Map();
1490
- for (const phase of orderedPhases) {
1491
- buckets.set(phase, []);
1492
- }
1493
- for (const job of jobs.values()) {
1494
- if (!job.enabled) continue;
1495
- let bucket = buckets.get(job.phase);
1496
- if (!bucket) {
1497
- bucket = [];
1498
- buckets.set(job.phase, bucket);
1499
- }
1500
- bucket.push(job);
1501
- }
1502
- const sortedBuckets = [];
1503
- for (const phase of orderedPhases) {
1504
- const bucket = buckets.get(phase);
1505
- if (!bucket || bucket.length === 0) continue;
1506
- bucket.sort((a, b) => {
1507
- if (a.priority !== b.priority) return b.priority - a.priority;
1508
- return a.index - b.index;
1509
- });
1510
- sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
1511
- }
1512
- for (const [phase, bucket] of buckets) {
1513
- if (!orderedPhases.includes(phase) && bucket.length > 0) {
1514
- bucket.sort((a, b) => {
1515
- if (a.priority !== b.priority) return b.priority - a.priority;
1516
- return a.index - b.index;
1517
- });
1518
- sortedBuckets.push(bucket);
1519
- }
1520
- }
1521
- return sortedBuckets.flat();
1522
- }
1523
- function hasCrossJobConstraints(bucket) {
1524
- const jobIds = new Set(bucket.map((j) => j.id));
1525
- for (const job of bucket) {
1526
- for (const ref of job.before) {
1527
- if (jobIds.has(ref)) return true;
1528
- }
1529
- for (const ref of job.after) {
1530
- if (jobIds.has(ref)) return true;
1531
- }
1532
- }
1533
- return false;
1534
- }
1535
- function topologicalSort(jobs) {
1536
- const n = jobs.length;
1537
- if (n <= 1) return jobs;
1538
- const jobMap = /* @__PURE__ */ new Map();
1539
- const inDegree = /* @__PURE__ */ new Map();
1540
- const adjacency = /* @__PURE__ */ new Map();
1541
- for (const job of jobs) {
1542
- jobMap.set(job.id, job);
1543
- inDegree.set(job.id, 0);
1544
- adjacency.set(job.id, []);
1545
- }
1546
- for (const job of jobs) {
1547
- for (const ref of job.before) {
1548
- if (jobMap.has(ref)) {
1549
- adjacency.get(job.id).push(ref);
1550
- inDegree.set(ref, inDegree.get(ref) + 1);
1551
- }
1552
- }
1553
- for (const ref of job.after) {
1554
- if (jobMap.has(ref)) {
1555
- adjacency.get(ref).push(job.id);
1556
- inDegree.set(job.id, inDegree.get(job.id) + 1);
1557
- }
1558
- }
1559
- }
1560
- const queue = [];
1561
- for (const job of jobs) {
1562
- if (inDegree.get(job.id) === 0) {
1563
- queue.push(job);
1564
- }
1565
- }
1566
- queue.sort((a, b) => {
1567
- if (a.priority !== b.priority) return b.priority - a.priority;
1568
- return a.index - b.index;
1569
- });
1570
- const result = [];
1571
- while (queue.length > 0) {
1572
- const job = queue.shift();
1573
- result.push(job);
1574
- const neighbors = adjacency.get(job.id) || [];
1575
- for (const neighborId of neighbors) {
1576
- const newDegree = inDegree.get(neighborId) - 1;
1577
- inDegree.set(neighborId, newDegree);
1578
- if (newDegree === 0) {
1579
- const neighbor = jobMap.get(neighborId);
1580
- insertSorted(queue, neighbor);
1581
- }
1582
- }
1583
- }
1584
- if (result.length !== n) {
1585
- console.warn("[useFrame] Circular dependency detected in job constraints");
1586
- const resultIds = new Set(result.map((j) => j.id));
1587
- for (const job of jobs) {
1588
- if (!resultIds.has(job.id)) result.push(job);
1589
- }
1590
- }
1591
- return result;
1592
- }
1593
- function insertSorted(arr, job) {
1594
- let i = 0;
1595
- while (i < arr.length) {
1596
- const cmp = arr[i];
1597
- if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
1598
- break;
1599
- }
1600
- i++;
1601
- }
1602
- arr.splice(i, 0, job);
1603
- }
1604
-
1605
- function shouldRun(job, now) {
1606
- if (!job.enabled) return false;
1607
- if (!job.fps) return true;
1608
- const minInterval = 1e3 / job.fps;
1609
- const lastRun = job.lastRun ?? 0;
1610
- const elapsed = now - lastRun;
1611
- if (elapsed < minInterval) return false;
1612
- if (job.drop) {
1613
- job.lastRun = now;
1614
- } else {
1615
- const steps = Math.floor(elapsed / minInterval);
1616
- job.lastRun = lastRun + steps * minInterval;
1617
- if (job.lastRun < now - minInterval) {
1618
- job.lastRun = now;
1619
- }
1620
- }
1621
- return true;
1622
- }
1623
- function resetJobTiming(job) {
1624
- job.lastRun = void 0;
1625
- }
1626
-
1627
- var __defProp$1 = Object.defineProperty;
1628
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1629
- var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1630
- const hmrData = (() => {
1631
- if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
1632
- if (typeof import_meta_hot !== "undefined") return import_meta_hot;
1633
- try {
1634
- return (0, eval)("import.meta.hot");
1635
- } catch {
1636
- return void 0;
1637
- }
1638
- })();
1639
- const _Scheduler = class _Scheduler {
1640
- //* Constructor ================================
1641
- constructor() {
1642
- //* Critical State ================================
1643
- __publicField$1(this, "roots", /* @__PURE__ */ new Map());
1644
- __publicField$1(this, "phaseGraph");
1645
- __publicField$1(this, "loopState", {
1646
- running: false,
1647
- rafHandle: null,
1648
- lastTime: null,
1649
- // null = uninitialized, 0+ = valid timestamp
1650
- frameCount: 0,
1651
- elapsedTime: 0,
1652
- createdAt: performance.now()
1653
- });
1654
- __publicField$1(this, "stoppedTime", 0);
1655
- //* Private State ================================
1656
- __publicField$1(this, "nextRootIndex", 0);
1657
- __publicField$1(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
1658
- __publicField$1(this, "globalAfterJobs", /* @__PURE__ */ new Map());
1659
- __publicField$1(this, "nextGlobalIndex", 0);
1660
- __publicField$1(this, "idleCallbacks", /* @__PURE__ */ new Set());
1661
- __publicField$1(this, "nextJobIndex", 0);
1662
- __publicField$1(this, "jobStateListeners", /* @__PURE__ */ new Map());
1663
- __publicField$1(this, "pendingFrames", 0);
1664
- __publicField$1(this, "_frameloop", "always");
1665
- //* Independent Mode & Error Handling State ================================
1666
- __publicField$1(this, "_independent", false);
1667
- __publicField$1(this, "errorHandler", null);
1668
- __publicField$1(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1669
- //* Core Loop Execution Methods ================================
1670
- /**
1671
- * Main RAF loop callback.
1672
- * Executes frame, handles demand mode, and schedules next frame.
1673
- * @param {number} timestamp - RAF timestamp in milliseconds
1674
- * @returns {void}
1675
- * @private
1676
- */
1677
- __publicField$1(this, "loop", (timestamp) => {
1678
- if (!this.loopState.running) return;
1679
- this.executeFrame(timestamp);
1680
- if (this._frameloop === "demand") {
1681
- this.pendingFrames = Math.max(0, this.pendingFrames - 1);
1682
- if (this.pendingFrames === 0) {
1683
- this.notifyIdle(timestamp);
1684
- return this.stop();
1685
- }
1686
- }
1687
- this.loopState.rafHandle = requestAnimationFrame(this.loop);
1688
- });
1689
- this.phaseGraph = new PhaseGraph();
1690
- }
1691
- static get instance() {
1692
- return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1693
- }
1694
- static set instance(value) {
1695
- globalThis[_Scheduler.INSTANCE_KEY] = value;
1696
- }
1697
- /**
1698
- * Get the global scheduler instance (creates if doesn't exist).
1699
- * Uses HMR data to preserve instance across hot reloads.
1700
- * @returns {Scheduler} The singleton scheduler instance
1701
- */
1702
- static get() {
1703
- if (!_Scheduler.instance && hmrData?.data?.scheduler) {
1704
- _Scheduler.instance = hmrData.data.scheduler;
1705
- }
1706
- if (!_Scheduler.instance) {
1707
- _Scheduler.instance = new _Scheduler();
1708
- if (hmrData?.data) {
1709
- hmrData.data.scheduler = _Scheduler.instance;
1710
- }
1711
- }
1712
- return _Scheduler.instance;
1713
- }
1714
- /**
1715
- * Reset the singleton instance. Stops the loop and clears all state.
1716
- * Primarily used for testing to ensure clean state between tests.
1717
- * @returns {void}
1718
- */
1719
- static reset() {
1720
- if (_Scheduler.instance) {
1721
- _Scheduler.instance.stop();
1722
- _Scheduler.instance = null;
1723
- }
1724
- if (hmrData?.data) {
1725
- hmrData.data.scheduler = null;
1726
- }
1727
- }
1728
- //* Getters & Setters ================================
1729
- get phases() {
1730
- return this.phaseGraph.getOrderedPhases();
1731
- }
1732
- get frameloop() {
1733
- return this._frameloop;
1734
- }
1735
- set frameloop(mode) {
1736
- if (this._frameloop === mode) return;
1737
- const wasAlways = this._frameloop === "always";
1738
- this._frameloop = mode;
1739
- if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
1740
- else if (mode !== "always" && wasAlways) this.stop();
1741
- }
1742
- get isRunning() {
1743
- return this.loopState.running;
1744
- }
1745
- get isReady() {
1746
- return this.roots.size > 0;
1747
- }
1748
- get independent() {
1749
- return this._independent;
1750
- }
1751
- set independent(value) {
1752
- this._independent = value;
1753
- if (value) this.ensureDefaultRoot();
1754
- }
1755
- //* Root Management Methods ================================
1756
- /**
1757
- * Register a root (Canvas) with the scheduler.
1758
- * The first root to register starts the RAF loop (if frameloop='always').
1759
- * @param {string} id - Unique identifier for this root
1760
- * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
1761
- * @returns {() => void} Unsubscribe function to remove this root
1762
- */
1763
- registerRoot(id, options = {}) {
1764
- if (this.roots.has(id)) {
1765
- console.warn(`[Scheduler] Root "${id}" already registered`);
1766
- return () => this.unregisterRoot(id);
1767
- }
1768
- const entry = {
1769
- id,
1770
- getState: options.getState ?? (() => ({})),
1771
- jobs: /* @__PURE__ */ new Map(),
1772
- sortedJobs: [],
1773
- needsRebuild: false
1774
- };
1775
- if (options.onError) {
1776
- this.errorHandler = options.onError;
1777
- }
1778
- this.roots.set(id, entry);
1779
- if (this.roots.size === 1) {
1780
- this.notifyRootReady();
1781
- if (this._frameloop === "always") this.start();
1782
- }
1783
- return () => this.unregisterRoot(id);
1784
- }
1785
- /**
1786
- * Unregister a root from the scheduler.
1787
- * Cleans up all job state listeners for this root's jobs.
1788
- * The last root to unregister stops the RAF loop.
1789
- * @param {string} id - The root ID to unregister
1790
- * @returns {void}
1791
- */
1792
- unregisterRoot(id) {
1793
- const root = this.roots.get(id);
1794
- if (!root) return;
1795
- for (const jobId of root.jobs.keys()) {
1796
- this.jobStateListeners.delete(jobId);
1797
- }
1798
- this.roots.delete(id);
1799
- if (this.roots.size === 0) {
1800
- this.stop();
1801
- this.errorHandler = null;
1802
- }
1803
- }
1804
- /**
1805
- * Subscribe to be notified when a root becomes available.
1806
- * Fires immediately if a root already exists.
1807
- * @param {() => void} callback - Function called when first root registers
1808
- * @returns {() => void} Unsubscribe function
1809
- */
1810
- onRootReady(callback) {
1811
- if (this.roots.size > 0) {
1812
- callback();
1813
- return () => {
1814
- };
1815
- }
1816
- this.rootReadyCallbacks.add(callback);
1817
- return () => this.rootReadyCallbacks.delete(callback);
1818
- }
1819
- /**
1820
- * Notify all registered root-ready callbacks.
1821
- * Called when the first root registers.
1822
- * @returns {void}
1823
- * @private
1824
- */
1825
- notifyRootReady() {
1826
- for (const cb of this.rootReadyCallbacks) {
1827
- try {
1828
- cb();
1829
- } catch (error) {
1830
- console.error("[Scheduler] Error in root-ready callback:", error);
1831
- }
1832
- }
1833
- this.rootReadyCallbacks.clear();
1834
- }
1835
- /**
1836
- * Ensure a default root exists for independent mode.
1837
- * Creates a minimal root with no state provider.
1838
- * @returns {void}
1839
- * @private
1840
- */
1841
- ensureDefaultRoot() {
1842
- if (!this.roots.has("__default__")) {
1843
- this.registerRoot("__default__");
1844
- }
1845
- }
1846
- /**
1847
- * Trigger error handling for job errors.
1848
- * Uses the bound error handler if available, otherwise logs to console.
1849
- * @param {Error} error - The error to handle
1850
- * @returns {void}
1851
- */
1852
- triggerError(error) {
1853
- if (this.errorHandler) this.errorHandler(error);
1854
- else console.error("[Scheduler]", error);
1855
- }
1856
- //* Phase Management Methods ================================
1857
- /**
1858
- * Add a named phase to the scheduler's execution order.
1859
- * Marks all roots for rebuild to incorporate the new phase.
1860
- * @param {string} name - The phase name (e.g., 'physics', 'postprocess')
1861
- * @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
1862
- * @returns {void}
1863
- * @example
1864
- * scheduler.addPhase('physics', { before: 'update' });
1865
- * scheduler.addPhase('postprocess', { after: 'render' });
1866
- */
1867
- addPhase(name, options) {
1868
- this.phaseGraph.addPhase(name, options);
1869
- for (const root of this.roots.values()) {
1870
- root.needsRebuild = true;
1871
- }
1872
- }
1873
- /**
1874
- * Check if a phase exists in the scheduler.
1875
- * @param {string} name - The phase name to check
1876
- * @returns {boolean} True if the phase exists
1877
- */
1878
- hasPhase(name) {
1879
- return this.phaseGraph.hasPhase(name);
1880
- }
1881
- //* Global Job Registration Methods (Deprecated APIs) ================================
1882
- /**
1883
- * Register a global job that runs once per frame (not per-root).
1884
- * Used internally by deprecated addEffect/addAfterEffect APIs.
1885
- * @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
1886
- * @param {string} id - Unique identifier for this global job
1887
- * @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
1888
- * @returns {() => void} Unsubscribe function to remove this global job
1889
- * @deprecated Use useFrame with phases instead
1890
- */
1891
- registerGlobal(phase, id, callback) {
1892
- const job = { id, callback };
1893
- if (phase === "before") {
1894
- this.globalBeforeJobs.set(id, job);
1895
- } else {
1896
- this.globalAfterJobs.set(id, job);
1897
- }
1898
- return () => {
1899
- if (phase === "before") this.globalBeforeJobs.delete(id);
1900
- else this.globalAfterJobs.delete(id);
1901
- };
1902
- }
1903
- //* Idle Callback Methods (Deprecated API) ================================
1904
- /**
1905
- * Register an idle callback that fires when the loop stops.
1906
- * Used internally by deprecated addTail API.
1907
- * @param {(timestamp: number) => void} callback - Function called when loop becomes idle
1908
- * @returns {() => void} Unsubscribe function to remove this idle callback
1909
- * @deprecated Use demand mode with invalidate() instead
1910
- */
1911
- onIdle(callback) {
1912
- this.idleCallbacks.add(callback);
1913
- return () => this.idleCallbacks.delete(callback);
1914
- }
1915
- /**
1916
- * Notify all registered idle callbacks.
1917
- * Called when the loop stops in demand mode.
1918
- * @param {number} timestamp - The RAF timestamp when idle occurred
1919
- * @returns {void}
1920
- * @private
1921
- */
1922
- notifyIdle(timestamp) {
1923
- for (const cb of this.idleCallbacks) {
1924
- try {
1925
- cb(timestamp);
1926
- } catch (error) {
1927
- console.error("[Scheduler] Error in idle callback:", error);
1928
- }
1929
- }
1930
- }
1931
- //* Job Registration & Management Methods ================================
1932
- /**
1933
- * Register a job (frame callback) with a specific root.
1934
- * This is the core registration method used by useFrame internally.
1935
- * @param {FrameNextCallback} callback - The function to call each frame
1936
- * @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
1937
- * @param {string} [options.rootId] - Target root ID (defaults to first registered root)
1938
- * @param {string} [options.id] - Unique job ID (auto-generated if not provided)
1939
- * @param {string} [options.phase] - Execution phase (defaults to 'update')
1940
- * @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
1941
- * @param {number} [options.fps] - FPS throttle limit
1942
- * @param {boolean} [options.drop] - Drop frames when behind (default true)
1943
- * @param {boolean} [options.enabled] - Whether job is active (default true)
1944
- * @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
1945
- * @returns {() => void} Unsubscribe function to remove this job
1946
- */
1947
- register(callback, options = {}) {
1948
- const rootId = options.rootId;
1949
- const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
1950
- if (!root) {
1951
- console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
1952
- return () => {
1953
- };
1954
- }
1955
- const id = options.id ?? this.generateJobId();
1956
- let phase = options.phase ?? "update";
1957
- if (!options.phase && (options.before || options.after)) {
1958
- phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
1959
- }
1960
- const before = this.normalizeConstraints(options.before);
1961
- const after = this.normalizeConstraints(options.after);
1962
- const job = {
1963
- id,
1964
- callback,
1965
- phase,
1966
- before,
1967
- after,
1968
- priority: options.priority ?? 0,
1969
- index: this.nextJobIndex++,
1970
- fps: options.fps,
1971
- drop: options.drop ?? true,
1972
- enabled: options.enabled ?? true,
1973
- system: options.system ?? false
1974
- };
1975
- if (root.jobs.has(id)) {
1976
- console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
1977
- }
1978
- root.jobs.set(id, job);
1979
- root.needsRebuild = true;
1980
- return () => this.unregister(id, root.id);
1981
- }
1982
- /**
1983
- * Unregister a job by its ID.
1984
- * Searches all roots if rootId is not provided.
1985
- * @param {string} id - The job ID to unregister
1986
- * @param {string} [rootId] - Optional root ID to search (searches all if not provided)
1987
- * @returns {void}
1988
- */
1989
- unregister(id, rootId) {
1990
- const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
1991
- if (root?.jobs.delete(id)) {
1992
- root.needsRebuild = true;
1993
- this.jobStateListeners.delete(id);
1994
- }
1995
- }
1996
- /**
1997
- * Update a job's options dynamically.
1998
- * Searches all roots to find the job by ID.
1999
- * Phase/constraint changes trigger a rebuild of the sorted job list.
2000
- * @param {string} id - The job ID to update
2001
- * @param {Partial<JobOptions>} options - The options to update
2002
- * @returns {void}
2003
- */
2004
- updateJob(id, options) {
2005
- let job;
2006
- let root;
2007
- for (const r of this.roots.values()) {
2008
- job = r.jobs.get(id);
2009
- if (job) {
2010
- root = r;
2011
- break;
2012
- }
2013
- }
2014
- if (!job || !root) return;
2015
- if (options.priority !== void 0) job.priority = options.priority;
2016
- if (options.fps !== void 0) job.fps = options.fps;
2017
- if (options.drop !== void 0) job.drop = options.drop;
2018
- if (options.enabled !== void 0) {
2019
- const wasEnabled = job.enabled;
2020
- job.enabled = options.enabled;
2021
- if (!wasEnabled && job.enabled) resetJobTiming(job);
2022
- if (wasEnabled !== job.enabled) root.needsRebuild = true;
2023
- }
2024
- if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
2025
- if (options.phase) job.phase = options.phase;
2026
- if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
2027
- if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
2028
- root.needsRebuild = true;
2029
- }
2030
- }
2031
- //* Job State Management Methods ================================
2032
- /**
2033
- * Check if a job is currently paused (disabled).
2034
- * @param {string} id - The job ID to check
2035
- * @returns {boolean} True if the job exists and is paused
2036
- */
2037
- isJobPaused(id) {
2038
- for (const root of this.roots.values()) {
2039
- const job = root.jobs.get(id);
2040
- if (job) return !job.enabled;
2041
- }
2042
- return false;
2043
- }
2044
- /**
2045
- * Subscribe to state changes for a specific job.
2046
- * Listener is called when job is paused or resumed.
2047
- * @param {string} id - The job ID to subscribe to
2048
- * @param {() => void} listener - Callback invoked on state changes
2049
- * @returns {() => void} Unsubscribe function
2050
- */
2051
- subscribeJobState(id, listener) {
2052
- if (!this.jobStateListeners.has(id)) {
2053
- this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
2054
- }
2055
- this.jobStateListeners.get(id).add(listener);
2056
- return () => {
2057
- this.jobStateListeners.get(id)?.delete(listener);
2058
- if (this.jobStateListeners.get(id)?.size === 0) {
2059
- this.jobStateListeners.delete(id);
2060
- }
2061
- };
2062
- }
2063
- /**
2064
- * Notify all listeners that a job's state has changed.
2065
- * @param {string} id - The job ID that changed
2066
- * @returns {void}
2067
- * @private
2068
- */
2069
- notifyJobStateChange(id) {
2070
- this.jobStateListeners.get(id)?.forEach((listener) => listener());
2071
- }
2072
- /**
2073
- * Pause a job by ID (sets enabled=false).
2074
- * Notifies any subscribed state listeners.
2075
- * @param {string} id - The job ID to pause
2076
- * @returns {void}
2077
- */
2078
- pauseJob(id) {
2079
- this.updateJob(id, { enabled: false });
2080
- this.notifyJobStateChange(id);
2081
- }
2082
- /**
2083
- * Resume a paused job by ID (sets enabled=true).
2084
- * Resets job timing to prevent frame accumulation.
2085
- * Notifies any subscribed state listeners.
2086
- * @param {string} id - The job ID to resume
2087
- * @returns {void}
2088
- */
2089
- resumeJob(id) {
2090
- this.updateJob(id, { enabled: true });
2091
- this.notifyJobStateChange(id);
2092
- }
2093
- //* Frame Loop Control Methods ================================
2094
- /**
2095
- * Start the requestAnimationFrame loop.
2096
- * Resets timing state (elapsedTime, frameCount) on start.
2097
- * No-op if already running.
2098
- * @returns {void}
2099
- */
2100
- start() {
2101
- if (this.loopState.running) return;
2102
- const { elapsedTime, createdAt } = this.loopState;
2103
- let adjustedCreated = 0;
2104
- if (this.stoppedTime > 0) {
2105
- adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
2106
- this.stoppedTime = 0;
2107
- }
2108
- Object.assign(this.loopState, {
2109
- running: true,
2110
- elapsedTime: elapsedTime ?? 0,
2111
- lastTime: performance.now(),
2112
- createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
2113
- frameCount: 0,
2114
- rafHandle: requestAnimationFrame(this.loop)
2115
- });
2116
- }
2117
- /**
2118
- * Stop the requestAnimationFrame loop.
2119
- * Cancels any pending RAF callback.
2120
- * No-op if not running.
2121
- * @returns {void}
2122
- */
2123
- stop() {
2124
- if (!this.loopState.running) return;
2125
- this.loopState.running = false;
2126
- if (this.loopState.rafHandle !== null) {
2127
- cancelAnimationFrame(this.loopState.rafHandle);
2128
- this.loopState.rafHandle = null;
2129
- }
2130
- this.stoppedTime = performance.now();
2131
- }
2132
- /**
2133
- * Request frames to be rendered in demand mode.
2134
- * Accumulates pending frames (capped at 60) and starts the loop if not running.
2135
- * No-op if frameloop is not 'demand'.
2136
- * @param {number} [frames=1] - Number of frames to request
2137
- * @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
2138
- * - `false` (default): Sets pending frames to the specified value (replaces existing count)
2139
- * - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
2140
- * @returns {void}
2141
- * @example
2142
- * // Request a single frame render
2143
- * scheduler.invalidate();
2144
- *
2145
- * @example
2146
- * // Request 5 frames (e.g., for animations)
2147
- * scheduler.invalidate(5);
2148
- *
2149
- * @example
2150
- * // Set pending frames to exactly 3 (don't stack with existing)
2151
- * scheduler.invalidate(3, false);
2152
- *
2153
- * @example
2154
- * // Add 2 more frames to existing pending count
2155
- * scheduler.invalidate(2, true);
2156
- */
2157
- invalidate(frames = 1, stackFrames = false) {
2158
- if (this._frameloop !== "demand") return;
2159
- const baseFrames = stackFrames ? this.pendingFrames : 0;
2160
- this.pendingFrames = Math.min(60, baseFrames + frames);
2161
- if (!this.loopState.running && this.pendingFrames > 0) this.start();
2162
- }
2163
- /**
2164
- * Reset timing state for deterministic testing.
2165
- * Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
2166
- * @returns {void}
2167
- */
2168
- resetTiming() {
2169
- this.loopState.lastTime = null;
2170
- this.loopState.frameCount = 0;
2171
- this.loopState.elapsedTime = 0;
2172
- this.loopState.createdAt = performance.now();
2173
- }
2174
- //* Manual Stepping Methods ================================
2175
- /**
2176
- * Manually execute a single frame for all roots.
2177
- * Useful for frameloop='never' mode or testing scenarios.
2178
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2179
- * @returns {void}
2180
- * @example
2181
- * // Manual control mode
2182
- * scheduler.frameloop = 'never';
2183
- * scheduler.step(); // Execute one frame
2184
- */
2185
- step(timestamp) {
2186
- const now = timestamp ?? performance.now();
2187
- this.executeFrame(now);
2188
- }
2189
- /**
2190
- * Manually execute a single job by its ID.
2191
- * Useful for testing individual job callbacks in isolation.
2192
- * @param {string} id - The job ID to step
2193
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2194
- * @returns {void}
2195
- */
2196
- stepJob(id, timestamp) {
2197
- let job;
2198
- let root;
2199
- for (const r of this.roots.values()) {
2200
- job = r.jobs.get(id);
2201
- if (job) {
2202
- root = r;
2203
- break;
2204
- }
2205
- }
2206
- if (!job || !root) {
2207
- console.warn(`[Scheduler] Job "${id}" not found`);
2208
- return;
2209
- }
2210
- const now = timestamp ?? performance.now();
2211
- const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
2212
- const delta = deltaMs / 1e3;
2213
- const elapsed = now - this.loopState.createdAt;
2214
- const providedState = root.getState?.() ?? {};
2215
- const frameState = {
2216
- ...providedState,
2217
- time: now,
2218
- delta,
2219
- elapsed,
2220
- frame: this.loopState.frameCount
2221
- };
2222
- try {
2223
- job.callback(frameState, delta);
2224
- } catch (error) {
2225
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2226
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2227
- }
2228
- }
2229
- /**
2230
- * Execute a single frame across all roots.
2231
- * Order: globalBefore → each root's jobs → globalAfter
2232
- * @param {number} timestamp - RAF timestamp in milliseconds
2233
- * @returns {void}
2234
- * @private
2235
- */
2236
- executeFrame(timestamp) {
2237
- const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
2238
- const delta = deltaMs / 1e3;
2239
- this.loopState.lastTime = timestamp;
2240
- this.loopState.frameCount++;
2241
- this.loopState.elapsedTime += deltaMs;
2242
- this.runGlobalJobs(this.globalBeforeJobs, timestamp);
2243
- for (const root of this.roots.values()) {
2244
- this.tickRoot(root, timestamp, delta);
2245
- }
2246
- this.runGlobalJobs(this.globalAfterJobs, timestamp);
2247
- }
2248
- /**
2249
- * Run all global jobs from a job map.
2250
- * Catches and logs errors without stopping execution.
2251
- * @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
2252
- * @param {number} timestamp - RAF timestamp in milliseconds
2253
- * @returns {void}
2254
- * @private
2255
- */
2256
- runGlobalJobs(jobs, timestamp) {
2257
- for (const job of jobs.values()) {
2258
- try {
2259
- job.callback(timestamp);
2260
- } catch (error) {
2261
- console.error(`[Scheduler] Error in global job "${job.id}":`, error);
2262
- }
2263
- }
2264
- }
2265
- /**
2266
- * Execute all jobs for a single root in sorted order.
2267
- * Rebuilds sorted job list if needed, then dispatches each job.
2268
- * Errors are caught and propagated via triggerError.
2269
- * @param {RootEntry} root - The root entry to tick
2270
- * @param {number} timestamp - RAF timestamp in milliseconds
2271
- * @param {number} delta - Time since last frame in seconds
2272
- * @returns {void}
2273
- * @private
2274
- */
2275
- tickRoot(root, timestamp, delta) {
2276
- if (root.needsRebuild) {
2277
- root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
2278
- root.needsRebuild = false;
2279
- }
2280
- const providedState = root.getState?.() ?? {};
2281
- const frameState = {
2282
- ...providedState,
2283
- time: timestamp,
2284
- delta,
2285
- elapsed: this.loopState.elapsedTime / 1e3,
2286
- // Convert ms to seconds
2287
- frame: this.loopState.frameCount
2288
- };
2289
- for (const job of root.sortedJobs) {
2290
- if (!shouldRun(job, timestamp)) continue;
2291
- try {
2292
- job.callback(frameState, delta);
2293
- } catch (error) {
2294
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2295
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2296
- }
2297
- }
2298
- }
2299
- //* Debug & Inspection Methods ================================
2300
- /**
2301
- * Get the total number of registered jobs across all roots.
2302
- * Includes both per-root jobs and global before/after jobs.
2303
- * @returns {number} Total job count
2304
- */
2305
- getJobCount() {
2306
- let count = 0;
2307
- for (const root of this.roots.values()) {
2308
- count += root.jobs.size;
2309
- }
2310
- return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
2311
- }
2312
- /**
2313
- * Get all registered job IDs across all roots.
2314
- * Includes both per-root jobs and global before/after jobs.
2315
- * @returns {string[]} Array of all job IDs
2316
- */
2317
- getJobIds() {
2318
- const ids = [];
2319
- for (const root of this.roots.values()) {
2320
- ids.push(...root.jobs.keys());
2321
- }
2322
- ids.push(...this.globalBeforeJobs.keys());
2323
- ids.push(...this.globalAfterJobs.keys());
2324
- return ids;
2325
- }
2326
- /**
2327
- * Get the number of registered roots (Canvas instances).
2328
- * @returns {number} Number of registered roots
2329
- */
2330
- getRootCount() {
2331
- return this.roots.size;
2332
- }
2333
- /**
2334
- * Check if any user (non-system) jobs are registered in a specific phase.
2335
- * Used by the default render job to know if a user has taken over rendering.
2336
- *
2337
- * @param phase The phase to check
2338
- * @param rootId Optional root ID to check (checks all roots if not provided)
2339
- * @returns true if any user jobs exist in the phase
2340
- */
2341
- hasUserJobsInPhase(phase, rootId) {
2342
- const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
2343
- return rootsToCheck.some((root) => {
2344
- if (!root) return false;
2345
- for (const job of root.jobs.values()) {
2346
- if (job.phase === phase && !job.system && job.enabled) return true;
2347
- }
2348
- return false;
2349
- });
2350
- }
2351
- //* Utility Methods ================================
2352
- /**
2353
- * Generate a unique root ID for automatic root registration.
2354
- * @returns {string} A unique root ID in the format 'root_N'
2355
- */
2356
- generateRootId() {
2357
- return `root_${this.nextRootIndex++}`;
2358
- }
2359
- /**
2360
- * Generate a unique job ID.
2361
- * @returns {string} A unique job ID in the format 'job_N'
2362
- * @private
2363
- */
2364
- generateJobId() {
2365
- return `job_${this.nextJobIndex}`;
2366
- }
2367
- /**
2368
- * Normalize before/after constraints to a Set.
2369
- * Handles undefined, single string, or array inputs.
2370
- * @param {string | string[] | undefined} value - The constraint value(s)
2371
- * @returns {Set<string>} Normalized Set of constraint strings
2372
- * @private
2373
- */
2374
- normalizeConstraints(value) {
2375
- if (!value) return /* @__PURE__ */ new Set();
2376
- if (Array.isArray(value)) return new Set(value);
2377
- return /* @__PURE__ */ new Set([value]);
2378
- }
2379
- };
2380
- //* Static State & Methods (Singleton Usage) ================================
2381
- //* Cross-Bundle Singleton Key ==============================
2382
- // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2383
- // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2384
- __publicField$1(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
2385
- let Scheduler = _Scheduler;
2386
- const getScheduler = () => Scheduler.get();
2387
- if (hmrData) {
2388
- hmrData.accept?.();
2389
- }
2390
-
2391
- function useFrame(callback, priorityOrOptions) {
2392
- const store = React__namespace.useContext(context);
2393
- const isInsideCanvas = store !== null;
2394
- const scheduler = getScheduler();
2395
- const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
2396
- id: priorityOrOptions.id,
2397
- phase: priorityOrOptions.phase,
2398
- priority: priorityOrOptions.priority,
2399
- fps: priorityOrOptions.fps,
2400
- drop: priorityOrOptions.drop,
2401
- enabled: priorityOrOptions.enabled,
2402
- before: priorityOrOptions.before,
2403
- after: priorityOrOptions.after
2404
- }) : "";
2405
- const options = React__namespace.useMemo(() => {
2406
- return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
2407
- }, [optionsKey]);
2408
- const reactId = React__namespace.useId();
2409
- const id = options.id ?? reactId;
2410
- const callbackRef = useMutableCallback(callback);
2411
- const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
2412
- useIsomorphicLayoutEffect(() => {
2413
- if (!callback) return;
2414
- if (isInsideCanvas) {
2415
- const state = store.getState();
2416
- const rootId = state.internal.rootId;
2417
- if (isLegacyPriority) {
2418
- state.internal.priority++;
2419
- let parentRoot = state.previousRoot;
2420
- while (parentRoot) {
2421
- const parentState = parentRoot.getState();
2422
- if (parentState?.internal) parentState.internal.priority++;
2423
- parentRoot = parentState?.previousRoot;
2424
- }
2425
- notifyDepreciated({
2426
- heading: "useFrame with numeric priority is deprecated",
2427
- 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 })',
2428
- link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2429
- });
2430
- }
2431
- const wrappedCallback = (frameState, delta) => {
2432
- const localState = store.getState();
2433
- const mergedState = {
2434
- ...localState,
2435
- time: frameState.time,
2436
- delta: frameState.delta,
2437
- elapsed: frameState.elapsed,
2438
- frame: frameState.frame
2439
- };
2440
- callbackRef.current?.(mergedState, delta);
2441
- };
2442
- const unregister = scheduler.register(wrappedCallback, {
2443
- id,
2444
- rootId,
2445
- ...options
2446
- });
2447
- return () => {
2448
- unregister();
2449
- if (isLegacyPriority) {
2450
- const currentState = store.getState();
2451
- if (currentState.internal) {
2452
- currentState.internal.priority--;
2453
- let parentRoot = currentState.previousRoot;
2454
- while (parentRoot) {
2455
- const parentState = parentRoot.getState();
2456
- if (parentState?.internal) parentState.internal.priority--;
2457
- parentRoot = parentState?.previousRoot;
2458
- }
2459
- }
2460
- }
2461
- };
2462
- } else {
2463
- const registerOutside = () => {
2464
- return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
2465
- };
2466
- if (scheduler.independent || scheduler.isReady) {
2467
- return registerOutside();
2468
- }
2469
- let unregisterJob = null;
2470
- const unsubReady = scheduler.onRootReady(() => {
2471
- unregisterJob = registerOutside();
2472
- });
2473
- return () => {
2474
- unsubReady();
2475
- unregisterJob?.();
2476
- };
2477
- }
2478
- }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2112
+ }, [store, scheduler$1, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2479
2113
  const isPaused = React__namespace.useSyncExternalStore(
2480
2114
  // Subscribe function
2481
2115
  React__namespace.useCallback(
2482
2116
  (onStoreChange) => {
2483
- return getScheduler().subscribeJobState(id, onStoreChange);
2117
+ return scheduler.getScheduler().subscribeJobState(id, onStoreChange);
2484
2118
  },
2485
2119
  [id]
2486
2120
  ),
2487
2121
  // getSnapshot function
2488
- React__namespace.useCallback(() => getScheduler().isJobPaused(id), [id]),
2122
+ React__namespace.useCallback(() => scheduler.getScheduler().isJobPaused(id), [id]),
2489
2123
  // getServerSnapshot function (SSR)
2490
2124
  React__namespace.useCallback(() => false, [])
2491
2125
  );
2492
2126
  const controls = React__namespace.useMemo(() => {
2493
- const scheduler2 = getScheduler();
2127
+ const scheduler2 = scheduler.getScheduler();
2494
2128
  return {
2495
2129
  /** The job's unique ID */
2496
2130
  id,
@@ -2505,7 +2139,7 @@ function useFrame(callback, priorityOrOptions) {
2505
2139
  * @param timestamp Optional timestamp (defaults to performance.now())
2506
2140
  */
2507
2141
  step: (timestamp) => {
2508
- getScheduler().stepJob(id, timestamp);
2142
+ scheduler.getScheduler().stepJob(id, timestamp);
2509
2143
  },
2510
2144
  /**
2511
2145
  * Manually step ALL jobs in the scheduler.
@@ -2513,20 +2147,20 @@ function useFrame(callback, priorityOrOptions) {
2513
2147
  * @param timestamp Optional timestamp (defaults to performance.now())
2514
2148
  */
2515
2149
  stepAll: (timestamp) => {
2516
- getScheduler().step(timestamp);
2150
+ scheduler.getScheduler().step(timestamp);
2517
2151
  },
2518
2152
  /**
2519
2153
  * Pause this job (set enabled=false).
2520
2154
  * Job remains registered but won't run.
2521
2155
  */
2522
2156
  pause: () => {
2523
- getScheduler().pauseJob(id);
2157
+ scheduler.getScheduler().pauseJob(id);
2524
2158
  },
2525
2159
  /**
2526
2160
  * Resume this job (set enabled=true).
2527
2161
  */
2528
2162
  resume: () => {
2529
- getScheduler().resumeJob(id);
2163
+ scheduler.getScheduler().resumeJob(id);
2530
2164
  },
2531
2165
  /**
2532
2166
  * Reactive paused state - automatically updates when pause/resume is called.
@@ -2564,22 +2198,29 @@ function buildFromCache(input, textureCache) {
2564
2198
  function useTexture(input, optionsOrOnLoad) {
2565
2199
  const renderer = useThree((state) => state.internal.actualRenderer);
2566
2200
  const store = useStore();
2567
- const textureCache = useThree((state) => state.textures);
2568
2201
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
2569
- const { onLoad, cache = false } = options;
2202
+ const { onLoad, cache = true } = options;
2203
+ const onLoadRef = React.useRef(onLoad);
2204
+ onLoadRef.current = onLoad;
2205
+ const onLoadCalledForRef = React.useRef(null);
2570
2206
  const urls = React.useMemo(() => getUrls(input), [input]);
2571
2207
  const cachedResult = React.useMemo(() => {
2572
2208
  if (!cache) return null;
2573
- if (!allUrlsCached(urls, textureCache)) return null;
2574
- return buildFromCache(input, textureCache);
2575
- }, [cache, urls, textureCache, input]);
2209
+ const textures = store.getState().textures;
2210
+ if (!allUrlsCached(urls, textures)) return null;
2211
+ return buildFromCache(input, textures);
2212
+ }, [cache, urls, input, store]);
2576
2213
  const loadedTextures = useLoader(
2577
2214
  webgpu.TextureLoader,
2578
2215
  IsObject(input) ? Object.values(input) : input
2579
2216
  );
2217
+ const inputKey = urls.join("\0");
2580
2218
  React.useLayoutEffect(() => {
2581
- if (!cachedResult) onLoad?.(loadedTextures);
2582
- }, [onLoad, cachedResult, loadedTextures]);
2219
+ if (cachedResult) return;
2220
+ if (onLoadCalledForRef.current === inputKey) return;
2221
+ onLoadCalledForRef.current = inputKey;
2222
+ onLoadRef.current?.(loadedTextures);
2223
+ }, [cachedResult, loadedTextures, inputKey]);
2583
2224
  React.useEffect(() => {
2584
2225
  if (cachedResult) return;
2585
2226
  if ("initTexture" in renderer) {
@@ -2612,8 +2253,6 @@ function useTexture(input, optionsOrOnLoad) {
2612
2253
  }, [input, loadedTextures, cachedResult]);
2613
2254
  React.useEffect(() => {
2614
2255
  if (!cache) return;
2615
- if (cachedResult) return;
2616
- const set = store.setState;
2617
2256
  const urlTextureMap = [];
2618
2257
  if (typeof input === "string") {
2619
2258
  urlTextureMap.push([input, mappedTextures]);
@@ -2627,18 +2266,32 @@ function useTexture(input, optionsOrOnLoad) {
2627
2266
  urlTextureMap.push([url, textureRecord[key]]);
2628
2267
  }
2629
2268
  }
2630
- set((state) => {
2631
- const newMap = new Map(state.textures);
2632
- let changed = false;
2269
+ store.setState((state) => {
2270
+ const refs = new Map(state._textureRefs);
2271
+ let textures = state.textures;
2272
+ let added = false;
2633
2273
  for (const [url, texture] of urlTextureMap) {
2634
- if (!newMap.has(url)) {
2635
- newMap.set(url, texture);
2636
- changed = true;
2274
+ if (!textures.has(url)) {
2275
+ if (!added) {
2276
+ textures = new Map(textures);
2277
+ added = true;
2278
+ }
2279
+ textures.set(url, texture);
2637
2280
  }
2281
+ refs.set(url, (refs.get(url) ?? 0) + 1);
2638
2282
  }
2639
- return changed ? { textures: newMap } : state;
2283
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
2640
2284
  });
2641
- }, [cache, input, mappedTextures, store, cachedResult]);
2285
+ return () => store.setState((state) => {
2286
+ const refs = new Map(state._textureRefs);
2287
+ for (const [url] of urlTextureMap) {
2288
+ const next = (refs.get(url) ?? 0) - 1;
2289
+ if (next <= 0) refs.delete(url);
2290
+ else refs.set(url, next);
2291
+ }
2292
+ return { _textureRefs: refs };
2293
+ });
2294
+ }, [cache, input, mappedTextures, store]);
2642
2295
  return mappedTextures;
2643
2296
  }
2644
2297
  useTexture.preload = (url) => useLoader.preload(webgpu.TextureLoader, url);
@@ -2654,106 +2307,90 @@ const Texture = ({
2654
2307
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children?.(ret) });
2655
2308
  };
2656
2309
 
2657
- function getTextureValue(entry) {
2658
- if (entry instanceof webgpu.Texture) return entry;
2659
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof webgpu.Texture) {
2660
- return entry.value;
2661
- }
2662
- return null;
2663
- }
2664
- function useTextures() {
2310
+ function useTextures(selector) {
2665
2311
  const store = useStore();
2666
- return React.useMemo(() => {
2667
- const set = store.setState;
2312
+ const registry = React.useMemo(() => {
2668
2313
  const getState = store.getState;
2669
- const add = (key, value) => {
2670
- set((state) => {
2671
- const newMap = new Map(state.textures);
2672
- newMap.set(key, value);
2673
- return { textures: newMap };
2674
- });
2675
- };
2676
- const addMultiple = (items) => {
2677
- set((state) => {
2678
- const newMap = new Map(state.textures);
2679
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
2680
- for (const [key, value] of entries) {
2681
- newMap.set(key, value);
2682
- }
2683
- return { textures: newMap };
2684
- });
2685
- };
2686
- const remove = (key) => {
2687
- set((state) => {
2688
- const newMap = new Map(state.textures);
2689
- newMap.delete(key);
2690
- return { textures: newMap };
2691
- });
2692
- };
2693
- const removeMultiple = (keys) => {
2694
- set((state) => {
2695
- const newMap = new Map(state.textures);
2696
- for (const key of keys) newMap.delete(key);
2697
- return { textures: newMap };
2698
- });
2699
- };
2700
- const dispose = (key) => {
2701
- const entry = getState().textures.get(key);
2702
- if (entry) {
2703
- const tex = getTextureValue(entry);
2704
- tex?.dispose();
2705
- }
2706
- remove(key);
2707
- };
2708
- const disposeMultiple = (keys) => {
2709
- const textures = getState().textures;
2710
- for (const key of keys) {
2711
- const entry = textures.get(key);
2712
- if (entry) {
2713
- const tex = getTextureValue(entry);
2714
- tex?.dispose();
2715
- }
2716
- }
2717
- removeMultiple(keys);
2718
- };
2719
- const disposeAll = () => {
2720
- const textures = getState().textures;
2721
- for (const entry of textures.values()) {
2722
- const tex = getTextureValue(entry);
2723
- tex?.dispose();
2724
- }
2725
- set({ textures: /* @__PURE__ */ new Map() });
2726
- };
2314
+ const setState = store.setState;
2315
+ const getOne = (key) => getState().textures.get(key);
2727
2316
  return {
2728
- // Getter for the textures Map (reactive via getState)
2729
- get textures() {
2317
+ get all() {
2730
2318
  return getState().textures;
2731
2319
  },
2732
- // Read
2733
- get: (key) => getState().textures.get(key),
2320
+ get(input) {
2321
+ if (typeof input === "string") return getOne(input);
2322
+ if (Array.isArray(input)) return input.map(getOne);
2323
+ const out = {};
2324
+ for (const name in input) out[name] = getOne(input[name]);
2325
+ return out;
2326
+ },
2734
2327
  has: (key) => getState().textures.has(key),
2735
- // Write
2736
- add,
2737
- addMultiple,
2738
- // Remove (cache only)
2739
- remove,
2740
- removeMultiple,
2741
- // Dispose (GPU + cache)
2742
- dispose,
2743
- disposeMultiple,
2744
- disposeAll
2328
+ add(keyOrRecord, texture) {
2329
+ setState((state) => {
2330
+ const textures = new Map(state.textures);
2331
+ if (typeof keyOrRecord === "string") {
2332
+ textures.set(keyOrRecord, texture);
2333
+ } else {
2334
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2335
+ }
2336
+ return { textures };
2337
+ });
2338
+ },
2339
+ dispose(key, options) {
2340
+ const state = getState();
2341
+ const refs = state._textureRefs.get(key) ?? 0;
2342
+ if (refs > 0 && !options?.force) {
2343
+ console.warn(
2344
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2345
+ );
2346
+ return false;
2347
+ }
2348
+ state.textures.get(key)?.dispose();
2349
+ setState((s) => {
2350
+ const textures = new Map(s.textures);
2351
+ textures.delete(key);
2352
+ const nextRefs = new Map(s._textureRefs);
2353
+ nextRefs.delete(key);
2354
+ return { textures, _textureRefs: nextRefs };
2355
+ });
2356
+ return true;
2357
+ },
2358
+ disposeAll() {
2359
+ for (const texture of getState().textures.values()) texture.dispose();
2360
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2361
+ }
2745
2362
  };
2746
2363
  }, [store]);
2364
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2365
+ const selected = useThree(subscribe);
2366
+ return selector ? selected : registry;
2747
2367
  }
2748
2368
 
2749
- function useRenderTarget(width, height, options) {
2369
+ function useRenderTarget(widthOrOptions, heightOrOptions, options) {
2750
2370
  const isLegacy = useThree((s) => s.isLegacy);
2751
2371
  const size = useThree((s) => s.size);
2372
+ let width;
2373
+ let height;
2374
+ let opts;
2375
+ if (typeof widthOrOptions === "object") {
2376
+ opts = widthOrOptions;
2377
+ } else if (typeof widthOrOptions === "number") {
2378
+ width = widthOrOptions;
2379
+ if (typeof heightOrOptions === "object") {
2380
+ height = widthOrOptions;
2381
+ opts = heightOrOptions;
2382
+ } else if (typeof heightOrOptions === "number") {
2383
+ height = heightOrOptions;
2384
+ opts = options;
2385
+ } else {
2386
+ height = widthOrOptions;
2387
+ }
2388
+ }
2752
2389
  return React.useMemo(() => {
2753
2390
  const w = width ?? size.width;
2754
2391
  const h = height ?? size.height;
2755
- return new webgpu.RenderTarget(w, h, options);
2756
- }, [width, height, size.width, size.height, options, isLegacy]);
2392
+ return new webgpu.RenderTarget(w, h, opts);
2393
+ }, [width, height, size.width, size.height, opts, isLegacy]);
2757
2394
  }
2758
2395
 
2759
2396
  function useStore() {
@@ -2781,7 +2418,7 @@ function addEffect(callback) {
2781
2418
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2782
2419
  });
2783
2420
  const id = `legacy_effect_${effectId++}`;
2784
- return getScheduler().registerGlobal("before", id, callback);
2421
+ return scheduler.getScheduler().registerGlobal("before", id, callback);
2785
2422
  }
2786
2423
  function addAfterEffect(callback) {
2787
2424
  notifyDepreciated({
@@ -2790,7 +2427,7 @@ function addAfterEffect(callback) {
2790
2427
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2791
2428
  });
2792
2429
  const id = `legacy_afterEffect_${effectId++}`;
2793
- return getScheduler().registerGlobal("after", id, callback);
2430
+ return scheduler.getScheduler().registerGlobal("after", id, callback);
2794
2431
  }
2795
2432
  function addTail(callback) {
2796
2433
  notifyDepreciated({
@@ -2798,16 +2435,16 @@ function addTail(callback) {
2798
2435
  body: "Use scheduler.onIdle(callback) instead.\naddTail will be removed in a future version.",
2799
2436
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2800
2437
  });
2801
- return getScheduler().onIdle(callback);
2438
+ return scheduler.getScheduler().onIdle(callback);
2802
2439
  }
2803
2440
  function invalidate(state, frames = 1, stackFrames = false) {
2804
- getScheduler().invalidate(frames, stackFrames);
2441
+ scheduler.getScheduler().invalidate(frames, stackFrames);
2805
2442
  }
2806
- function advance(timestamp, runGlobalEffects = true, state, frame) {
2807
- getScheduler().step(timestamp);
2443
+ function advance(timestamp) {
2444
+ scheduler.getScheduler().step(timestamp);
2808
2445
  }
2809
2446
 
2810
- const version = "10.0.0-alpha.2";
2447
+ const version = "10.0.0-alpha.3";
2811
2448
  const packageData = {
2812
2449
  version: version};
2813
2450
 
@@ -14246,6 +13883,7 @@ function swapInstances() {
14246
13883
  }
14247
13884
  }
14248
13885
  if (instance.isHidden) unhideInstance(instance);
13886
+ if (isObject3D(instance.object)) removeInteractivity(findInitialRoot(instance), instance.object);
14249
13887
  if (instance.object.__r3f) delete instance.object.__r3f;
14250
13888
  if (instance.type !== "primitive") disposeOnIdle(instance.object);
14251
13889
  }
@@ -14257,6 +13895,9 @@ function swapInstances() {
14257
13895
  instance.object = instance.props.object ?? new target(...instance.props.args ?? []);
14258
13896
  instance.object.__r3f = instance;
14259
13897
  setFiberRef(fiber, instance.object);
13898
+ delete instance.appliedOnce;
13899
+ instance.eventCount = 0;
13900
+ instance.handlers = {};
14260
13901
  applyProps(instance.object, instance.props);
14261
13902
  if (instance.props.attach) {
14262
13903
  attach(parent, instance);
@@ -14330,8 +13971,22 @@ const reconciler = /* @__PURE__ */ createReconciler({
14330
13971
  const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
14331
13972
  if (isTailSibling) swapInstances();
14332
13973
  },
14333
- finalizeInitialChildren: () => false,
14334
- commitMount() {
13974
+ finalizeInitialChildren: (instance) => {
13975
+ for (const prop in instance.props) {
13976
+ if (isFromRef(instance.props[prop])) return true;
13977
+ }
13978
+ return false;
13979
+ },
13980
+ commitMount(instance) {
13981
+ const resolved = {};
13982
+ for (const prop in instance.props) {
13983
+ const value = instance.props[prop];
13984
+ if (isFromRef(value)) {
13985
+ const ref = value[FROM_REF];
13986
+ if (ref.current != null) resolved[prop] = ref.current;
13987
+ }
13988
+ }
13989
+ if (Object.keys(resolved).length) applyProps(instance.object, resolved);
14335
13990
  },
14336
13991
  getPublicInstance: (instance) => instance?.object,
14337
13992
  prepareForCommit: () => null,
@@ -14458,11 +14113,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
14458
14113
  cloneMutableTextInstance(textInstance) {
14459
14114
  return textInstance;
14460
14115
  },
14116
+ // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
14117
+ // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
14118
+ // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
14119
+ // React never calls them. They stay as no-ops rather than throws: the host config is shared by
14120
+ // every root, and a throw here would turn a React-internal probe into a hard crash for users.
14121
+ // Guarded by a test asserting a full render + unmount never reaches them.
14461
14122
  cloneRootViewTransitionContainer(_rootContainer) {
14462
- throw new Error("Not implemented.");
14463
14123
  },
14464
14124
  removeRootViewTransitionClone(_rootContainer, _clone) {
14465
- throw new Error("Not implemented.");
14466
14125
  },
14467
14126
  // https://github.com/facebook/react/pull/32465
14468
14127
  createFragmentInstance: (_fiber) => null,
@@ -14547,11 +14206,15 @@ function createRoot(canvas) {
14547
14206
  const lastConfiguredProps = {};
14548
14207
  let configured = false;
14549
14208
  let pending = null;
14209
+ let rendererSetup = null;
14550
14210
  return {
14551
14211
  async configure(props = {}) {
14552
14212
  let resolve;
14553
14213
  pending = new Promise((_resolve) => resolve = _resolve);
14554
14214
  const {
14215
+ id: canvasId,
14216
+ primaryCanvas,
14217
+ scheduler: schedulerConfig,
14555
14218
  gl: glConfig,
14556
14219
  renderer: rendererConfig,
14557
14220
  size: propsSize,
@@ -14559,10 +14222,6 @@ function createRoot(canvas) {
14559
14222
  events,
14560
14223
  onCreated: onCreatedCallback,
14561
14224
  shadows = false,
14562
- linear = false,
14563
- flat = false,
14564
- textureColorSpace = webgpu.SRGBColorSpace,
14565
- legacy = false,
14566
14225
  orthographic = false,
14567
14226
  frameloop = "always",
14568
14227
  dpr = [1, 2],
@@ -14574,11 +14233,14 @@ function createRoot(canvas) {
14574
14233
  onDropMissed,
14575
14234
  autoUpdateFrustum = true,
14576
14235
  occlusion = false,
14577
- _sizeProps
14236
+ _sizeProps,
14237
+ forceEven
14578
14238
  } = props;
14579
- const state = store.getState();
14239
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || webgpu.SRGBColorSpace;
14240
+ let state = store.getState();
14580
14241
  const defaultGPUProps = {
14581
- canvas
14242
+ canvas,
14243
+ antialias: true
14582
14244
  };
14583
14245
  if (glConfig && !R3F_BUILD_LEGACY) {
14584
14246
  throw new Error(
@@ -14590,14 +14252,63 @@ function createRoot(canvas) {
14590
14252
  }
14591
14253
  let renderer = state.internal.actualRenderer;
14592
14254
  if (!state.internal.actualRenderer) {
14593
- renderer = await resolveRenderer(rendererConfig, defaultGPUProps, webgpu.WebGPURenderer);
14594
- if (!renderer.hasInitialized?.()) {
14595
- await renderer.init();
14255
+ if (!rendererSetup) {
14256
+ rendererSetup = (async () => {
14257
+ if (primaryCanvas) {
14258
+ const primary = await waitForPrimary(primaryCanvas);
14259
+ renderer = primary.renderer;
14260
+ state.internal.actualRenderer = renderer;
14261
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14262
+ primary.store.setState((prev) => ({
14263
+ internal: { ...prev.internal, isMultiCanvas: true }
14264
+ }));
14265
+ state.set((prev) => ({
14266
+ webGPUSupported: primary.store.getState().webGPUSupported,
14267
+ renderer,
14268
+ primaryStore: primary.store,
14269
+ internal: {
14270
+ ...prev.internal,
14271
+ canvasTarget,
14272
+ isMultiCanvas: true,
14273
+ isSecondary: true,
14274
+ targetId: primaryCanvas
14275
+ }
14276
+ }));
14277
+ } else {
14278
+ renderer = await resolveRenderer(rendererConfig, defaultGPUProps, webgpu.WebGPURenderer);
14279
+ if (!renderer.hasInitialized?.()) {
14280
+ const size2 = computeInitialSize(canvas, propsSize);
14281
+ if (size2.width > 0 && size2.height > 0) {
14282
+ const pixelRatio = calculateDpr(dpr);
14283
+ canvas.width = size2.width * pixelRatio;
14284
+ canvas.height = size2.height * pixelRatio;
14285
+ }
14286
+ await renderer.init();
14287
+ }
14288
+ const backend = renderer.backend;
14289
+ const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14290
+ state.internal.actualRenderer = renderer;
14291
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14292
+ if (canvasId && !state.internal.isSecondary) {
14293
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14294
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14295
+ state.set((prev) => ({
14296
+ internal: {
14297
+ ...prev.internal,
14298
+ canvasTarget,
14299
+ unregisterPrimary
14300
+ }
14301
+ }));
14302
+ }
14303
+ }
14304
+ })().catch((err) => {
14305
+ rendererSetup = null;
14306
+ throw err;
14307
+ });
14596
14308
  }
14597
- const backend = renderer.backend;
14598
- const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14599
- state.internal.actualRenderer = renderer;
14600
- state.set({ webGPUSupported: isWebGPUBackend, renderer });
14309
+ await rendererSetup;
14310
+ state = store.getState();
14311
+ renderer = state.internal.actualRenderer;
14601
14312
  }
14602
14313
  let raycaster = state.raycaster;
14603
14314
  if (!raycaster) state.set({ raycaster: raycaster = new webgpu.Raycaster() });
@@ -14606,6 +14317,7 @@ function createRoot(canvas) {
14606
14317
  if (!is.equ(params, raycaster.params, shallowLoose)) {
14607
14318
  applyProps(raycaster, { params: { ...raycaster.params, ...params } });
14608
14319
  }
14320
+ let tempCamera = state.camera;
14609
14321
  if (!state.camera || state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose)) {
14610
14322
  lastCamera = cameraOptions;
14611
14323
  const isCamera = cameraOptions?.isCamera;
@@ -14625,6 +14337,7 @@ function createRoot(canvas) {
14625
14337
  if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0);
14626
14338
  }
14627
14339
  state.set({ camera });
14340
+ tempCamera = camera;
14628
14341
  raycaster.camera = camera;
14629
14342
  }
14630
14343
  if (!state.scene) {
@@ -14642,7 +14355,7 @@ function createRoot(canvas) {
14642
14355
  rootScene: scene,
14643
14356
  internal: { ...prev.internal, container: scene }
14644
14357
  }));
14645
- const camera = state.camera;
14358
+ const camera = tempCamera;
14646
14359
  if (camera && !camera.parent) scene.add(camera);
14647
14360
  }
14648
14361
  if (events && !state.events.handlers) {
@@ -14659,6 +14372,9 @@ function createRoot(canvas) {
14659
14372
  if (_sizeProps !== void 0) {
14660
14373
  state.set({ _sizeProps });
14661
14374
  }
14375
+ if (forceEven !== void 0 && state.internal.forceEven !== forceEven) {
14376
+ state.set((prev) => ({ internal: { ...prev.internal, forceEven } }));
14377
+ }
14662
14378
  const size = computeInitialSize(canvas, propsSize);
14663
14379
  if (!state._sizeImperative && !is.equ(size, state.size, shallowLoose)) {
14664
14380
  const wasImperative = state._sizeImperative;
@@ -14685,10 +14401,10 @@ function createRoot(canvas) {
14685
14401
  lastConfiguredProps.performance = performance;
14686
14402
  }
14687
14403
  if (!state.xr) {
14688
- const handleXRFrame = (timestamp, frame) => {
14404
+ const handleXRFrame = (timestamp, _frame) => {
14689
14405
  const state2 = store.getState();
14690
14406
  if (state2.frameloop === "never") return;
14691
- advance(timestamp, true);
14407
+ advance(timestamp);
14692
14408
  };
14693
14409
  const actualRenderer = state.internal.actualRenderer;
14694
14410
  const handleSessionChange = () => {
@@ -14700,16 +14416,16 @@ function createRoot(canvas) {
14700
14416
  };
14701
14417
  const xr = {
14702
14418
  connect() {
14703
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14704
- const actualRenderer2 = renderer2 || gl;
14705
- actualRenderer2.xr.addEventListener("sessionstart", handleSessionChange);
14706
- actualRenderer2.xr.addEventListener("sessionend", handleSessionChange);
14419
+ const { gl, renderer: renderer2 } = store.getState();
14420
+ const xrManager = (renderer2 || gl).xr;
14421
+ xrManager.addEventListener("sessionstart", handleSessionChange);
14422
+ xrManager.addEventListener("sessionend", handleSessionChange);
14707
14423
  },
14708
14424
  disconnect() {
14709
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14710
- const actualRenderer2 = renderer2 || gl;
14711
- actualRenderer2.xr.removeEventListener("sessionstart", handleSessionChange);
14712
- actualRenderer2.xr.removeEventListener("sessionend", handleSessionChange);
14425
+ const { gl, renderer: renderer2 } = store.getState();
14426
+ const xrManager = (renderer2 || gl).xr;
14427
+ xrManager.removeEventListener("sessionstart", handleSessionChange);
14428
+ xrManager.removeEventListener("sessionend", handleSessionChange);
14713
14429
  }
14714
14430
  };
14715
14431
  if (typeof renderer.xr?.addEventListener === "function") xr.connect();
@@ -14721,15 +14437,22 @@ function createRoot(canvas) {
14721
14437
  const oldType = renderer.shadowMap.type;
14722
14438
  renderer.shadowMap.enabled = !!shadows;
14723
14439
  if (is.boo(shadows)) {
14724
- renderer.shadowMap.type = webgpu.PCFSoftShadowMap;
14440
+ renderer.shadowMap.type = webgpu.PCFShadowMap;
14725
14441
  } else if (is.str(shadows)) {
14442
+ if (shadows === "soft") {
14443
+ notifyDepreciated({
14444
+ heading: 'shadows="soft" is deprecated',
14445
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14446
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14447
+ });
14448
+ }
14726
14449
  const types = {
14727
14450
  basic: webgpu.BasicShadowMap,
14728
14451
  percentage: webgpu.PCFShadowMap,
14729
- soft: webgpu.PCFSoftShadowMap,
14452
+ soft: webgpu.PCFShadowMap,
14730
14453
  variance: webgpu.VSMShadowMap
14731
14454
  };
14732
- renderer.shadowMap.type = types[shadows] ?? webgpu.PCFSoftShadowMap;
14455
+ renderer.shadowMap.type = types[shadows] ?? webgpu.PCFShadowMap;
14733
14456
  } else if (is.obj(shadows)) {
14734
14457
  Object.assign(renderer.shadowMap, shadows);
14735
14458
  }
@@ -14737,28 +14460,70 @@ function createRoot(canvas) {
14737
14460
  renderer.shadowMap.needsUpdate = true;
14738
14461
  }
14739
14462
  }
14463
+ if (!configured) {
14464
+ renderer.outputColorSpace = webgpu.SRGBColorSpace;
14465
+ renderer.toneMapping = webgpu.ACESFilmicToneMapping;
14466
+ }
14740
14467
  if (textureColorSpace !== lastConfiguredProps.textureColorSpace) {
14741
14468
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
14742
14469
  lastConfiguredProps.textureColorSpace = textureColorSpace;
14743
14470
  }
14471
+ const r3fProps = ["textureColorSpace"];
14472
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14473
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
14744
14474
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
14745
- applyProps(renderer, glConfig);
14475
+ const glProps = {};
14476
+ for (const key in glConfig) {
14477
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14478
+ }
14479
+ applyProps(renderer, glProps);
14746
14480
  }
14747
14481
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
14748
14482
  const currentRenderer = state.renderer;
14749
14483
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
14750
- applyProps(currentRenderer, rendererConfig);
14484
+ const rendererProps = {};
14485
+ for (const key in rendererConfig) {
14486
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14487
+ }
14488
+ applyProps(currentRenderer, rendererProps);
14751
14489
  }
14752
14490
  }
14753
- const scheduler = getScheduler();
14491
+ const scheduler$1 = scheduler.getScheduler();
14754
14492
  const rootId = state.internal.rootId;
14755
14493
  if (!rootId) {
14756
- const newRootId = scheduler.generateRootId();
14757
- const unregisterRoot = scheduler.registerRoot(newRootId, {
14494
+ const newRootId = canvasId || scheduler$1.generateRootId();
14495
+ const unregisterRoot = scheduler$1.registerRoot(newRootId, {
14758
14496
  getState: () => store.getState(),
14759
14497
  onError: (err) => store.getState().setError(err)
14760
14498
  });
14761
- const unregisterFrustum = scheduler.register(
14499
+ const unregisterCanvasTarget = scheduler$1.register(
14500
+ () => {
14501
+ const state2 = store.getState();
14502
+ if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14503
+ const renderer2 = state2.internal.actualRenderer;
14504
+ renderer2.setCanvasTarget(state2.internal.canvasTarget);
14505
+ }
14506
+ },
14507
+ {
14508
+ id: `${newRootId}_canvasTarget`,
14509
+ rootId: newRootId,
14510
+ phase: "start",
14511
+ system: true
14512
+ }
14513
+ );
14514
+ const unregisterEventsFlush = scheduler$1.register(
14515
+ () => {
14516
+ const state2 = store.getState();
14517
+ state2.events.flush?.();
14518
+ },
14519
+ {
14520
+ id: `${newRootId}_events`,
14521
+ rootId: newRootId,
14522
+ phase: "input",
14523
+ system: true
14524
+ }
14525
+ );
14526
+ const unregisterFrustum = scheduler$1.register(
14762
14527
  () => {
14763
14528
  const state2 = store.getState();
14764
14529
  if (state2.autoUpdateFrustum && state2.camera) {
@@ -14768,11 +14533,11 @@ function createRoot(canvas) {
14768
14533
  {
14769
14534
  id: `${newRootId}_frustum`,
14770
14535
  rootId: newRootId,
14771
- phase: "preRender",
14536
+ before: "render",
14772
14537
  system: true
14773
14538
  }
14774
14539
  );
14775
- const unregisterVisibility = scheduler.register(
14540
+ const unregisterVisibility = scheduler$1.register(
14776
14541
  () => {
14777
14542
  const state2 = store.getState();
14778
14543
  checkVisibility(state2);
@@ -14780,30 +14545,34 @@ function createRoot(canvas) {
14780
14545
  {
14781
14546
  id: `${newRootId}_visibility`,
14782
14547
  rootId: newRootId,
14783
- phase: "preRender",
14548
+ before: "render",
14784
14549
  system: true,
14785
14550
  after: `${newRootId}_frustum`
14786
14551
  }
14787
14552
  );
14788
- const unregisterRender = scheduler.register(
14553
+ const unregisterRender = scheduler$1.register(
14789
14554
  () => {
14790
14555
  const state2 = store.getState();
14791
14556
  const renderer2 = state2.internal.actualRenderer;
14792
- const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14557
+ const userHandlesRender = scheduler$1.hasUserJobsInPhase("render", newRootId);
14793
14558
  if (userHandlesRender || state2.internal.priority) return;
14794
14559
  try {
14795
- if (state2.postProcessing?.render) state2.postProcessing.render();
14560
+ if (state2.renderPipeline?.render) state2.renderPipeline.render();
14796
14561
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
14797
14562
  } catch (error) {
14798
14563
  state2.setError(error instanceof Error ? error : new Error(String(error)));
14799
14564
  }
14800
14565
  },
14801
14566
  {
14802
- id: `${newRootId}_render`,
14567
+ // Use canvas ID directly as job ID if available, otherwise use generated rootId
14568
+ id: canvasId || `${newRootId}_render`,
14803
14569
  rootId: newRootId,
14804
14570
  phase: "render",
14805
- system: true
14571
+ system: true,
14806
14572
  // Internal flag: this is a system job, not user-controlled
14573
+ // Apply scheduler config for render ordering and rate limiting
14574
+ ...schedulerConfig?.after && { after: schedulerConfig.after },
14575
+ ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14807
14576
  }
14808
14577
  );
14809
14578
  state.set((state2) => ({
@@ -14812,15 +14581,17 @@ function createRoot(canvas) {
14812
14581
  rootId: newRootId,
14813
14582
  unregisterRoot: () => {
14814
14583
  unregisterRoot();
14584
+ unregisterCanvasTarget();
14585
+ unregisterEventsFlush();
14815
14586
  unregisterFrustum();
14816
14587
  unregisterVisibility();
14817
14588
  unregisterRender();
14818
14589
  },
14819
- scheduler
14590
+ scheduler: scheduler$1
14820
14591
  }
14821
14592
  }));
14822
14593
  }
14823
- scheduler.frameloop = frameloop;
14594
+ scheduler$1.frameloop = frameloop;
14824
14595
  onCreated = onCreatedCallback;
14825
14596
  configured = true;
14826
14597
  resolve();
@@ -14870,15 +14641,24 @@ function unmountComponentAtNode(canvas, callback) {
14870
14641
  const renderer = state.internal.actualRenderer;
14871
14642
  const unregisterRoot = state.internal.unregisterRoot;
14872
14643
  if (unregisterRoot) unregisterRoot();
14644
+ const unregisterPrimary = state.internal.unregisterPrimary;
14645
+ if (unregisterPrimary) unregisterPrimary();
14646
+ const canvasTarget = state.internal.canvasTarget;
14647
+ if (canvasTarget?.dispose) canvasTarget.dispose();
14873
14648
  state.events.disconnect?.();
14874
14649
  cleanupHelperGroup(root.store);
14875
- renderer?.renderLists?.dispose?.();
14876
- renderer?.forceContextLoss?.();
14877
- if (renderer?.xr) state.xr.disconnect();
14650
+ if (state.isLegacy && renderer) {
14651
+ ;
14652
+ renderer.renderLists?.dispose?.();
14653
+ renderer.forceContextLoss?.();
14654
+ }
14655
+ if (!state.internal.isSecondary) {
14656
+ if (renderer?.xr) state.xr.disconnect();
14657
+ }
14878
14658
  dispose(state.scene);
14879
14659
  _roots.delete(canvas);
14880
14660
  if (callback) callback(canvas);
14881
- } catch (e) {
14661
+ } catch {
14882
14662
  }
14883
14663
  }, 500);
14884
14664
  }
@@ -14886,36 +14666,34 @@ function unmountComponentAtNode(canvas, callback) {
14886
14666
  }
14887
14667
  }
14888
14668
  function createPortal(children, container, state) {
14889
- return /* @__PURE__ */ jsxRuntime.jsx(PortalWrapper, { children, container, state });
14669
+ return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children, container, state });
14890
14670
  }
14891
- function PortalWrapper({ children, container, state }) {
14671
+ function Portal({ children, container, state }) {
14892
14672
  const isRef = React.useCallback((obj) => obj && "current" in obj, []);
14893
- const [resolvedContainer, setResolvedContainer] = React.useState(() => {
14673
+ const [resolvedContainer, _setResolvedContainer] = React.useState(() => {
14894
14674
  if (isRef(container)) return container.current ?? null;
14895
14675
  return container;
14896
14676
  });
14677
+ const setResolvedContainer = React.useCallback(
14678
+ (newContainer) => {
14679
+ if (!newContainer || newContainer === resolvedContainer) return;
14680
+ _setResolvedContainer(isRef(newContainer) ? newContainer.current : newContainer);
14681
+ },
14682
+ [resolvedContainer, _setResolvedContainer, isRef]
14683
+ );
14897
14684
  React.useMemo(() => {
14898
- if (isRef(container)) {
14899
- const current = container.current;
14900
- if (!current) {
14901
- queueMicrotask(() => {
14902
- const updated = container.current;
14903
- if (updated && updated !== resolvedContainer) {
14904
- setResolvedContainer(updated);
14905
- }
14906
- });
14907
- } else if (current !== resolvedContainer) {
14908
- setResolvedContainer(current);
14909
- }
14910
- } else if (container !== resolvedContainer) {
14911
- setResolvedContainer(container);
14685
+ if (isRef(container) && !container.current) {
14686
+ return queueMicrotask(() => {
14687
+ setResolvedContainer(container.current);
14688
+ });
14912
14689
  }
14913
- }, [container, resolvedContainer, isRef]);
14690
+ setResolvedContainer(container);
14691
+ }, [container, isRef, setResolvedContainer]);
14914
14692
  if (!resolvedContainer) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {});
14915
14693
  const portalKey = resolvedContainer.uuid ?? `portal-${resolvedContainer.id ?? "unknown"}`;
14916
- return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children, container: resolvedContainer, state }, portalKey);
14694
+ return /* @__PURE__ */ jsxRuntime.jsx(PortalInner, { children, container: resolvedContainer, state }, portalKey);
14917
14695
  }
14918
- function Portal({ state = {}, children, container }) {
14696
+ function PortalInner({ state = {}, children, container }) {
14919
14697
  const { events, size, injectScene = true, ...rest } = state;
14920
14698
  const previousRoot = useStore();
14921
14699
  const [raycaster] = React.useState(() => new webgpu.Raycaster());
@@ -14936,12 +14714,14 @@ function Portal({ state = {}, children, container }) {
14936
14714
  };
14937
14715
  }, [portalScene, container, injectScene]);
14938
14716
  const inject = useMutableCallback((rootState, injectState) => {
14717
+ const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
14939
14718
  let viewport = void 0;
14940
- if (injectState.camera && size) {
14719
+ if (injectState.camera && (size || injectState.size)) {
14941
14720
  const camera = injectState.camera;
14942
- viewport = rootState.viewport.getCurrentViewport(camera, new webgpu.Vector3(), size);
14943
- if (camera !== rootState.camera) updateCamera(camera, size);
14721
+ viewport = rootState.viewport.getCurrentViewport(camera, new webgpu.Vector3(), resolvedSize);
14722
+ if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
14944
14723
  }
14724
+ const set = injectState.set;
14945
14725
  return {
14946
14726
  // The intersect consists of the previous root state
14947
14727
  ...rootState,
@@ -14957,10 +14737,10 @@ function Portal({ state = {}, children, container }) {
14957
14737
  previousRoot,
14958
14738
  // Events, size and viewport can be overridden by the inject layer
14959
14739
  events: { ...rootState.events, ...injectState.events, ...events },
14960
- size: { ...rootState.size, ...size },
14740
+ size: resolvedSize,
14961
14741
  viewport: { ...rootState.viewport, ...viewport },
14962
14742
  // Layers are allowed to override events
14963
- setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14743
+ setEvents: (events2) => set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14964
14744
  // Container for child attachment - the portalScene (injected or container itself)
14965
14745
  internal: { ...rootState.internal, ...injectState.internal, container: portalScene }
14966
14746
  };
@@ -14969,9 +14749,13 @@ function Portal({ state = {}, children, container }) {
14969
14749
  const store = traditional.createWithEqualityFn((set, get) => ({ ...rest, set, get }));
14970
14750
  const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
14971
14751
  onMutate(previousRoot.getState());
14972
- previousRoot.subscribe(onMutate);
14973
14752
  return store;
14974
14753
  }, [previousRoot, container]);
14754
+ useIsomorphicLayoutEffect(() => {
14755
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14756
+ const unsubscribe = previousRoot.subscribe(onMutate);
14757
+ return unsubscribe;
14758
+ }, [previousRoot, usePortalStore]);
14975
14759
  return (
14976
14760
  // @ts-ignore, reconciler types are not maintained
14977
14761
  /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reconciler.createPortal(
@@ -14985,21 +14769,72 @@ function flushSync(fn) {
14985
14769
  return reconciler.flushSyncFromReconciler(fn);
14986
14770
  }
14987
14771
 
14772
+ function parseBackground(background) {
14773
+ if (!background) return null;
14774
+ if (typeof background === "object" && !background.isColor) {
14775
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14776
+ return {
14777
+ ...rest,
14778
+ preset,
14779
+ files: envMap || files,
14780
+ backgroundFiles: backgroundMap,
14781
+ background: true
14782
+ };
14783
+ }
14784
+ if (typeof background === "number") {
14785
+ return { color: background, background: true };
14786
+ }
14787
+ if (typeof background === "string") {
14788
+ if (background in presetsObj) {
14789
+ return { preset: background, background: true };
14790
+ }
14791
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14792
+ return { files: background, background: true };
14793
+ }
14794
+ return { color: background, background: true };
14795
+ }
14796
+ if (background.isColor) {
14797
+ return { color: background, background: true };
14798
+ }
14799
+ return null;
14800
+ }
14801
+
14802
+ function parseRendererConfig(rendererProp) {
14803
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14804
+ if (isRendererConfig) {
14805
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14806
+ return {
14807
+ primaryCanvas: pc,
14808
+ scheduler: sc,
14809
+ renderer: Object.keys(rest).length > 0 ? rest : rendererProp
14810
+ };
14811
+ }
14812
+ return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
14813
+ }
14814
+
14815
+ function clearHmrCaches(store) {
14816
+ store.setState((state) => ({
14817
+ nodes: {},
14818
+ uniforms: {},
14819
+ buffers: {},
14820
+ gpuStorage: {},
14821
+ _hmrVersion: state._hmrVersion + 1
14822
+ }));
14823
+ }
14824
+
14988
14825
  function CanvasImpl({
14989
14826
  ref,
14990
14827
  children,
14991
14828
  fallback,
14992
14829
  resize,
14993
14830
  style,
14831
+ id,
14994
14832
  gl,
14995
- renderer,
14833
+ renderer: rendererProp,
14996
14834
  events = createPointerEvents,
14997
14835
  eventSource,
14998
14836
  eventPrefix,
14999
14837
  shadows,
15000
- linear,
15001
- flat,
15002
- legacy,
15003
14838
  orthographic,
15004
14839
  frameloop,
15005
14840
  dpr,
@@ -15007,6 +14842,8 @@ function CanvasImpl({
15007
14842
  raycaster,
15008
14843
  camera,
15009
14844
  scene,
14845
+ autoUpdateFrustum,
14846
+ occlusion,
15010
14847
  onPointerMissed,
15011
14848
  onDragOverMissed,
15012
14849
  onDropMissed,
@@ -15014,10 +14851,14 @@ function CanvasImpl({
15014
14851
  hmr,
15015
14852
  width,
15016
14853
  height,
14854
+ background,
14855
+ forceEven,
15017
14856
  ...props
15018
14857
  }) {
14858
+ const { primaryCanvas, scheduler, renderer } = parseRendererConfig(rendererProp);
15019
14859
  React__namespace.useMemo(() => extend(THREE), []);
15020
14860
  const Bridge = useBridge();
14861
+ const backgroundProps = React__namespace.useMemo(() => parseBackground(background), [background]);
15021
14862
  const hasInitialSizeRef = React__namespace.useRef(false);
15022
14863
  const measureConfig = React__namespace.useMemo(() => {
15023
14864
  if (!hasInitialSizeRef.current) {
@@ -15034,15 +14875,20 @@ function CanvasImpl({
15034
14875
  };
15035
14876
  }, [resize, hasInitialSizeRef.current]);
15036
14877
  const [containerRef, containerRect] = useMeasure__default(measureConfig);
15037
- const effectiveSize = React__namespace.useMemo(
15038
- () => ({
15039
- width: width ?? containerRect.width,
15040
- height: height ?? containerRect.height,
14878
+ const effectiveSize = React__namespace.useMemo(() => {
14879
+ let w = width ?? containerRect.width;
14880
+ let h = height ?? containerRect.height;
14881
+ if (forceEven) {
14882
+ w = Math.ceil(w / 2) * 2;
14883
+ h = Math.ceil(h / 2) * 2;
14884
+ }
14885
+ return {
14886
+ width: w,
14887
+ height: h,
15041
14888
  top: containerRect.top,
15042
14889
  left: containerRect.left
15043
- }),
15044
- [width, height, containerRect]
15045
- );
14890
+ };
14891
+ }, [width, height, containerRect, forceEven]);
15046
14892
  if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
15047
14893
  hasInitialSizeRef.current = true;
15048
14894
  }
@@ -15054,6 +14900,7 @@ function CanvasImpl({
15054
14900
  const handleDropMissed = useMutableCallback(onDropMissed);
15055
14901
  const [block, setBlock] = React__namespace.useState(false);
15056
14902
  const [error, setError] = React__namespace.useState(false);
14903
+ const [fallbackVisible, setFallbackVisible] = React__namespace.useState(false);
15057
14904
  if (block) throw block;
15058
14905
  if (error) throw error;
15059
14906
  const root = React__namespace.useRef(null);
@@ -15061,6 +14908,7 @@ function CanvasImpl({
15061
14908
  const unsubscribeErrorRef = React__namespace.useRef(null);
15062
14909
  useIsomorphicLayoutEffect(() => {
15063
14910
  effectActiveRef.current = true;
14911
+ if (fallbackVisible) return;
15064
14912
  const canvas = canvasRef.current;
15065
14913
  if (effectiveSize.width > 0 && effectiveSize.height > 0 && canvas) {
15066
14914
  if (!root.current) {
@@ -15081,24 +14929,27 @@ function CanvasImpl({
15081
14929
  }
15082
14930
  async function run() {
15083
14931
  if (!effectActiveRef.current || !root.current) return;
15084
- await root.current.configure({
14932
+ const configured = await root.current.configure({
14933
+ id,
14934
+ primaryCanvas,
14935
+ scheduler,
15085
14936
  gl,
15086
14937
  renderer,
15087
14938
  scene,
15088
14939
  events,
15089
14940
  shadows,
15090
- linear,
15091
- flat,
15092
- legacy,
15093
14941
  orthographic,
15094
14942
  frameloop,
15095
14943
  dpr,
15096
14944
  performance,
15097
14945
  raycaster,
15098
14946
  camera,
14947
+ autoUpdateFrustum,
14948
+ occlusion,
15099
14949
  size: effectiveSize,
15100
14950
  // Store size props for reset functionality
15101
14951
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
14952
+ forceEven,
15102
14953
  // Pass mutable reference to onPointerMissed so it's free to update
15103
14954
  onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
15104
14955
  onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
@@ -15119,10 +14970,25 @@ function CanvasImpl({
15119
14970
  }
15120
14971
  onCreated?.(state);
15121
14972
  }
15122
- });
15123
- if (!effectActiveRef.current || !root.current) return;
14973
+ }).then(
14974
+ () => true,
14975
+ // Renderer setup failed (e.g. no WebGL/WebGPU support). The `fallback` prop lives
14976
+ // inside <canvas>, which browsers don't display, so surface it as visible DOM
14977
+ // instead; with no fallback, rethrow to an external error boundary. (#3757)
14978
+ (setupError) => {
14979
+ if (effectActiveRef.current) {
14980
+ if (fallback != null) setFallbackVisible(true);
14981
+ else setError(setupError);
14982
+ }
14983
+ return false;
14984
+ }
14985
+ );
14986
+ if (!configured || !effectActiveRef.current || !root.current) return;
15124
14987
  root.current.render(
15125
- /* @__PURE__ */ jsxRuntime.jsx(Bridge, { children: /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxRuntime.jsx(React__namespace.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(Block, { set: setBlock }), children: children ?? null }) }) })
14988
+ /* @__PURE__ */ jsxRuntime.jsx(Bridge, { children: /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(Block, { set: setBlock }), children: [
14989
+ backgroundProps && /* @__PURE__ */ jsxRuntime.jsx(Environment, { ...backgroundProps }),
14990
+ children ?? null
14991
+ ] }) }) })
15126
14992
  );
15127
14993
  }
15128
14994
  run();
@@ -15149,20 +15015,15 @@ function CanvasImpl({
15149
15015
  const canvas = canvasRef.current;
15150
15016
  if (!canvas) return;
15151
15017
  const handleHMR = () => {
15152
- const rootEntry = _roots.get(canvas);
15153
- if (rootEntry?.store) {
15154
- rootEntry.store.setState((state) => ({
15155
- nodes: {},
15156
- uniforms: {},
15157
- _hmrVersion: state._hmrVersion + 1
15158
- }));
15159
- }
15018
+ queueMicrotask(() => {
15019
+ const rootEntry = _roots.get(canvas);
15020
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
15021
+ });
15160
15022
  };
15161
15023
  if (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) }) !== "undefined" && undefined) {
15162
15024
  const hot = undefined;
15163
15025
  hot.on("vite:afterUpdate", handleHMR);
15164
- return () => hot.dispose?.(() => {
15165
- });
15026
+ return () => hot.off?.("vite:afterUpdate", handleHMR);
15166
15027
  }
15167
15028
  if (typeof module !== "undefined" && module.hot) {
15168
15029
  const hot = module.hot;
@@ -15185,7 +15046,20 @@ function CanvasImpl({
15185
15046
  ...style
15186
15047
  },
15187
15048
  ...props,
15188
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "r3f-canvas", style: { display: "block" }, children: fallback }) })
15049
+ children: fallbackVisible ? (
15050
+ // Renderer setup failed: render the fallback as visible DOM. Inside <canvas> (below)
15051
+ // it exists in the tree but browsers never paint it, which is the whole bug (#3757).
15052
+ fallback
15053
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(
15054
+ "canvas",
15055
+ {
15056
+ ref: canvasRef,
15057
+ id,
15058
+ className: "r3f-canvas",
15059
+ style: { display: "block", width: "100%", height: "100%" },
15060
+ children: fallback
15061
+ }
15062
+ ) })
15189
15063
  }
15190
15064
  );
15191
15065
  }
@@ -15258,6 +15132,48 @@ let ScopedStore = _ScopedStore;
15258
15132
  function createScopedStore(data) {
15259
15133
  return new ScopedStore(data);
15260
15134
  }
15135
+ function createLazyCreatorState(state) {
15136
+ let _uniforms = null;
15137
+ let _nodes = null;
15138
+ let _buffers = null;
15139
+ let _gpuStorage = null;
15140
+ return Object.create(state, {
15141
+ uniforms: {
15142
+ get() {
15143
+ return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
15144
+ }
15145
+ },
15146
+ nodes: {
15147
+ get() {
15148
+ return _nodes ?? (_nodes = createScopedStore(state.nodes));
15149
+ }
15150
+ },
15151
+ buffers: {
15152
+ get() {
15153
+ return _buffers ?? (_buffers = createScopedStore(state.buffers));
15154
+ }
15155
+ },
15156
+ gpuStorage: {
15157
+ get() {
15158
+ return _gpuStorage ?? (_gpuStorage = createScopedStore(state.gpuStorage));
15159
+ }
15160
+ }
15161
+ });
15162
+ }
15163
+
15164
+ function resolvePrimary(local) {
15165
+ return local.getState().primaryStore ?? local;
15166
+ }
15167
+ function usePrimaryStore() {
15168
+ const local = useStore();
15169
+ const primary = local.getState().primaryStore;
15170
+ return React.useMemo(() => primary ?? local, [primary, local]);
15171
+ }
15172
+ function usePrimaryThree(selector = (state) => state, equalityFn) {
15173
+ const local = useStore();
15174
+ const primary = resolvePrimary(local);
15175
+ return primary(selector, equalityFn);
15176
+ }
15261
15177
 
15262
15178
  function addTexture(set, key, value) {
15263
15179
  set((state) => {
@@ -15298,6 +15214,27 @@ function createTextureOperations(set) {
15298
15214
  removeMultiple: (keys) => removeTextures(set, keys)
15299
15215
  };
15300
15216
  }
15217
+ function extractTSLValue(value) {
15218
+ if (value === null || value === void 0) return value;
15219
+ if (typeof value !== "object") return value;
15220
+ const node = value;
15221
+ if (!node.isNode) return value;
15222
+ if (node.isConstNode) {
15223
+ return node.value;
15224
+ }
15225
+ if ("value" in node) {
15226
+ let extractedValue = node.value;
15227
+ if (typeof node.traverse === "function") {
15228
+ node.traverse((n) => {
15229
+ if (n.isConstNode) {
15230
+ extractedValue = n.value;
15231
+ }
15232
+ });
15233
+ }
15234
+ return extractedValue;
15235
+ }
15236
+ return value;
15237
+ }
15301
15238
  function vectorize(inObject) {
15302
15239
  if (inObject === null || inObject === void 0) return inObject;
15303
15240
  if (typeof inObject === "string") {
@@ -15310,9 +15247,16 @@ function vectorize(inObject) {
15310
15247
  }
15311
15248
  if (typeof inObject !== "object") return inObject;
15312
15249
  const obj = inObject;
15250
+ if (obj.isNode) {
15251
+ return extractTSLValue(inObject);
15252
+ }
15313
15253
  if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
15314
15254
  if (obj.isMatrix3 || obj.isMatrix4) return inObject;
15315
15255
  if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
15256
+ if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
15257
+ const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
15258
+ return new webgpu.Color(obj.r * scale, obj.g * scale, obj.b * scale);
15259
+ }
15316
15260
  if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
15317
15261
  if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
15318
15262
  return new webgpu.Vector4(obj.x, obj.y, obj.z, obj.w);
@@ -15336,7 +15280,7 @@ function useCompareMemoize(value, deep) {
15336
15280
 
15337
15281
  const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15338
15282
  function useUniforms(creatorOrScope, scope) {
15339
- const store = useStore();
15283
+ const store = usePrimaryStore();
15340
15284
  const removeUniforms2 = React.useCallback(
15341
15285
  (names, targetScope) => {
15342
15286
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15375,17 +15319,14 @@ function useUniforms(creatorOrScope, scope) {
15375
15319
  const rebuildUniforms = React.useCallback(
15376
15320
  (targetScope) => {
15377
15321
  store.setState((state) => {
15378
- let newUniforms = state.uniforms;
15322
+ let newUniforms = {};
15379
15323
  if (targetScope && targetScope !== "root") {
15380
15324
  const { [targetScope]: _, ...rest } = state.uniforms;
15381
15325
  newUniforms = rest;
15382
15326
  } else if (targetScope === "root") {
15383
- newUniforms = {};
15384
15327
  for (const [key, value] of Object.entries(state.uniforms)) {
15385
15328
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15386
15329
  }
15387
- } else {
15388
- newUniforms = {};
15389
15330
  }
15390
15331
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15391
15332
  });
@@ -15393,20 +15334,26 @@ function useUniforms(creatorOrScope, scope) {
15393
15334
  [store]
15394
15335
  );
15395
15336
  const inputForMemoization = React.useMemo(() => {
15337
+ let raw = creatorOrScope;
15396
15338
  if (is.fun(creatorOrScope)) {
15397
- const state = store.getState();
15398
- const wrappedState = {
15399
- ...state,
15400
- uniforms: createScopedStore(state.uniforms),
15401
- nodes: createScopedStore(state.nodes)
15402
- };
15403
- return creatorOrScope(wrappedState);
15339
+ const wrappedState = createLazyCreatorState(store.getState());
15340
+ raw = creatorOrScope(wrappedState);
15341
+ }
15342
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
15343
+ const normalized = {};
15344
+ for (const [key, value] of Object.entries(raw)) {
15345
+ normalized[key] = vectorize(value);
15346
+ }
15347
+ return normalized;
15404
15348
  }
15405
- return creatorOrScope;
15349
+ return raw;
15406
15350
  }, [creatorOrScope, store]);
15407
15351
  const memoizedInput = useCompareMemoize(inputForMemoization);
15408
15352
  const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
15409
- const storeUniforms = useThree((s) => s.uniforms);
15353
+ const storeUniforms = usePrimaryThree((s) => s.uniforms);
15354
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15355
+ const readerDep = isReader ? storeUniforms : null;
15356
+ const creatorDep = isReader ? null : hmrVersion;
15410
15357
  const uniforms = React.useMemo(() => {
15411
15358
  if (memoizedInput === void 0) {
15412
15359
  return storeUniforms;
@@ -15461,28 +15408,20 @@ function useUniforms(creatorOrScope, scope) {
15461
15408
  }
15462
15409
  }
15463
15410
  return result;
15464
- }, [
15465
- store,
15466
- memoizedInput,
15467
- scope,
15468
- // Only include storeUniforms in deps for reader modes to enable reactivity
15469
- isReader ? storeUniforms : null
15470
- ]);
15411
+ }, [store, memoizedInput, scope, readerDep, creatorDep]);
15471
15412
  return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
15472
15413
  }
15473
15414
  function rebuildAllUniforms(store, scope) {
15415
+ store = store.getState().primaryStore ?? store;
15474
15416
  store.setState((state) => {
15475
- let newUniforms = state.uniforms;
15417
+ let newUniforms = {};
15476
15418
  if (scope && scope !== "root") {
15477
15419
  const { [scope]: _, ...rest } = state.uniforms;
15478
15420
  newUniforms = rest;
15479
15421
  } else if (scope === "root") {
15480
- newUniforms = {};
15481
15422
  for (const [key, value] of Object.entries(state.uniforms)) {
15482
15423
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15483
15424
  }
15484
- } else {
15485
- newUniforms = {};
15486
15425
  }
15487
15426
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15488
15427
  });
@@ -15550,15 +15489,17 @@ function isSameThreeType(a, b) {
15550
15489
  }
15551
15490
 
15552
15491
  const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15492
+ const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
15553
15493
  function useUniform(name, value) {
15554
15494
  const store = useStore();
15495
+ const hmrVersion = useThree((s) => s._hmrVersion);
15555
15496
  return React.useMemo(() => {
15556
15497
  const state = store.getState();
15557
15498
  const set = store.setState;
15558
15499
  const existing = state.uniforms[name];
15559
15500
  if (existing && isUniformNode(existing)) {
15560
- if (value !== void 0) {
15561
- existing.value = value;
15501
+ if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
15502
+ existing.value = typeof value === "string" ? new webgpu.Color(value) : value;
15562
15503
  }
15563
15504
  return existing;
15564
15505
  }
@@ -15567,7 +15508,24 @@ function useUniform(name, value) {
15567
15508
  `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
15568
15509
  );
15569
15510
  }
15570
- const node = tsl.uniform(value);
15511
+ if (isUniformNode(value)) {
15512
+ const node2 = value;
15513
+ if (typeof node2.setName === "function") {
15514
+ node2.setName(name);
15515
+ }
15516
+ set((s) => ({
15517
+ uniforms: { ...s.uniforms, [name]: node2 }
15518
+ }));
15519
+ return node2;
15520
+ }
15521
+ let node;
15522
+ if (isTSLNode$1(value)) {
15523
+ node = tsl.uniform(value);
15524
+ } else if (typeof value === "string") {
15525
+ node = tsl.uniform(new webgpu.Color(value));
15526
+ } else {
15527
+ node = tsl.uniform(value);
15528
+ }
15571
15529
  if (typeof node.setName === "function") {
15572
15530
  node.setName(name);
15573
15531
  }
@@ -15578,12 +15536,12 @@ function useUniform(name, value) {
15578
15536
  }
15579
15537
  }));
15580
15538
  return node;
15581
- }, [store, name]);
15539
+ }, [store, name, hmrVersion]);
15582
15540
  }
15583
15541
 
15584
15542
  const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
15585
15543
  function useNodes(creatorOrScope, scope) {
15586
- const store = useStore();
15544
+ const store = usePrimaryStore();
15587
15545
  const removeNodes2 = React.useCallback(
15588
15546
  (names, targetScope) => {
15589
15547
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15640,8 +15598,11 @@ function useNodes(creatorOrScope, scope) {
15640
15598
  [store]
15641
15599
  );
15642
15600
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15643
- const storeNodes = useThree((s) => s.nodes);
15644
- const hmrVersion = useThree((s) => s._hmrVersion);
15601
+ const storeNodes = usePrimaryThree((s) => s.nodes);
15602
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15603
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15604
+ const readerDep = isReader ? storeNodes : null;
15605
+ const creatorDep = isReader ? null : hmrVersion;
15645
15606
  const nodes = React.useMemo(() => {
15646
15607
  if (creatorOrScope === void 0) {
15647
15608
  return storeNodes;
@@ -15654,11 +15615,7 @@ function useNodes(creatorOrScope, scope) {
15654
15615
  const state = store.getState();
15655
15616
  const set = store.setState;
15656
15617
  const creator = creatorOrScope;
15657
- const wrappedState = {
15658
- ...state,
15659
- uniforms: createScopedStore(state.uniforms),
15660
- nodes: createScopedStore(state.nodes)
15661
- };
15618
+ const wrappedState = createLazyCreatorState(state);
15662
15619
  const created = creator(wrappedState);
15663
15620
  const result = {};
15664
15621
  let hasNewNodes = false;
@@ -15668,7 +15625,7 @@ function useNodes(creatorOrScope, scope) {
15668
15625
  if (currentScope[name]) {
15669
15626
  result[name] = currentScope[name];
15670
15627
  } else {
15671
- if (typeof node.label === "function") node.setName(`${scope}.${name}`);
15628
+ node.setName?.(`${scope}.${name}`);
15672
15629
  result[name] = node;
15673
15630
  hasNewNodes = true;
15674
15631
  }
@@ -15688,7 +15645,7 @@ function useNodes(creatorOrScope, scope) {
15688
15645
  if (existing && isTSLNode(existing)) {
15689
15646
  result[name] = existing;
15690
15647
  } else {
15691
- if (typeof node.label === "function") node.setName(name);
15648
+ node.setName?.(name);
15692
15649
  result[name] = node;
15693
15650
  hasNewNodes = true;
15694
15651
  }
@@ -15697,18 +15654,11 @@ function useNodes(creatorOrScope, scope) {
15697
15654
  set((s) => ({ nodes: { ...s.nodes, ...result } }));
15698
15655
  }
15699
15656
  return result;
15700
- }, [
15701
- store,
15702
- typeof creatorOrScope === "string" ? creatorOrScope : scope,
15703
- // Only include storeNodes in deps for reader modes to enable reactivity
15704
- // Creator mode intentionally excludes it to avoid re-running creator on unrelated changes
15705
- isReader ? storeNodes : null,
15706
- // Include hmrVersion for creator modes to allow rebuildNodes() to bust the cache
15707
- isReader ? null : hmrVersion
15708
- ]);
15657
+ }, [store, scopeDep, readerDep, creatorDep]);
15709
15658
  return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
15710
15659
  }
15711
15660
  function rebuildAllNodes(store, scope) {
15661
+ store = store.getState().primaryStore ?? store;
15712
15662
  store.setState((state) => {
15713
15663
  let newNodes = state.nodes;
15714
15664
  if (scope && scope !== "root") {
@@ -15753,22 +15703,365 @@ function clearRootNodes(set) {
15753
15703
  });
15754
15704
  }
15755
15705
  function useLocalNodes(creator) {
15756
- const store = useStore();
15757
- const uniforms = useThree((s) => s.uniforms);
15758
- const nodes = useThree((s) => s.nodes);
15759
- const textures = useThree((s) => s.textures);
15706
+ const store = usePrimaryStore();
15707
+ const uniforms = usePrimaryThree((s) => s.uniforms);
15708
+ const nodes = usePrimaryThree((s) => s.nodes);
15709
+ const textures = usePrimaryThree((s) => s.textures);
15710
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15760
15711
  return React.useMemo(() => {
15761
- const state = store.getState();
15762
- const wrappedState = {
15763
- ...state,
15764
- uniforms: createScopedStore(state.uniforms),
15765
- nodes: createScopedStore(state.nodes)
15766
- };
15712
+ const wrappedState = createLazyCreatorState(store.getState());
15767
15713
  return creator(wrappedState);
15768
- }, [store, creator, uniforms, nodes, textures]);
15714
+ }, [store, creator, uniforms, nodes, textures, hmrVersion]);
15715
+ }
15716
+
15717
+ const isBufferLike = (value) => {
15718
+ if (value === null || typeof value !== "object") return false;
15719
+ if (ArrayBuffer.isView(value)) return true;
15720
+ if ("isBufferAttribute" in value) return true;
15721
+ if ("uuid" in value || "nodeType" in value) return true;
15722
+ return false;
15723
+ };
15724
+ const disposeBuffer = (buffer) => {
15725
+ if (buffer === null || typeof buffer !== "object") return;
15726
+ if ("dispose" in buffer && typeof buffer.dispose === "function") {
15727
+ buffer.dispose();
15728
+ }
15729
+ };
15730
+ function useBuffers(creatorOrScope, scope) {
15731
+ const store = usePrimaryStore();
15732
+ const removeBuffers = React.useCallback(
15733
+ (names, targetScope) => {
15734
+ const nameArray = Array.isArray(names) ? names : [names];
15735
+ store.setState((state) => {
15736
+ if (targetScope) {
15737
+ const currentScope = { ...state.buffers[targetScope] };
15738
+ for (const name of nameArray) delete currentScope[name];
15739
+ return { buffers: { ...state.buffers, [targetScope]: currentScope } };
15740
+ }
15741
+ const buffers2 = { ...state.buffers };
15742
+ for (const name of nameArray) if (isBufferLike(buffers2[name])) delete buffers2[name];
15743
+ return { buffers: buffers2 };
15744
+ });
15745
+ },
15746
+ [store]
15747
+ );
15748
+ const clearBuffers = React.useCallback(
15749
+ (targetScope) => {
15750
+ store.setState((state) => {
15751
+ if (targetScope && targetScope !== "root") {
15752
+ const { [targetScope]: _, ...rest } = state.buffers;
15753
+ return { buffers: rest };
15754
+ }
15755
+ if (targetScope === "root") {
15756
+ const buffers2 = {};
15757
+ for (const [key, value] of Object.entries(state.buffers)) {
15758
+ if (!isBufferLike(value)) buffers2[key] = value;
15759
+ }
15760
+ return { buffers: buffers2 };
15761
+ }
15762
+ return { buffers: {} };
15763
+ });
15764
+ },
15765
+ [store]
15766
+ );
15767
+ const rebuildBuffers = React.useCallback(
15768
+ (targetScope) => {
15769
+ store.setState((state) => {
15770
+ let newBuffers = state.buffers;
15771
+ if (targetScope && targetScope !== "root") {
15772
+ const { [targetScope]: _, ...rest } = state.buffers;
15773
+ newBuffers = rest;
15774
+ } else if (targetScope === "root") {
15775
+ newBuffers = {};
15776
+ for (const [key, value] of Object.entries(state.buffers)) {
15777
+ if (!isBufferLike(value)) newBuffers[key] = value;
15778
+ }
15779
+ } else {
15780
+ newBuffers = {};
15781
+ }
15782
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15783
+ });
15784
+ },
15785
+ [store]
15786
+ );
15787
+ const disposeBuffers = React.useCallback(
15788
+ (names, targetScope) => {
15789
+ const nameArray = Array.isArray(names) ? names : [names];
15790
+ const state = store.getState();
15791
+ for (const name of nameArray) {
15792
+ const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
15793
+ if (buffer && isBufferLike(buffer)) {
15794
+ disposeBuffer(buffer);
15795
+ }
15796
+ }
15797
+ removeBuffers(names, targetScope);
15798
+ },
15799
+ [store, removeBuffers]
15800
+ );
15801
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15802
+ const storeBuffers = usePrimaryThree((s) => s.buffers);
15803
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15804
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15805
+ const readerDep = isReader ? storeBuffers : null;
15806
+ const creatorDep = isReader ? null : hmrVersion;
15807
+ const buffers = React.useMemo(() => {
15808
+ if (creatorOrScope === void 0) {
15809
+ return storeBuffers;
15810
+ }
15811
+ if (typeof creatorOrScope === "string") {
15812
+ const scopeData = storeBuffers[creatorOrScope];
15813
+ if (scopeData && !isBufferLike(scopeData)) return scopeData;
15814
+ return {};
15815
+ }
15816
+ const state = store.getState();
15817
+ const set = store.setState;
15818
+ const creator = creatorOrScope;
15819
+ const wrappedState = createLazyCreatorState(state);
15820
+ const created = creator(wrappedState);
15821
+ const result = {};
15822
+ let hasNewBuffers = false;
15823
+ if (scope) {
15824
+ const currentScope = state.buffers[scope] ?? {};
15825
+ for (const [name, buffer] of Object.entries(created)) {
15826
+ if (currentScope[name]) {
15827
+ result[name] = currentScope[name];
15828
+ } else {
15829
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15830
+ buffer.setName(`${scope}.${name}`);
15831
+ }
15832
+ result[name] = buffer;
15833
+ hasNewBuffers = true;
15834
+ }
15835
+ }
15836
+ if (hasNewBuffers) {
15837
+ set((s) => ({
15838
+ buffers: {
15839
+ ...s.buffers,
15840
+ [scope]: { ...s.buffers[scope], ...result }
15841
+ }
15842
+ }));
15843
+ }
15844
+ return result;
15845
+ }
15846
+ for (const [name, buffer] of Object.entries(created)) {
15847
+ const existing = state.buffers[name];
15848
+ if (existing && isBufferLike(existing)) {
15849
+ result[name] = existing;
15850
+ } else {
15851
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15852
+ buffer.setName(name);
15853
+ }
15854
+ result[name] = buffer;
15855
+ hasNewBuffers = true;
15856
+ }
15857
+ }
15858
+ if (hasNewBuffers) {
15859
+ set((s) => ({ buffers: { ...s.buffers, ...result } }));
15860
+ }
15861
+ return result;
15862
+ }, [store, scopeDep, readerDep, creatorDep]);
15863
+ return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
15864
+ }
15865
+ function rebuildAllBuffers(store, scope) {
15866
+ store = store.getState().primaryStore ?? store;
15867
+ store.setState((state) => {
15868
+ let newBuffers = state.buffers;
15869
+ if (scope && scope !== "root") {
15870
+ const { [scope]: _, ...rest } = state.buffers;
15871
+ newBuffers = rest;
15872
+ } else if (scope === "root") {
15873
+ newBuffers = {};
15874
+ for (const [key, value] of Object.entries(state.buffers)) {
15875
+ if (!isBufferLike(value)) newBuffers[key] = value;
15876
+ }
15877
+ } else {
15878
+ newBuffers = {};
15879
+ }
15880
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15881
+ });
15882
+ }
15883
+
15884
+ const isStorageLike = (value) => {
15885
+ if (value === null || typeof value !== "object") return false;
15886
+ if ("isTexture" in value) return true;
15887
+ if ("isData3DTexture" in value) return true;
15888
+ if ("uuid" in value || "nodeType" in value) return true;
15889
+ return false;
15890
+ };
15891
+ const disposeStorage = (storage) => {
15892
+ if (storage === null || typeof storage !== "object") return;
15893
+ if ("dispose" in storage && typeof storage.dispose === "function") {
15894
+ storage.dispose();
15895
+ }
15896
+ };
15897
+ function useGPUStorage(creatorOrScope, scope) {
15898
+ const store = usePrimaryStore();
15899
+ const removeStorage = React.useCallback(
15900
+ (names, targetScope) => {
15901
+ const nameArray = Array.isArray(names) ? names : [names];
15902
+ store.setState((state) => {
15903
+ if (targetScope) {
15904
+ const currentScope = { ...state.gpuStorage[targetScope] };
15905
+ for (const name of nameArray) delete currentScope[name];
15906
+ return { gpuStorage: { ...state.gpuStorage, [targetScope]: currentScope } };
15907
+ }
15908
+ const gpuStorage2 = { ...state.gpuStorage };
15909
+ for (const name of nameArray) if (isStorageLike(gpuStorage2[name])) delete gpuStorage2[name];
15910
+ return { gpuStorage: gpuStorage2 };
15911
+ });
15912
+ },
15913
+ [store]
15914
+ );
15915
+ const clearStorage = React.useCallback(
15916
+ (targetScope) => {
15917
+ store.setState((state) => {
15918
+ if (targetScope && targetScope !== "root") {
15919
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15920
+ return { gpuStorage: rest };
15921
+ }
15922
+ if (targetScope === "root") {
15923
+ const gpuStorage2 = {};
15924
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15925
+ if (!isStorageLike(value)) gpuStorage2[key] = value;
15926
+ }
15927
+ return { gpuStorage: gpuStorage2 };
15928
+ }
15929
+ return { gpuStorage: {} };
15930
+ });
15931
+ },
15932
+ [store]
15933
+ );
15934
+ const rebuildStorage = React.useCallback(
15935
+ (targetScope) => {
15936
+ store.setState((state) => {
15937
+ let newStorage = state.gpuStorage;
15938
+ if (targetScope && targetScope !== "root") {
15939
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15940
+ newStorage = rest;
15941
+ } else if (targetScope === "root") {
15942
+ newStorage = {};
15943
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15944
+ if (!isStorageLike(value)) newStorage[key] = value;
15945
+ }
15946
+ } else {
15947
+ newStorage = {};
15948
+ }
15949
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
15950
+ });
15951
+ },
15952
+ [store]
15953
+ );
15954
+ const disposeStorageFn = React.useCallback(
15955
+ (names, targetScope) => {
15956
+ const nameArray = Array.isArray(names) ? names : [names];
15957
+ const state = store.getState();
15958
+ for (const name of nameArray) {
15959
+ const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
15960
+ if (storage && isStorageLike(storage)) {
15961
+ disposeStorage(storage);
15962
+ }
15963
+ }
15964
+ removeStorage(names, targetScope);
15965
+ },
15966
+ [store, removeStorage]
15967
+ );
15968
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15969
+ const storeStorage = usePrimaryThree((s) => s.gpuStorage);
15970
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15971
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15972
+ const readerDep = isReader ? storeStorage : null;
15973
+ const creatorDep = isReader ? null : hmrVersion;
15974
+ const gpuStorage = React.useMemo(() => {
15975
+ if (creatorOrScope === void 0) {
15976
+ return storeStorage;
15977
+ }
15978
+ if (typeof creatorOrScope === "string") {
15979
+ const scopeData = storeStorage[creatorOrScope];
15980
+ if (scopeData && !isStorageLike(scopeData)) return scopeData;
15981
+ return {};
15982
+ }
15983
+ const state = store.getState();
15984
+ const set = store.setState;
15985
+ const creator = creatorOrScope;
15986
+ const wrappedState = createLazyCreatorState(state);
15987
+ const created = creator(wrappedState);
15988
+ const result = {};
15989
+ let hasNewStorage = false;
15990
+ if (scope) {
15991
+ const currentScope = state.gpuStorage[scope] ?? {};
15992
+ for (const [name, storage] of Object.entries(created)) {
15993
+ if (currentScope[name]) {
15994
+ result[name] = currentScope[name];
15995
+ } else {
15996
+ if ("setName" in storage && typeof storage.setName === "function") {
15997
+ storage.setName(`${scope}.${name}`);
15998
+ }
15999
+ if ("name" in storage && typeof storage.name === "string") {
16000
+ storage.name = `${scope}.${name}`;
16001
+ }
16002
+ result[name] = storage;
16003
+ hasNewStorage = true;
16004
+ }
16005
+ }
16006
+ if (hasNewStorage) {
16007
+ set((s) => ({
16008
+ gpuStorage: {
16009
+ ...s.gpuStorage,
16010
+ [scope]: { ...s.gpuStorage[scope], ...result }
16011
+ }
16012
+ }));
16013
+ }
16014
+ return result;
16015
+ }
16016
+ for (const [name, storage] of Object.entries(created)) {
16017
+ const existing = state.gpuStorage[name];
16018
+ if (existing && isStorageLike(existing)) {
16019
+ result[name] = existing;
16020
+ } else {
16021
+ if ("setName" in storage && typeof storage.setName === "function") {
16022
+ storage.setName(name);
16023
+ }
16024
+ if ("name" in storage && typeof storage.name === "string") {
16025
+ storage.name = name;
16026
+ }
16027
+ result[name] = storage;
16028
+ hasNewStorage = true;
16029
+ }
16030
+ }
16031
+ if (hasNewStorage) {
16032
+ set((s) => ({ gpuStorage: { ...s.gpuStorage, ...result } }));
16033
+ }
16034
+ return result;
16035
+ }, [store, scopeDep, readerDep, creatorDep]);
16036
+ return {
16037
+ ...gpuStorage,
16038
+ removeStorage,
16039
+ clearStorage,
16040
+ rebuildStorage,
16041
+ disposeStorage: disposeStorageFn
16042
+ };
16043
+ }
16044
+ function rebuildAllStorage(store, scope) {
16045
+ store = store.getState().primaryStore ?? store;
16046
+ store.setState((state) => {
16047
+ let newStorage = state.gpuStorage;
16048
+ if (scope && scope !== "root") {
16049
+ const { [scope]: _, ...rest } = state.gpuStorage;
16050
+ newStorage = rest;
16051
+ } else if (scope === "root") {
16052
+ newStorage = {};
16053
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
16054
+ if (!isStorageLike(value)) newStorage[key] = value;
16055
+ }
16056
+ } else {
16057
+ newStorage = {};
16058
+ }
16059
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
16060
+ });
15769
16061
  }
15770
16062
 
15771
- function usePostProcessing(mainCB, setupCB) {
16063
+ const RenderPipelineCtor = webgpu.RenderPipeline ?? webgpu.PostProcessing;
16064
+ function useRenderPipeline(mainCB, setupCB) {
15772
16065
  const store = useStore();
15773
16066
  const { scene, camera, renderer, isLegacy } = useThree();
15774
16067
  const callbacksRanRef = React.useRef(false);
@@ -15787,7 +16080,7 @@ function usePostProcessing(mainCB, setupCB) {
15787
16080
  }, [store]);
15788
16081
  const reset = React.useCallback(() => {
15789
16082
  store.setState({
15790
- postProcessing: null,
16083
+ renderPipeline: null,
15791
16084
  passes: {}
15792
16085
  });
15793
16086
  callbacksRanRef.current = false;
@@ -15800,18 +16093,18 @@ function usePostProcessing(mainCB, setupCB) {
15800
16093
  }, []);
15801
16094
  React.useLayoutEffect(() => {
15802
16095
  if (isLegacy) {
15803
- throw new Error("usePostProcessing is only available with WebGPU renderer. Set renderer prop on Canvas.");
16096
+ throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
15804
16097
  }
15805
16098
  if (!renderer || !scene || !camera) return;
15806
16099
  const state = store.getState();
15807
16100
  const set = store.setState;
15808
16101
  try {
15809
- let pp = state.postProcessing;
16102
+ let pipeline = state.renderPipeline;
15810
16103
  let currentPasses = { ...state.passes };
15811
- let justCreatedPP = false;
15812
- if (!pp) {
15813
- pp = new webgpu.PostProcessing(renderer);
15814
- justCreatedPP = true;
16104
+ let justCreatedPipeline = false;
16105
+ if (!pipeline) {
16106
+ pipeline = new RenderPipelineCtor(renderer);
16107
+ justCreatedPipeline = true;
15815
16108
  }
15816
16109
  const cacheValid = scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
15817
16110
  let scenePass;
@@ -15822,9 +16115,9 @@ function usePostProcessing(mainCB, setupCB) {
15822
16115
  scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
15823
16116
  }
15824
16117
  currentPasses.scenePass = scenePass;
15825
- if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
15826
- set({ postProcessing: pp, passes: currentPasses });
15827
- const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
16118
+ if (!pipeline.outputNode || justCreatedPipeline) pipeline.outputNode = scenePass;
16119
+ set({ renderPipeline: pipeline, passes: currentPasses });
16120
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
15828
16121
  if (shouldRunCallbacks) {
15829
16122
  if (setupCBRef.current) {
15830
16123
  const freshState = store.getState();
@@ -15845,33 +16138,41 @@ function usePostProcessing(mainCB, setupCB) {
15845
16138
  callbacksRanRef.current = true;
15846
16139
  }
15847
16140
  } catch (error) {
15848
- console.error("[usePostProcessing] Setup error:", error);
16141
+ console.error("[useRenderPipeline] Setup error:", error);
15849
16142
  }
15850
16143
  }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
15851
16144
  const passes = useThree((s) => s.passes);
15852
- const postProcessing = useThree((s) => s.postProcessing);
16145
+ const renderPipeline = useThree((s) => s.renderPipeline);
15853
16146
  return {
15854
16147
  passes,
15855
- postProcessing,
16148
+ renderPipeline,
15856
16149
  clearPasses,
15857
16150
  reset,
15858
16151
  rebuild,
15859
- // isReady indicates if PostProcessing is configured and ready for rendering
15860
- isReady: postProcessing !== null
16152
+ // isReady indicates if RenderPipeline is configured and ready for rendering
16153
+ isReady: renderPipeline !== null
15861
16154
  };
15862
16155
  }
15863
16156
 
15864
16157
  extend(THREE);
15865
16158
 
16159
+ exports.Scheduler = scheduler.Scheduler;
16160
+ exports.getScheduler = scheduler.getScheduler;
15866
16161
  exports.Block = Block;
15867
16162
  exports.Canvas = Canvas;
16163
+ exports.Environment = Environment;
16164
+ exports.EnvironmentCube = EnvironmentCube;
16165
+ exports.EnvironmentMap = EnvironmentMap;
16166
+ exports.EnvironmentPortal = EnvironmentPortal;
15868
16167
  exports.ErrorBoundary = ErrorBoundary;
16168
+ exports.FROM_REF = FROM_REF;
15869
16169
  exports.IsObject = IsObject;
16170
+ exports.ONCE = ONCE;
16171
+ exports.Portal = Portal;
15870
16172
  exports.R3F_BUILD_LEGACY = R3F_BUILD_LEGACY;
15871
16173
  exports.R3F_BUILD_WEBGPU = R3F_BUILD_WEBGPU;
15872
16174
  exports.REACT_INTERNAL_PROPS = REACT_INTERNAL_PROPS;
15873
16175
  exports.RESERVED_PROPS = RESERVED_PROPS;
15874
- exports.Scheduler = Scheduler;
15875
16176
  exports.Texture = Texture;
15876
16177
  exports._roots = _roots;
15877
16178
  exports.act = act;
@@ -15902,35 +16203,49 @@ exports.events = createPointerEvents;
15902
16203
  exports.extend = extend;
15903
16204
  exports.findInitialRoot = findInitialRoot;
15904
16205
  exports.flushSync = flushSync;
16206
+ exports.fromRef = fromRef;
15905
16207
  exports.getInstanceProps = getInstanceProps;
16208
+ exports.getPrimary = getPrimary;
16209
+ exports.getPrimaryIds = getPrimaryIds;
15906
16210
  exports.getRootState = getRootState;
15907
- exports.getScheduler = getScheduler;
15908
16211
  exports.getUuidPrefix = getUuidPrefix;
15909
16212
  exports.hasConstructor = hasConstructor;
16213
+ exports.hasPrimary = hasPrimary;
15910
16214
  exports.invalidate = invalidate;
15911
16215
  exports.invalidateInstance = invalidateInstance;
15912
16216
  exports.is = is;
15913
16217
  exports.isColorRepresentation = isColorRepresentation;
15914
16218
  exports.isCopyable = isCopyable;
16219
+ exports.isFromRef = isFromRef;
15915
16220
  exports.isObject3D = isObject3D;
16221
+ exports.isOnce = isOnce;
15916
16222
  exports.isOrthographicCamera = isOrthographicCamera;
15917
16223
  exports.isRef = isRef;
15918
16224
  exports.isRenderer = isRenderer;
15919
16225
  exports.isTexture = isTexture;
15920
16226
  exports.isVectorLike = isVectorLike;
16227
+ exports.once = once;
15921
16228
  exports.prepare = prepare;
16229
+ exports.presetsObj = presetsObj;
16230
+ exports.rebuildAllBuffers = rebuildAllBuffers;
15922
16231
  exports.rebuildAllNodes = rebuildAllNodes;
16232
+ exports.rebuildAllStorage = rebuildAllStorage;
15923
16233
  exports.rebuildAllUniforms = rebuildAllUniforms;
15924
16234
  exports.reconciler = reconciler;
16235
+ exports.registerPrimary = registerPrimary;
15925
16236
  exports.removeInteractivity = removeInteractivity;
15926
16237
  exports.removeNodes = removeNodes;
15927
16238
  exports.removeUniforms = removeUniforms;
15928
16239
  exports.resolve = resolve;
15929
16240
  exports.unmountComponentAtNode = unmountComponentAtNode;
16241
+ exports.unregisterPrimary = unregisterPrimary;
15930
16242
  exports.updateCamera = updateCamera;
15931
16243
  exports.updateFrustum = updateFrustum;
15932
16244
  exports.useBridge = useBridge;
16245
+ exports.useBuffers = useBuffers;
16246
+ exports.useEnvironment = useEnvironment;
15933
16247
  exports.useFrame = useFrame;
16248
+ exports.useGPUStorage = useGPUStorage;
15934
16249
  exports.useGraph = useGraph;
15935
16250
  exports.useInstanceHandle = useInstanceHandle;
15936
16251
  exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
@@ -15938,7 +16253,7 @@ exports.useLoader = useLoader;
15938
16253
  exports.useLocalNodes = useLocalNodes;
15939
16254
  exports.useMutableCallback = useMutableCallback;
15940
16255
  exports.useNodes = useNodes;
15941
- exports.usePostProcessing = usePostProcessing;
16256
+ exports.useRenderPipeline = useRenderPipeline;
15942
16257
  exports.useRenderTarget = useRenderTarget;
15943
16258
  exports.useStore = useStore;
15944
16259
  exports.useTexture = useTexture;
@@ -15946,3 +16261,4 @@ exports.useTextures = useTextures;
15946
16261
  exports.useThree = useThree;
15947
16262
  exports.useUniform = useUniform;
15948
16263
  exports.useUniforms = useUniforms;
16264
+ exports.waitForPrimary = waitForPrimary;