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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = {
@@ -246,7 +636,8 @@ function prepare(target, root, type, props) {
246
636
  object,
247
637
  eventCount: 0,
248
638
  handlers: {},
249
- isHidden: false
639
+ isHidden: false,
640
+ deferredRefs: []
250
641
  };
251
642
  if (object) object.__r3f = instance;
252
643
  }
@@ -295,7 +686,7 @@ function createOcclusionObserverNode(store, uniform) {
295
686
  let occlusionSetupPromise = null;
296
687
  function enableOcclusion(store) {
297
688
  const state = store.getState();
298
- const { internal, renderer, rootScene } = state;
689
+ const { internal, renderer } = state;
299
690
  if (internal.occlusionEnabled || occlusionSetupPromise) return;
300
691
  const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
301
692
  if (!hasOcclusionSupport) {
@@ -458,6 +849,22 @@ function hasVisibilityHandlers(handlers) {
458
849
  return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
459
850
  }
460
851
 
852
+ const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
853
+ function fromRef(ref) {
854
+ return { [FROM_REF]: ref };
855
+ }
856
+ function isFromRef(value) {
857
+ return value !== null && typeof value === "object" && FROM_REF in value;
858
+ }
859
+
860
+ const ONCE = Symbol.for("@react-three/fiber.once");
861
+ function once(...args) {
862
+ return { [ONCE]: args.length ? args : true };
863
+ }
864
+ function isOnce(value) {
865
+ return value !== null && typeof value === "object" && ONCE in value;
866
+ }
867
+
461
868
  const RESERVED_PROPS = [
462
869
  "children",
463
870
  "key",
@@ -528,7 +935,7 @@ function getMemoizedPrototype(root) {
528
935
  ctor = new root.constructor();
529
936
  MEMOIZED_PROTOTYPES.set(root.constructor, ctor);
530
937
  }
531
- } catch (e) {
938
+ } catch {
532
939
  }
533
940
  return ctor;
534
941
  }
@@ -574,6 +981,25 @@ function applyProps(object, props) {
574
981
  continue;
575
982
  }
576
983
  if (value === void 0) continue;
984
+ if (isFromRef(value)) {
985
+ instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
986
+ continue;
987
+ }
988
+ if (isOnce(value)) {
989
+ if (instance?.appliedOnce?.has(prop)) continue;
990
+ if (instance) {
991
+ instance.appliedOnce ?? (instance.appliedOnce = /* @__PURE__ */ new Set());
992
+ instance.appliedOnce.add(prop);
993
+ }
994
+ const { root: targetRoot, key: targetKey } = resolve(object, prop);
995
+ const args = value[ONCE];
996
+ if (typeof targetRoot[targetKey] === "function") {
997
+ targetRoot[targetKey](...args === true ? [] : args);
998
+ } else if (args !== true && args.length > 0) {
999
+ targetRoot[targetKey] = args[0];
1000
+ }
1001
+ continue;
1002
+ }
577
1003
  let { root, key, target } = resolve(object, prop);
578
1004
  if (target === void 0 && (typeof root !== "object" || root === null)) {
579
1005
  throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`);
@@ -596,7 +1022,10 @@ function applyProps(object, props) {
596
1022
  else target.set(value);
597
1023
  } else {
598
1024
  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
1025
+ if (key.endsWith("Node") && root.isMaterial) {
1026
+ root.needsUpdate = true;
1027
+ }
1028
+ 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
1029
  root[key].format === webgpu.RGBAFormat && root[key].type === webgpu.UnsignedByteType) {
601
1030
  root[key].colorSpace = rootState.textureColorSpace;
602
1031
  }
@@ -629,38 +1058,60 @@ function applyProps(object, props) {
629
1058
  return object;
630
1059
  }
631
1060
 
1061
+ const DEFAULT_POINTER_ID = 0;
1062
+ const XR_POINTER_ID_START = 1e3;
1063
+ function getPointerState(internal, pointerId) {
1064
+ let state = internal.pointerMap.get(pointerId);
1065
+ if (!state) {
1066
+ state = {
1067
+ hovered: /* @__PURE__ */ new Map(),
1068
+ captured: /* @__PURE__ */ new Map(),
1069
+ initialClick: [0, 0],
1070
+ initialHits: []
1071
+ };
1072
+ internal.pointerMap.set(pointerId, state);
1073
+ }
1074
+ return state;
1075
+ }
1076
+ function getPointerId(event) {
1077
+ return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
1078
+ }
632
1079
  function makeId(event) {
633
1080
  return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
634
1081
  }
635
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
636
- const captureData = captures.get(obj);
1082
+ function releaseInternalPointerCapture(internal, obj, pointerId) {
1083
+ const pointerState = internal.pointerMap.get(pointerId);
1084
+ if (!pointerState) return;
1085
+ const captureData = pointerState.captured.get(obj);
637
1086
  if (captureData) {
638
- captures.delete(obj);
639
- if (captures.size === 0) {
640
- capturedMap.delete(pointerId);
641
- captureData.target.releasePointerCapture(pointerId);
642
- }
1087
+ pointerState.captured.delete(obj);
1088
+ captureData.target.releasePointerCapture(pointerId);
643
1089
  }
644
1090
  }
645
1091
  function removeInteractivity(store, object) {
646
1092
  const { internal } = store.getState();
647
1093
  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);
1094
+ for (const [pointerId, pointerState] of internal.pointerMap) {
1095
+ pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
1096
+ pointerState.hovered.forEach((value, key) => {
1097
+ if (value.eventObject === object || value.object === object) {
1098
+ pointerState.hovered.delete(key);
1099
+ }
1100
+ });
1101
+ if (pointerState.captured.has(object)) {
1102
+ releaseInternalPointerCapture(internal, object, pointerId);
652
1103
  }
653
- });
654
- internal.capturedMap.forEach((captures, pointerId) => {
655
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
656
- });
1104
+ }
657
1105
  unregisterVisibility(store, object);
658
1106
  }
659
1107
  function createEvents(store) {
660
- function calculateDistance(event) {
1108
+ function calculateDistance(event, pointerId) {
661
1109
  const { internal } = store.getState();
662
- const dx = event.offsetX - internal.initialClick[0];
663
- const dy = event.offsetY - internal.initialClick[1];
1110
+ const pointerState = internal.pointerMap.get(pointerId);
1111
+ if (!pointerState) return 0;
1112
+ const [initialX, initialY] = pointerState.initialClick;
1113
+ const dx = event.offsetX - initialX;
1114
+ const dy = event.offsetY - initialY;
664
1115
  return Math.round(Math.sqrt(dx * dx + dy * dy));
665
1116
  }
666
1117
  function filterPointerEvents(objects) {
@@ -696,6 +1147,15 @@ function createEvents(store) {
696
1147
  return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
697
1148
  }
698
1149
  let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
1150
+ const aInteractivePriority = a.object.userData?.interactivePriority;
1151
+ const bInteractivePriority = b.object.userData?.interactivePriority;
1152
+ if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
1153
+ if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
1154
+ if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
1155
+ if (aInteractivePriority !== bInteractivePriority) {
1156
+ return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
1157
+ }
1158
+ }
699
1159
  const aState = getRootState(a.object);
700
1160
  const bState = getRootState(b.object);
701
1161
  const aPriority = aState?.events?.priority ?? 1;
@@ -717,9 +1177,13 @@ function createEvents(store) {
717
1177
  eventObject = eventObject.parent;
718
1178
  }
719
1179
  }
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);
1180
+ if ("pointerId" in event) {
1181
+ const pointerId = event.pointerId;
1182
+ const pointerState = state.internal.pointerMap.get(pointerId);
1183
+ if (pointerState?.captured.size) {
1184
+ for (const captureData of pointerState.captured.values()) {
1185
+ if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1186
+ }
723
1187
  }
724
1188
  }
725
1189
  return intersections;
@@ -732,27 +1196,25 @@ function createEvents(store) {
732
1196
  if (state) {
733
1197
  const { raycaster, pointer, camera, internal } = state;
734
1198
  const unprojectedPoint = new webgpu.Vector3(pointer.x, pointer.y, 0).unproject(camera);
735
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1199
+ const hasPointerCapture = (id) => {
1200
+ const pointerState = internal.pointerMap.get(id);
1201
+ return pointerState?.captured.has(hit.eventObject) ?? false;
1202
+ };
736
1203
  const setPointerCapture = (id) => {
737
1204
  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
- }
1205
+ const pointerState = getPointerState(internal, id);
1206
+ pointerState.captured.set(hit.eventObject, captureData);
743
1207
  event.target.setPointerCapture(id);
744
1208
  };
745
1209
  const releasePointerCapture = (id) => {
746
- const captures = internal.capturedMap.get(id);
747
- if (captures) {
748
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
749
- }
1210
+ releaseInternalPointerCapture(internal, hit.eventObject, id);
750
1211
  };
751
1212
  const extractEventProps = {};
752
1213
  for (const prop in event) {
753
1214
  const property = event[prop];
754
1215
  if (typeof property !== "function") extractEventProps[prop] = property;
755
1216
  }
1217
+ const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
756
1218
  const raycastEvent = {
757
1219
  ...hit,
758
1220
  ...extractEventProps,
@@ -763,18 +1225,19 @@ function createEvents(store) {
763
1225
  unprojectedPoint,
764
1226
  ray: raycaster.ray,
765
1227
  camera,
1228
+ pointerId: eventPointerId,
766
1229
  // Hijack stopPropagation, which just sets a flag
767
1230
  stopPropagation() {
768
- const capturesForPointer = "pointerId" in event && internal.capturedMap.get(event.pointerId);
1231
+ const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
769
1232
  if (
770
1233
  // ...if this pointer hasn't been captured
771
- !capturesForPointer || // ... or if the hit object is capturing the pointer
772
- capturesForPointer.has(hit.eventObject)
1234
+ !pointerState?.captured.size || // ... or if the hit object is capturing the pointer
1235
+ pointerState.captured.has(hit.eventObject)
773
1236
  ) {
774
1237
  raycastEvent.stopped = localState.stopped = true;
775
- if (internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1238
+ if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
776
1239
  const higher = intersections.slice(0, intersections.indexOf(hit));
777
- cancelPointer([...higher, hit]);
1240
+ cancelPointer([...higher, hit], eventPointerId);
778
1241
  }
779
1242
  }
780
1243
  },
@@ -790,15 +1253,18 @@ function createEvents(store) {
790
1253
  }
791
1254
  return intersections;
792
1255
  }
793
- function cancelPointer(intersections) {
1256
+ function cancelPointer(intersections, pointerId) {
794
1257
  const { internal } = store.getState();
795
- for (const hoveredObj of internal.hovered.values()) {
1258
+ const pid = pointerId ?? DEFAULT_POINTER_ID;
1259
+ const pointerState = internal.pointerMap.get(pid);
1260
+ if (!pointerState) return;
1261
+ for (const [hoveredId, hoveredObj] of pointerState.hovered) {
796
1262
  if (!intersections.length || !intersections.find(
797
1263
  (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
798
1264
  )) {
799
1265
  const eventObject = hoveredObj.eventObject;
800
1266
  const instance = eventObject.__r3f;
801
- internal.hovered.delete(makeId(hoveredObj));
1267
+ pointerState.hovered.delete(hoveredId);
802
1268
  if (instance?.eventCount) {
803
1269
  const handlers = instance.handlers;
804
1270
  const data = { ...hoveredObj, intersections };
@@ -827,41 +1293,118 @@ function createEvents(store) {
827
1293
  instance?.handlers.onDropMissed?.(event);
828
1294
  }
829
1295
  }
1296
+ function cleanupPointer(pointerId) {
1297
+ const { internal } = store.getState();
1298
+ const pointerState = internal.pointerMap.get(pointerId);
1299
+ if (pointerState) {
1300
+ for (const [, hoveredObj] of pointerState.hovered) {
1301
+ const eventObject = hoveredObj.eventObject;
1302
+ const instance = eventObject.__r3f;
1303
+ if (instance?.eventCount) {
1304
+ const handlers = instance.handlers;
1305
+ const data = { ...hoveredObj, intersections: [] };
1306
+ handlers.onPointerOut?.(data);
1307
+ handlers.onPointerLeave?.(data);
1308
+ }
1309
+ }
1310
+ internal.pointerMap.delete(pointerId);
1311
+ }
1312
+ internal.pointerDirty.delete(pointerId);
1313
+ }
1314
+ function processDeferredPointer(event, pointerId) {
1315
+ const state = store.getState();
1316
+ const { internal } = state;
1317
+ if (!state.events.enabled) return;
1318
+ const filter = filterPointerEvents;
1319
+ const hits = intersect(event, filter);
1320
+ cancelPointer(hits, pointerId);
1321
+ function onIntersect(data) {
1322
+ const eventObject = data.eventObject;
1323
+ const instance = eventObject.__r3f;
1324
+ if (!instance?.eventCount) return;
1325
+ const handlers = instance.handlers;
1326
+ if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1327
+ const id = makeId(data);
1328
+ const pointerState = getPointerState(internal, pointerId);
1329
+ const hoveredItem = pointerState.hovered.get(id);
1330
+ if (!hoveredItem) {
1331
+ pointerState.hovered.set(id, data);
1332
+ handlers.onPointerOver?.(data);
1333
+ handlers.onPointerEnter?.(data);
1334
+ } else if (hoveredItem.stopped) {
1335
+ data.stopPropagation();
1336
+ }
1337
+ }
1338
+ handlers.onPointerMove?.(data);
1339
+ }
1340
+ handleIntersects(hits, event, 0, onIntersect);
1341
+ }
830
1342
  function handlePointer(name) {
831
1343
  switch (name) {
832
1344
  case "onPointerLeave":
833
- case "onPointerCancel":
834
1345
  case "onDragLeave":
835
1346
  return () => cancelPointer([]);
1347
+ // Global cancel of these events
1348
+ case "onPointerCancel":
1349
+ return (event) => {
1350
+ const pointerId = getPointerId(event);
1351
+ cleanupPointer(pointerId);
1352
+ };
836
1353
  case "onLostPointerCapture":
837
1354
  return (event) => {
838
1355
  const { internal } = store.getState();
839
- if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) {
1356
+ const pointerId = getPointerId(event);
1357
+ const pointerState = internal.pointerMap.get(pointerId);
1358
+ if (pointerState?.captured.size) {
840
1359
  requestAnimationFrame(() => {
841
- if (internal.capturedMap.has(event.pointerId)) {
842
- internal.capturedMap.delete(event.pointerId);
843
- cancelPointer([]);
1360
+ const pointerState2 = internal.pointerMap.get(pointerId);
1361
+ if (pointerState2?.captured.size) {
1362
+ pointerState2.captured.clear();
844
1363
  }
1364
+ cancelPointer([], pointerId);
845
1365
  });
846
1366
  }
847
1367
  };
848
1368
  }
849
1369
  return function handleEvent(event) {
850
1370
  const state = store.getState();
851
- const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
1371
+ const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
1372
+ const pointerId = getPointerId(event);
852
1373
  internal.lastEvent.current = event;
853
- if (!state.events.enabled) return;
1374
+ if (!events.enabled) return;
854
1375
  const isPointerMove = name === "onPointerMove";
855
1376
  const isDragOver = name === "onDragOver";
856
1377
  const isDrop = name === "onDrop";
857
1378
  const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
1379
+ const isPointerDown = name === "onPointerDown";
1380
+ const isPointerUp = name === "onPointerUp";
1381
+ const isWheel = name === "onWheel";
1382
+ const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
1383
+ if (isPointerMove && canDeferRaycasts) {
1384
+ events.compute?.(event, state);
1385
+ internal.pointerDirty.set(pointerId, event);
1386
+ return;
1387
+ }
1388
+ if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
1389
+ events.compute?.(event, state);
1390
+ internal.pointerDirty.set(pointerId, event);
1391
+ return;
1392
+ }
1393
+ if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
1394
+ const deferredEvent = internal.pointerDirty.get(pointerId);
1395
+ internal.pointerDirty.delete(pointerId);
1396
+ processDeferredPointer(deferredEvent, pointerId);
1397
+ }
858
1398
  const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
859
1399
  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
- }
1400
+ const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
1401
+ if (isPointerDown) {
1402
+ const pointerState2 = getPointerState(internal, pointerId);
1403
+ pointerState2.initialClick = [event.offsetX, event.offsetY];
1404
+ pointerState2.initialHits = hits.map((hit) => hit.eventObject);
1405
+ }
1406
+ const pointerState = internal.pointerMap.get(pointerId);
1407
+ const initialHits = pointerState?.initialHits ?? [];
865
1408
  if (isClickEvent && !hits.length) {
866
1409
  if (delta <= 2) {
867
1410
  pointerMissed(event, internal.interaction);
@@ -876,7 +1419,9 @@ function createEvents(store) {
876
1419
  dropMissed(event, internal.interaction);
877
1420
  if (onDropMissed) onDropMissed(event);
878
1421
  }
879
- if (isPointerMove || isDragOver) cancelPointer(hits);
1422
+ if (isPointerMove || isDragOver) {
1423
+ cancelPointer(hits, pointerId);
1424
+ }
880
1425
  function onIntersect(data) {
881
1426
  const eventObject = data.eventObject;
882
1427
  const instance = eventObject.__r3f;
@@ -885,9 +1430,10 @@ function createEvents(store) {
885
1430
  if (isPointerMove) {
886
1431
  if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
887
1432
  const id = makeId(data);
888
- const hoveredItem = internal.hovered.get(id);
1433
+ const pointerState2 = getPointerState(internal, pointerId);
1434
+ const hoveredItem = pointerState2.hovered.get(id);
889
1435
  if (!hoveredItem) {
890
- internal.hovered.set(id, data);
1436
+ pointerState2.hovered.set(id, data);
891
1437
  handlers.onPointerOver?.(data);
892
1438
  handlers.onPointerEnter?.(data);
893
1439
  } else if (hoveredItem.stopped) {
@@ -897,9 +1443,10 @@ function createEvents(store) {
897
1443
  handlers.onPointerMove?.(data);
898
1444
  } else if (isDragOver) {
899
1445
  const id = makeId(data);
900
- const hoveredItem = internal.hovered.get(id);
1446
+ const pointerState2 = getPointerState(internal, pointerId);
1447
+ const hoveredItem = pointerState2.hovered.get(id);
901
1448
  if (!hoveredItem) {
902
- internal.hovered.set(id, data);
1449
+ pointerState2.hovered.set(id, data);
903
1450
  handlers.onDragOverEnter?.(data);
904
1451
  } else if (hoveredItem.stopped) {
905
1452
  data.stopPropagation();
@@ -910,18 +1457,18 @@ function createEvents(store) {
910
1457
  } else {
911
1458
  const handler = handlers[name];
912
1459
  if (handler) {
913
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1460
+ if (!isClickEvent || initialHits.includes(eventObject)) {
914
1461
  pointerMissed(
915
1462
  event,
916
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1463
+ internal.interaction.filter((object) => !initialHits.includes(object))
917
1464
  );
918
1465
  handler(data);
919
1466
  }
920
1467
  } else {
921
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1468
+ if (isClickEvent && initialHits.includes(eventObject)) {
922
1469
  pointerMissed(
923
1470
  event,
924
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1471
+ internal.interaction.filter((object) => !initialHits.includes(object))
925
1472
  );
926
1473
  }
927
1474
  }
@@ -930,7 +1477,15 @@ function createEvents(store) {
930
1477
  handleIntersects(hits, event, delta, onIntersect);
931
1478
  };
932
1479
  }
933
- return { handlePointer };
1480
+ function flushDeferredPointers() {
1481
+ const { internal, events } = store.getState();
1482
+ if (!events.frameTimedRaycasts) return;
1483
+ for (const [pointerId, event] of internal.pointerDirty) {
1484
+ processDeferredPointer(event, pointerId);
1485
+ }
1486
+ internal.pointerDirty.clear();
1487
+ }
1488
+ return { handlePointer, flushDeferredPointers, processDeferredPointer };
934
1489
  }
935
1490
  const DOM_EVENTS = {
936
1491
  onClick: ["click", false],
@@ -949,11 +1504,16 @@ const DOM_EVENTS = {
949
1504
  onLostPointerCapture: ["lostpointercapture", true]
950
1505
  };
951
1506
  function createPointerEvents(store) {
952
- const { handlePointer } = createEvents(store);
1507
+ const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
1508
+ let nextXRPointerId = XR_POINTER_ID_START;
1509
+ const xrPointers = /* @__PURE__ */ new Map();
953
1510
  return {
954
1511
  priority: 1,
955
1512
  enabled: true,
956
- compute(event, state, previous) {
1513
+ frameTimedRaycasts: true,
1514
+ alwaysFireOnScroll: true,
1515
+ updateOnFrame: false,
1516
+ compute(event, state) {
957
1517
  state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
958
1518
  state.raycaster.setFromCamera(state.pointer, state.camera);
959
1519
  },
@@ -962,11 +1522,33 @@ function createPointerEvents(store) {
962
1522
  (acc, key) => ({ ...acc, [key]: handlePointer(key) }),
963
1523
  {}
964
1524
  ),
965
- update: () => {
1525
+ update: (pointerId) => {
966
1526
  const { events, internal } = store.getState();
967
- if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current);
1527
+ if (!events.handlers) return;
1528
+ if (pointerId !== void 0) {
1529
+ const event = internal.pointerDirty.get(pointerId);
1530
+ if (event) {
1531
+ internal.pointerDirty.delete(pointerId);
1532
+ processDeferredPointer(event, pointerId);
1533
+ } else if (internal.lastEvent?.current) {
1534
+ processDeferredPointer(internal.lastEvent.current, pointerId);
1535
+ }
1536
+ } else {
1537
+ flushDeferredPointers();
1538
+ if (internal.lastEvent?.current) {
1539
+ events.handlers.onPointerMove(internal.lastEvent.current);
1540
+ }
1541
+ }
1542
+ },
1543
+ flush: () => {
1544
+ const { events, internal } = store.getState();
1545
+ flushDeferredPointers();
1546
+ if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
1547
+ events.handlers.onPointerMove(internal.lastEvent.current);
1548
+ }
968
1549
  },
969
1550
  connect: (target) => {
1551
+ if (!target) return;
970
1552
  const { set, events } = store.getState();
971
1553
  events.disconnect?.();
972
1554
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -990,6 +1572,32 @@ function createPointerEvents(store) {
990
1572
  }
991
1573
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
992
1574
  }
1575
+ },
1576
+ registerPointer: (config) => {
1577
+ const pointerId = nextXRPointerId++;
1578
+ xrPointers.set(pointerId, config);
1579
+ const { internal } = store.getState();
1580
+ getPointerState(internal, pointerId);
1581
+ return pointerId;
1582
+ },
1583
+ unregisterPointer: (pointerId) => {
1584
+ xrPointers.delete(pointerId);
1585
+ const { internal } = store.getState();
1586
+ const pointerState = internal.pointerMap.get(pointerId);
1587
+ if (pointerState) {
1588
+ for (const [, hoveredObj] of pointerState.hovered) {
1589
+ const eventObject = hoveredObj.eventObject;
1590
+ const instance = eventObject.__r3f;
1591
+ if (instance?.eventCount) {
1592
+ const handlers = instance.handlers;
1593
+ const data = { ...hoveredObj, intersections: [] };
1594
+ handlers.onPointerOut?.(data);
1595
+ handlers.onPointerLeave?.(data);
1596
+ }
1597
+ }
1598
+ internal.pointerMap.delete(pointerId);
1599
+ }
1600
+ internal.pointerDirty.delete(pointerId);
993
1601
  }
994
1602
  };
995
1603
  }
@@ -1080,23 +1688,29 @@ const createStore = (invalidate, advance) => {
1080
1688
  set,
1081
1689
  get,
1082
1690
  // Mock objects that have to be configured
1691
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
1692
+ primaryStore: null,
1083
1693
  gl: null,
1084
1694
  renderer: null,
1085
1695
  camera: null,
1086
1696
  frustum: new webgpu.Frustum(),
1087
1697
  autoUpdateFrustum: true,
1088
1698
  raycaster: null,
1089
- events: { priority: 1, enabled: true, connected: false },
1699
+ events: {
1700
+ priority: 1,
1701
+ enabled: true,
1702
+ connected: false,
1703
+ frameTimedRaycasts: true,
1704
+ alwaysFireOnScroll: true,
1705
+ updateOnFrame: false
1706
+ },
1090
1707
  scene: null,
1091
1708
  rootScene: null,
1092
1709
  xr: null,
1093
1710
  inspector: null,
1094
1711
  invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
1095
1712
  advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
1096
- legacy: false,
1097
- linear: false,
1098
- flat: false,
1099
- textureColorSpace: "srgb",
1713
+ textureColorSpace: webgpu.SRGBColorSpace,
1100
1714
  isLegacy: false,
1101
1715
  webGPUSupported: false,
1102
1716
  isNative: false,
@@ -1154,6 +1768,7 @@ const createStore = (invalidate, advance) => {
1154
1768
  size: newSize,
1155
1769
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
1156
1770
  }));
1771
+ scheduler.getScheduler().invalidate();
1157
1772
  }
1158
1773
  }
1159
1774
  return;
@@ -1168,6 +1783,7 @@ const createStore = (invalidate, advance) => {
1168
1783
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
1169
1784
  _sizeImperative: true
1170
1785
  }));
1786
+ scheduler.getScheduler().invalidate();
1171
1787
  },
1172
1788
  setDpr: (dpr) => set((state2) => {
1173
1789
  const resolved = calculateDpr(dpr);
@@ -1178,11 +1794,14 @@ const createStore = (invalidate, advance) => {
1178
1794
  },
1179
1795
  setError: (error) => set(() => ({ error })),
1180
1796
  error: null,
1181
- //* TSL State (managed via hooks: useUniforms, useNodes, useTextures, usePostProcessing) ==============================
1797
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
1182
1798
  uniforms: {},
1183
1799
  nodes: {},
1800
+ buffers: {},
1801
+ gpuStorage: {},
1184
1802
  textures: /* @__PURE__ */ new Map(),
1185
- postProcessing: null,
1803
+ _textureRefs: /* @__PURE__ */ new Map(),
1804
+ renderPipeline: null,
1186
1805
  passes: {},
1187
1806
  _hmrVersion: 0,
1188
1807
  _sizeImperative: false,
@@ -1191,12 +1810,16 @@ const createStore = (invalidate, advance) => {
1191
1810
  internal: {
1192
1811
  // Events
1193
1812
  interaction: [],
1194
- hovered: /* @__PURE__ */ new Map(),
1195
1813
  subscribers: [],
1814
+ // Per-pointer state (new unified structure)
1815
+ pointerMap: /* @__PURE__ */ new Map(),
1816
+ pointerDirty: /* @__PURE__ */ new Map(),
1817
+ lastEvent: React__namespace.createRef(),
1818
+ // Deprecated but kept for backwards compatibility
1819
+ hovered: /* @__PURE__ */ new Map(),
1196
1820
  initialClick: [0, 0],
1197
1821
  initialHits: [],
1198
1822
  capturedMap: /* @__PURE__ */ new Map(),
1199
- lastEvent: React__namespace.createRef(),
1200
1823
  // Visibility tracking (onFramed, onOccluded, onVisible)
1201
1824
  visibilityRegistry: /* @__PURE__ */ new Map(),
1202
1825
  // Occlusion system (WebGPU only)
@@ -1279,13 +1902,22 @@ const createStore = (invalidate, advance) => {
1279
1902
  rootStore.subscribe(() => {
1280
1903
  const { camera, size, viewport, set, internal } = rootStore.getState();
1281
1904
  const actualRenderer = internal.actualRenderer;
1905
+ const canvasTarget = internal.canvasTarget;
1282
1906
  if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1283
1907
  oldSize = size;
1284
1908
  oldDpr = viewport.dpr;
1285
1909
  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);
1910
+ if (internal.isSecondary && canvasTarget) {
1911
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1912
+ canvasTarget.setSize(size.width, size.height, false);
1913
+ } else {
1914
+ if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1915
+ actualRenderer.setSize(size.width, size.height, false);
1916
+ if (canvasTarget) {
1917
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1918
+ canvasTarget.setSize(size.width, size.height, false);
1919
+ }
1920
+ }
1289
1921
  }
1290
1922
  if (camera !== oldCamera) {
1291
1923
  oldCamera = camera;
@@ -1356,1141 +1988,103 @@ useLoader.clear = function(loader, input) {
1356
1988
  };
1357
1989
  useLoader.loader = getLoader;
1358
1990
 
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;
1399
- } 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);
1458
- }
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;
1991
+ function useFrame(callback, priorityOrOptions) {
1992
+ const store = React__namespace.useContext(context);
1993
+ const isInsideCanvas = store !== null;
1994
+ const scheduler$1 = scheduler.getScheduler();
1995
+ const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
1996
+ id: priorityOrOptions.id,
1997
+ phase: priorityOrOptions.phase,
1998
+ priority: priorityOrOptions.priority,
1999
+ fps: priorityOrOptions.fps,
2000
+ drop: priorityOrOptions.drop,
2001
+ enabled: priorityOrOptions.enabled,
2002
+ before: priorityOrOptions.before,
2003
+ after: priorityOrOptions.after
2004
+ }) : "";
2005
+ const options = React__namespace.useMemo(() => {
2006
+ return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
2007
+ }, [optionsKey]);
2008
+ const reactId = React__namespace.useId();
2009
+ const id = options.id ?? reactId;
2010
+ const callbackRef = useMutableCallback(callback);
2011
+ const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
2012
+ useIsomorphicLayoutEffect(() => {
2013
+ if (!callback) return;
2014
+ if (isInsideCanvas) {
2015
+ const state = store.getState();
2016
+ const rootId = state.internal.rootId;
2017
+ if (isLegacyPriority) {
2018
+ state.internal.priority++;
2019
+ let parentRoot = state.previousRoot;
2020
+ while (parentRoot) {
2021
+ const parentState = parentRoot.getState();
2022
+ if (parentState?.internal) parentState.internal.priority++;
2023
+ parentRoot = parentState?.previousRoot;
2024
+ }
2025
+ notifyDepreciated({
2026
+ heading: "useFrame with numeric priority is deprecated",
2027
+ 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 })',
2028
+ link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2029
+ });
2030
+ }
2031
+ const wrappedCallback = (frameState, delta) => {
2032
+ const localState = store.getState();
2033
+ const mergedState = {
2034
+ ...localState,
2035
+ time: frameState.time,
2036
+ delta: frameState.delta,
2037
+ elapsed: frameState.elapsed,
2038
+ frame: frameState.frame
2039
+ };
2040
+ callbackRef.current?.(mergedState, delta);
2041
+ };
2042
+ const unregister = scheduler$1.register(wrappedCallback, {
2043
+ id,
2044
+ rootId,
2045
+ ...options
1517
2046
  });
1518
- sortedBuckets.push(bucket);
2047
+ return () => {
2048
+ unregister();
2049
+ if (isLegacyPriority) {
2050
+ const currentState = store.getState();
2051
+ if (currentState.internal) {
2052
+ currentState.internal.priority--;
2053
+ let parentRoot = currentState.previousRoot;
2054
+ while (parentRoot) {
2055
+ const parentState = parentRoot.getState();
2056
+ if (parentState?.internal) parentState.internal.priority--;
2057
+ parentRoot = parentState?.previousRoot;
2058
+ }
2059
+ }
2060
+ }
2061
+ };
2062
+ } else {
2063
+ return scheduler$1.register(
2064
+ (state, delta) => {
2065
+ const frameState = state;
2066
+ if (!frameState.renderer) return;
2067
+ callbackRef.current?.(frameState, delta);
2068
+ },
2069
+ { id, ...options }
2070
+ );
1519
2071
  }
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]);
2072
+ }, [store, scheduler$1, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2479
2073
  const isPaused = React__namespace.useSyncExternalStore(
2480
2074
  // Subscribe function
2481
2075
  React__namespace.useCallback(
2482
2076
  (onStoreChange) => {
2483
- return getScheduler().subscribeJobState(id, onStoreChange);
2077
+ return scheduler.getScheduler().subscribeJobState(id, onStoreChange);
2484
2078
  },
2485
2079
  [id]
2486
2080
  ),
2487
2081
  // getSnapshot function
2488
- React__namespace.useCallback(() => getScheduler().isJobPaused(id), [id]),
2082
+ React__namespace.useCallback(() => scheduler.getScheduler().isJobPaused(id), [id]),
2489
2083
  // getServerSnapshot function (SSR)
2490
2084
  React__namespace.useCallback(() => false, [])
2491
2085
  );
2492
2086
  const controls = React__namespace.useMemo(() => {
2493
- const scheduler2 = getScheduler();
2087
+ const scheduler2 = scheduler.getScheduler();
2494
2088
  return {
2495
2089
  /** The job's unique ID */
2496
2090
  id,
@@ -2505,7 +2099,7 @@ function useFrame(callback, priorityOrOptions) {
2505
2099
  * @param timestamp Optional timestamp (defaults to performance.now())
2506
2100
  */
2507
2101
  step: (timestamp) => {
2508
- getScheduler().stepJob(id, timestamp);
2102
+ scheduler.getScheduler().stepJob(id, timestamp);
2509
2103
  },
2510
2104
  /**
2511
2105
  * Manually step ALL jobs in the scheduler.
@@ -2513,20 +2107,20 @@ function useFrame(callback, priorityOrOptions) {
2513
2107
  * @param timestamp Optional timestamp (defaults to performance.now())
2514
2108
  */
2515
2109
  stepAll: (timestamp) => {
2516
- getScheduler().step(timestamp);
2110
+ scheduler.getScheduler().step(timestamp);
2517
2111
  },
2518
2112
  /**
2519
2113
  * Pause this job (set enabled=false).
2520
2114
  * Job remains registered but won't run.
2521
2115
  */
2522
2116
  pause: () => {
2523
- getScheduler().pauseJob(id);
2117
+ scheduler.getScheduler().pauseJob(id);
2524
2118
  },
2525
2119
  /**
2526
2120
  * Resume this job (set enabled=true).
2527
2121
  */
2528
2122
  resume: () => {
2529
- getScheduler().resumeJob(id);
2123
+ scheduler.getScheduler().resumeJob(id);
2530
2124
  },
2531
2125
  /**
2532
2126
  * Reactive paused state - automatically updates when pause/resume is called.
@@ -2564,22 +2158,29 @@ function buildFromCache(input, textureCache) {
2564
2158
  function useTexture(input, optionsOrOnLoad) {
2565
2159
  const renderer = useThree((state) => state.internal.actualRenderer);
2566
2160
  const store = useStore();
2567
- const textureCache = useThree((state) => state.textures);
2568
2161
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
2569
- const { onLoad, cache = false } = options;
2162
+ const { onLoad, cache = true } = options;
2163
+ const onLoadRef = React.useRef(onLoad);
2164
+ onLoadRef.current = onLoad;
2165
+ const onLoadCalledForRef = React.useRef(null);
2570
2166
  const urls = React.useMemo(() => getUrls(input), [input]);
2571
2167
  const cachedResult = React.useMemo(() => {
2572
2168
  if (!cache) return null;
2573
- if (!allUrlsCached(urls, textureCache)) return null;
2574
- return buildFromCache(input, textureCache);
2575
- }, [cache, urls, textureCache, input]);
2169
+ const textures = store.getState().textures;
2170
+ if (!allUrlsCached(urls, textures)) return null;
2171
+ return buildFromCache(input, textures);
2172
+ }, [cache, urls, input, store]);
2576
2173
  const loadedTextures = useLoader(
2577
2174
  webgpu.TextureLoader,
2578
2175
  IsObject(input) ? Object.values(input) : input
2579
2176
  );
2177
+ const inputKey = urls.join("\0");
2580
2178
  React.useLayoutEffect(() => {
2581
- if (!cachedResult) onLoad?.(loadedTextures);
2582
- }, [onLoad, cachedResult, loadedTextures]);
2179
+ if (cachedResult) return;
2180
+ if (onLoadCalledForRef.current === inputKey) return;
2181
+ onLoadCalledForRef.current = inputKey;
2182
+ onLoadRef.current?.(loadedTextures);
2183
+ }, [cachedResult, loadedTextures, inputKey]);
2583
2184
  React.useEffect(() => {
2584
2185
  if (cachedResult) return;
2585
2186
  if ("initTexture" in renderer) {
@@ -2612,8 +2213,6 @@ function useTexture(input, optionsOrOnLoad) {
2612
2213
  }, [input, loadedTextures, cachedResult]);
2613
2214
  React.useEffect(() => {
2614
2215
  if (!cache) return;
2615
- if (cachedResult) return;
2616
- const set = store.setState;
2617
2216
  const urlTextureMap = [];
2618
2217
  if (typeof input === "string") {
2619
2218
  urlTextureMap.push([input, mappedTextures]);
@@ -2627,18 +2226,32 @@ function useTexture(input, optionsOrOnLoad) {
2627
2226
  urlTextureMap.push([url, textureRecord[key]]);
2628
2227
  }
2629
2228
  }
2630
- set((state) => {
2631
- const newMap = new Map(state.textures);
2632
- let changed = false;
2229
+ store.setState((state) => {
2230
+ const refs = new Map(state._textureRefs);
2231
+ let textures = state.textures;
2232
+ let added = false;
2633
2233
  for (const [url, texture] of urlTextureMap) {
2634
- if (!newMap.has(url)) {
2635
- newMap.set(url, texture);
2636
- changed = true;
2234
+ if (!textures.has(url)) {
2235
+ if (!added) {
2236
+ textures = new Map(textures);
2237
+ added = true;
2238
+ }
2239
+ textures.set(url, texture);
2637
2240
  }
2241
+ refs.set(url, (refs.get(url) ?? 0) + 1);
2638
2242
  }
2639
- return changed ? { textures: newMap } : state;
2243
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
2244
+ });
2245
+ return () => store.setState((state) => {
2246
+ const refs = new Map(state._textureRefs);
2247
+ for (const [url] of urlTextureMap) {
2248
+ const next = (refs.get(url) ?? 0) - 1;
2249
+ if (next <= 0) refs.delete(url);
2250
+ else refs.set(url, next);
2251
+ }
2252
+ return { _textureRefs: refs };
2640
2253
  });
2641
- }, [cache, input, mappedTextures, store, cachedResult]);
2254
+ }, [cache, input, mappedTextures, store]);
2642
2255
  return mappedTextures;
2643
2256
  }
2644
2257
  useTexture.preload = (url) => useLoader.preload(webgpu.TextureLoader, url);
@@ -2654,106 +2267,90 @@ const Texture = ({
2654
2267
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children?.(ret) });
2655
2268
  };
2656
2269
 
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() {
2270
+ function useTextures(selector) {
2665
2271
  const store = useStore();
2666
- return React.useMemo(() => {
2667
- const set = store.setState;
2272
+ const registry = React.useMemo(() => {
2668
2273
  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
- };
2274
+ const setState = store.setState;
2275
+ const getOne = (key) => getState().textures.get(key);
2727
2276
  return {
2728
- // Getter for the textures Map (reactive via getState)
2729
- get textures() {
2277
+ get all() {
2730
2278
  return getState().textures;
2731
2279
  },
2732
- // Read
2733
- get: (key) => getState().textures.get(key),
2280
+ get(input) {
2281
+ if (typeof input === "string") return getOne(input);
2282
+ if (Array.isArray(input)) return input.map(getOne);
2283
+ const out = {};
2284
+ for (const name in input) out[name] = getOne(input[name]);
2285
+ return out;
2286
+ },
2734
2287
  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
2288
+ add(keyOrRecord, texture) {
2289
+ setState((state) => {
2290
+ const textures = new Map(state.textures);
2291
+ if (typeof keyOrRecord === "string") {
2292
+ textures.set(keyOrRecord, texture);
2293
+ } else {
2294
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2295
+ }
2296
+ return { textures };
2297
+ });
2298
+ },
2299
+ dispose(key, options) {
2300
+ const state = getState();
2301
+ const refs = state._textureRefs.get(key) ?? 0;
2302
+ if (refs > 0 && !options?.force) {
2303
+ console.warn(
2304
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2305
+ );
2306
+ return false;
2307
+ }
2308
+ state.textures.get(key)?.dispose();
2309
+ setState((s) => {
2310
+ const textures = new Map(s.textures);
2311
+ textures.delete(key);
2312
+ const nextRefs = new Map(s._textureRefs);
2313
+ nextRefs.delete(key);
2314
+ return { textures, _textureRefs: nextRefs };
2315
+ });
2316
+ return true;
2317
+ },
2318
+ disposeAll() {
2319
+ for (const texture of getState().textures.values()) texture.dispose();
2320
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2321
+ }
2745
2322
  };
2746
2323
  }, [store]);
2324
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2325
+ const selected = useThree(subscribe);
2326
+ return selector ? selected : registry;
2747
2327
  }
2748
2328
 
2749
- function useRenderTarget(width, height, options) {
2329
+ function useRenderTarget(widthOrOptions, heightOrOptions, options) {
2750
2330
  const isLegacy = useThree((s) => s.isLegacy);
2751
2331
  const size = useThree((s) => s.size);
2332
+ let width;
2333
+ let height;
2334
+ let opts;
2335
+ if (typeof widthOrOptions === "object") {
2336
+ opts = widthOrOptions;
2337
+ } else if (typeof widthOrOptions === "number") {
2338
+ width = widthOrOptions;
2339
+ if (typeof heightOrOptions === "object") {
2340
+ height = widthOrOptions;
2341
+ opts = heightOrOptions;
2342
+ } else if (typeof heightOrOptions === "number") {
2343
+ height = heightOrOptions;
2344
+ opts = options;
2345
+ } else {
2346
+ height = widthOrOptions;
2347
+ }
2348
+ }
2752
2349
  return React.useMemo(() => {
2753
2350
  const w = width ?? size.width;
2754
2351
  const h = height ?? size.height;
2755
- return new webgpu.RenderTarget(w, h, options);
2756
- }, [width, height, size.width, size.height, options, isLegacy]);
2352
+ return new webgpu.RenderTarget(w, h, opts);
2353
+ }, [width, height, size.width, size.height, opts, isLegacy]);
2757
2354
  }
2758
2355
 
2759
2356
  function useStore() {
@@ -2781,7 +2378,7 @@ function addEffect(callback) {
2781
2378
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2782
2379
  });
2783
2380
  const id = `legacy_effect_${effectId++}`;
2784
- return getScheduler().registerGlobal("before", id, callback);
2381
+ return scheduler.getScheduler().registerGlobal("before", id, callback);
2785
2382
  }
2786
2383
  function addAfterEffect(callback) {
2787
2384
  notifyDepreciated({
@@ -2790,7 +2387,7 @@ function addAfterEffect(callback) {
2790
2387
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2791
2388
  });
2792
2389
  const id = `legacy_afterEffect_${effectId++}`;
2793
- return getScheduler().registerGlobal("after", id, callback);
2390
+ return scheduler.getScheduler().registerGlobal("after", id, callback);
2794
2391
  }
2795
2392
  function addTail(callback) {
2796
2393
  notifyDepreciated({
@@ -2798,13 +2395,13 @@ function addTail(callback) {
2798
2395
  body: "Use scheduler.onIdle(callback) instead.\naddTail will be removed in a future version.",
2799
2396
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2800
2397
  });
2801
- return getScheduler().onIdle(callback);
2398
+ return scheduler.getScheduler().onIdle(callback);
2802
2399
  }
2803
2400
  function invalidate(state, frames = 1, stackFrames = false) {
2804
- getScheduler().invalidate(frames, stackFrames);
2401
+ scheduler.getScheduler().invalidate(frames, stackFrames);
2805
2402
  }
2806
- function advance(timestamp, runGlobalEffects = true, state, frame) {
2807
- getScheduler().step(timestamp);
2403
+ function advance(timestamp) {
2404
+ scheduler.getScheduler().step(timestamp);
2808
2405
  }
2809
2406
 
2810
2407
  const version = "10.0.0-alpha.2";
@@ -14257,6 +13854,7 @@ function swapInstances() {
14257
13854
  instance.object = instance.props.object ?? new target(...instance.props.args ?? []);
14258
13855
  instance.object.__r3f = instance;
14259
13856
  setFiberRef(fiber, instance.object);
13857
+ delete instance.appliedOnce;
14260
13858
  applyProps(instance.object, instance.props);
14261
13859
  if (instance.props.attach) {
14262
13860
  attach(parent, instance);
@@ -14330,8 +13928,22 @@ const reconciler = /* @__PURE__ */ createReconciler({
14330
13928
  const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
14331
13929
  if (isTailSibling) swapInstances();
14332
13930
  },
14333
- finalizeInitialChildren: () => false,
14334
- commitMount() {
13931
+ finalizeInitialChildren: (instance) => {
13932
+ for (const prop in instance.props) {
13933
+ if (isFromRef(instance.props[prop])) return true;
13934
+ }
13935
+ return false;
13936
+ },
13937
+ commitMount(instance) {
13938
+ const resolved = {};
13939
+ for (const prop in instance.props) {
13940
+ const value = instance.props[prop];
13941
+ if (isFromRef(value)) {
13942
+ const ref = value[FROM_REF];
13943
+ if (ref.current != null) resolved[prop] = ref.current;
13944
+ }
13945
+ }
13946
+ if (Object.keys(resolved).length) applyProps(instance.object, resolved);
14335
13947
  },
14336
13948
  getPublicInstance: (instance) => instance?.object,
14337
13949
  prepareForCommit: () => null,
@@ -14552,6 +14164,9 @@ function createRoot(canvas) {
14552
14164
  let resolve;
14553
14165
  pending = new Promise((_resolve) => resolve = _resolve);
14554
14166
  const {
14167
+ id: canvasId,
14168
+ primaryCanvas,
14169
+ scheduler: schedulerConfig,
14555
14170
  gl: glConfig,
14556
14171
  renderer: rendererConfig,
14557
14172
  size: propsSize,
@@ -14559,10 +14174,6 @@ function createRoot(canvas) {
14559
14174
  events,
14560
14175
  onCreated: onCreatedCallback,
14561
14176
  shadows = false,
14562
- linear = false,
14563
- flat = false,
14564
- textureColorSpace = webgpu.SRGBColorSpace,
14565
- legacy = false,
14566
14177
  orthographic = false,
14567
14178
  frameloop = "always",
14568
14179
  dpr = [1, 2],
@@ -14574,11 +14185,14 @@ function createRoot(canvas) {
14574
14185
  onDropMissed,
14575
14186
  autoUpdateFrustum = true,
14576
14187
  occlusion = false,
14577
- _sizeProps
14188
+ _sizeProps,
14189
+ forceEven
14578
14190
  } = props;
14191
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || webgpu.SRGBColorSpace;
14579
14192
  const state = store.getState();
14580
14193
  const defaultGPUProps = {
14581
- canvas
14194
+ canvas,
14195
+ antialias: true
14582
14196
  };
14583
14197
  if (glConfig && !R3F_BUILD_LEGACY) {
14584
14198
  throw new Error(
@@ -14589,15 +14203,52 @@ function createRoot(canvas) {
14589
14203
  throw new Error("Cannot use both gl and renderer props at the same time");
14590
14204
  }
14591
14205
  let renderer = state.internal.actualRenderer;
14592
- if (!state.internal.actualRenderer) {
14206
+ if (primaryCanvas && !state.internal.actualRenderer) {
14207
+ const primary = await waitForPrimary(primaryCanvas);
14208
+ renderer = primary.renderer;
14209
+ state.internal.actualRenderer = renderer;
14210
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14211
+ primary.store.setState((prev) => ({
14212
+ internal: { ...prev.internal, isMultiCanvas: true }
14213
+ }));
14214
+ state.set((prev) => ({
14215
+ webGPUSupported: primary.store.getState().webGPUSupported,
14216
+ renderer,
14217
+ primaryStore: primary.store,
14218
+ internal: {
14219
+ ...prev.internal,
14220
+ canvasTarget,
14221
+ isMultiCanvas: true,
14222
+ isSecondary: true,
14223
+ targetId: primaryCanvas
14224
+ }
14225
+ }));
14226
+ } else if (!state.internal.actualRenderer) {
14593
14227
  renderer = await resolveRenderer(rendererConfig, defaultGPUProps, webgpu.WebGPURenderer);
14594
14228
  if (!renderer.hasInitialized?.()) {
14229
+ const size2 = computeInitialSize(canvas, propsSize);
14230
+ if (size2.width > 0 && size2.height > 0) {
14231
+ const pixelRatio = calculateDpr(dpr);
14232
+ canvas.width = size2.width * pixelRatio;
14233
+ canvas.height = size2.height * pixelRatio;
14234
+ }
14595
14235
  await renderer.init();
14596
14236
  }
14597
14237
  const backend = renderer.backend;
14598
14238
  const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14599
14239
  state.internal.actualRenderer = renderer;
14600
- state.set({ webGPUSupported: isWebGPUBackend, renderer });
14240
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14241
+ if (canvasId && !state.internal.isSecondary) {
14242
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14243
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14244
+ state.set((prev) => ({
14245
+ internal: {
14246
+ ...prev.internal,
14247
+ canvasTarget,
14248
+ unregisterPrimary
14249
+ }
14250
+ }));
14251
+ }
14601
14252
  }
14602
14253
  let raycaster = state.raycaster;
14603
14254
  if (!raycaster) state.set({ raycaster: raycaster = new webgpu.Raycaster() });
@@ -14606,6 +14257,7 @@ function createRoot(canvas) {
14606
14257
  if (!is.equ(params, raycaster.params, shallowLoose)) {
14607
14258
  applyProps(raycaster, { params: { ...raycaster.params, ...params } });
14608
14259
  }
14260
+ let tempCamera = state.camera;
14609
14261
  if (!state.camera || state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose)) {
14610
14262
  lastCamera = cameraOptions;
14611
14263
  const isCamera = cameraOptions?.isCamera;
@@ -14625,6 +14277,7 @@ function createRoot(canvas) {
14625
14277
  if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0);
14626
14278
  }
14627
14279
  state.set({ camera });
14280
+ tempCamera = camera;
14628
14281
  raycaster.camera = camera;
14629
14282
  }
14630
14283
  if (!state.scene) {
@@ -14642,7 +14295,7 @@ function createRoot(canvas) {
14642
14295
  rootScene: scene,
14643
14296
  internal: { ...prev.internal, container: scene }
14644
14297
  }));
14645
- const camera = state.camera;
14298
+ const camera = tempCamera;
14646
14299
  if (camera && !camera.parent) scene.add(camera);
14647
14300
  }
14648
14301
  if (events && !state.events.handlers) {
@@ -14659,6 +14312,9 @@ function createRoot(canvas) {
14659
14312
  if (_sizeProps !== void 0) {
14660
14313
  state.set({ _sizeProps });
14661
14314
  }
14315
+ if (forceEven !== void 0 && state.internal.forceEven !== forceEven) {
14316
+ state.set((prev) => ({ internal: { ...prev.internal, forceEven } }));
14317
+ }
14662
14318
  const size = computeInitialSize(canvas, propsSize);
14663
14319
  if (!state._sizeImperative && !is.equ(size, state.size, shallowLoose)) {
14664
14320
  const wasImperative = state._sizeImperative;
@@ -14685,10 +14341,10 @@ function createRoot(canvas) {
14685
14341
  lastConfiguredProps.performance = performance;
14686
14342
  }
14687
14343
  if (!state.xr) {
14688
- const handleXRFrame = (timestamp, frame) => {
14344
+ const handleXRFrame = (timestamp, _frame) => {
14689
14345
  const state2 = store.getState();
14690
14346
  if (state2.frameloop === "never") return;
14691
- advance(timestamp, true);
14347
+ advance(timestamp);
14692
14348
  };
14693
14349
  const actualRenderer = state.internal.actualRenderer;
14694
14350
  const handleSessionChange = () => {
@@ -14700,16 +14356,16 @@ function createRoot(canvas) {
14700
14356
  };
14701
14357
  const xr = {
14702
14358
  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);
14359
+ const { gl, renderer: renderer2 } = store.getState();
14360
+ const xrManager = (renderer2 || gl).xr;
14361
+ xrManager.addEventListener("sessionstart", handleSessionChange);
14362
+ xrManager.addEventListener("sessionend", handleSessionChange);
14707
14363
  },
14708
14364
  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);
14365
+ const { gl, renderer: renderer2 } = store.getState();
14366
+ const xrManager = (renderer2 || gl).xr;
14367
+ xrManager.removeEventListener("sessionstart", handleSessionChange);
14368
+ xrManager.removeEventListener("sessionend", handleSessionChange);
14713
14369
  }
14714
14370
  };
14715
14371
  if (typeof renderer.xr?.addEventListener === "function") xr.connect();
@@ -14721,15 +14377,22 @@ function createRoot(canvas) {
14721
14377
  const oldType = renderer.shadowMap.type;
14722
14378
  renderer.shadowMap.enabled = !!shadows;
14723
14379
  if (is.boo(shadows)) {
14724
- renderer.shadowMap.type = webgpu.PCFSoftShadowMap;
14380
+ renderer.shadowMap.type = webgpu.PCFShadowMap;
14725
14381
  } else if (is.str(shadows)) {
14382
+ if (shadows === "soft") {
14383
+ notifyDepreciated({
14384
+ heading: 'shadows="soft" is deprecated',
14385
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14386
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14387
+ });
14388
+ }
14726
14389
  const types = {
14727
14390
  basic: webgpu.BasicShadowMap,
14728
14391
  percentage: webgpu.PCFShadowMap,
14729
- soft: webgpu.PCFSoftShadowMap,
14392
+ soft: webgpu.PCFShadowMap,
14730
14393
  variance: webgpu.VSMShadowMap
14731
14394
  };
14732
- renderer.shadowMap.type = types[shadows] ?? webgpu.PCFSoftShadowMap;
14395
+ renderer.shadowMap.type = types[shadows] ?? webgpu.PCFShadowMap;
14733
14396
  } else if (is.obj(shadows)) {
14734
14397
  Object.assign(renderer.shadowMap, shadows);
14735
14398
  }
@@ -14737,28 +14400,70 @@ function createRoot(canvas) {
14737
14400
  renderer.shadowMap.needsUpdate = true;
14738
14401
  }
14739
14402
  }
14403
+ if (!configured) {
14404
+ renderer.outputColorSpace = webgpu.SRGBColorSpace;
14405
+ renderer.toneMapping = webgpu.ACESFilmicToneMapping;
14406
+ }
14740
14407
  if (textureColorSpace !== lastConfiguredProps.textureColorSpace) {
14741
14408
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
14742
14409
  lastConfiguredProps.textureColorSpace = textureColorSpace;
14743
14410
  }
14411
+ const r3fProps = ["textureColorSpace"];
14412
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14413
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
14744
14414
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
14745
- applyProps(renderer, glConfig);
14415
+ const glProps = {};
14416
+ for (const key in glConfig) {
14417
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14418
+ }
14419
+ applyProps(renderer, glProps);
14746
14420
  }
14747
14421
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
14748
14422
  const currentRenderer = state.renderer;
14749
14423
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
14750
- applyProps(currentRenderer, rendererConfig);
14424
+ const rendererProps = {};
14425
+ for (const key in rendererConfig) {
14426
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14427
+ }
14428
+ applyProps(currentRenderer, rendererProps);
14751
14429
  }
14752
14430
  }
14753
- const scheduler = getScheduler();
14431
+ const scheduler$1 = scheduler.getScheduler();
14754
14432
  const rootId = state.internal.rootId;
14755
14433
  if (!rootId) {
14756
- const newRootId = scheduler.generateRootId();
14757
- const unregisterRoot = scheduler.registerRoot(newRootId, {
14434
+ const newRootId = canvasId || scheduler$1.generateRootId();
14435
+ const unregisterRoot = scheduler$1.registerRoot(newRootId, {
14758
14436
  getState: () => store.getState(),
14759
14437
  onError: (err) => store.getState().setError(err)
14760
14438
  });
14761
- const unregisterFrustum = scheduler.register(
14439
+ const unregisterCanvasTarget = scheduler$1.register(
14440
+ () => {
14441
+ const state2 = store.getState();
14442
+ if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14443
+ const renderer2 = state2.internal.actualRenderer;
14444
+ renderer2.setCanvasTarget(state2.internal.canvasTarget);
14445
+ }
14446
+ },
14447
+ {
14448
+ id: `${newRootId}_canvasTarget`,
14449
+ rootId: newRootId,
14450
+ phase: "start",
14451
+ system: true
14452
+ }
14453
+ );
14454
+ const unregisterEventsFlush = scheduler$1.register(
14455
+ () => {
14456
+ const state2 = store.getState();
14457
+ state2.events.flush?.();
14458
+ },
14459
+ {
14460
+ id: `${newRootId}_events`,
14461
+ rootId: newRootId,
14462
+ phase: "input",
14463
+ system: true
14464
+ }
14465
+ );
14466
+ const unregisterFrustum = scheduler$1.register(
14762
14467
  () => {
14763
14468
  const state2 = store.getState();
14764
14469
  if (state2.autoUpdateFrustum && state2.camera) {
@@ -14768,11 +14473,11 @@ function createRoot(canvas) {
14768
14473
  {
14769
14474
  id: `${newRootId}_frustum`,
14770
14475
  rootId: newRootId,
14771
- phase: "preRender",
14476
+ before: "render",
14772
14477
  system: true
14773
14478
  }
14774
14479
  );
14775
- const unregisterVisibility = scheduler.register(
14480
+ const unregisterVisibility = scheduler$1.register(
14776
14481
  () => {
14777
14482
  const state2 = store.getState();
14778
14483
  checkVisibility(state2);
@@ -14780,30 +14485,34 @@ function createRoot(canvas) {
14780
14485
  {
14781
14486
  id: `${newRootId}_visibility`,
14782
14487
  rootId: newRootId,
14783
- phase: "preRender",
14488
+ before: "render",
14784
14489
  system: true,
14785
14490
  after: `${newRootId}_frustum`
14786
14491
  }
14787
14492
  );
14788
- const unregisterRender = scheduler.register(
14493
+ const unregisterRender = scheduler$1.register(
14789
14494
  () => {
14790
14495
  const state2 = store.getState();
14791
14496
  const renderer2 = state2.internal.actualRenderer;
14792
- const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14497
+ const userHandlesRender = scheduler$1.hasUserJobsInPhase("render", newRootId);
14793
14498
  if (userHandlesRender || state2.internal.priority) return;
14794
14499
  try {
14795
- if (state2.postProcessing?.render) state2.postProcessing.render();
14500
+ if (state2.renderPipeline?.render) state2.renderPipeline.render();
14796
14501
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
14797
14502
  } catch (error) {
14798
14503
  state2.setError(error instanceof Error ? error : new Error(String(error)));
14799
14504
  }
14800
14505
  },
14801
14506
  {
14802
- id: `${newRootId}_render`,
14507
+ // Use canvas ID directly as job ID if available, otherwise use generated rootId
14508
+ id: canvasId || `${newRootId}_render`,
14803
14509
  rootId: newRootId,
14804
14510
  phase: "render",
14805
- system: true
14511
+ system: true,
14806
14512
  // Internal flag: this is a system job, not user-controlled
14513
+ // Apply scheduler config for render ordering and rate limiting
14514
+ ...schedulerConfig?.after && { after: schedulerConfig.after },
14515
+ ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14807
14516
  }
14808
14517
  );
14809
14518
  state.set((state2) => ({
@@ -14812,15 +14521,17 @@ function createRoot(canvas) {
14812
14521
  rootId: newRootId,
14813
14522
  unregisterRoot: () => {
14814
14523
  unregisterRoot();
14524
+ unregisterCanvasTarget();
14525
+ unregisterEventsFlush();
14815
14526
  unregisterFrustum();
14816
14527
  unregisterVisibility();
14817
14528
  unregisterRender();
14818
14529
  },
14819
- scheduler
14530
+ scheduler: scheduler$1
14820
14531
  }
14821
14532
  }));
14822
14533
  }
14823
- scheduler.frameloop = frameloop;
14534
+ scheduler$1.frameloop = frameloop;
14824
14535
  onCreated = onCreatedCallback;
14825
14536
  configured = true;
14826
14537
  resolve();
@@ -14870,15 +14581,24 @@ function unmountComponentAtNode(canvas, callback) {
14870
14581
  const renderer = state.internal.actualRenderer;
14871
14582
  const unregisterRoot = state.internal.unregisterRoot;
14872
14583
  if (unregisterRoot) unregisterRoot();
14584
+ const unregisterPrimary = state.internal.unregisterPrimary;
14585
+ if (unregisterPrimary) unregisterPrimary();
14586
+ const canvasTarget = state.internal.canvasTarget;
14587
+ if (canvasTarget?.dispose) canvasTarget.dispose();
14873
14588
  state.events.disconnect?.();
14874
14589
  cleanupHelperGroup(root.store);
14875
- renderer?.renderLists?.dispose?.();
14876
- renderer?.forceContextLoss?.();
14877
- if (renderer?.xr) state.xr.disconnect();
14590
+ if (state.isLegacy && renderer) {
14591
+ ;
14592
+ renderer.renderLists?.dispose?.();
14593
+ renderer.forceContextLoss?.();
14594
+ }
14595
+ if (!state.internal.isSecondary) {
14596
+ if (renderer?.xr) state.xr.disconnect();
14597
+ }
14878
14598
  dispose(state.scene);
14879
14599
  _roots.delete(canvas);
14880
14600
  if (callback) callback(canvas);
14881
- } catch (e) {
14601
+ } catch {
14882
14602
  }
14883
14603
  }, 500);
14884
14604
  }
@@ -14886,36 +14606,34 @@ function unmountComponentAtNode(canvas, callback) {
14886
14606
  }
14887
14607
  }
14888
14608
  function createPortal(children, container, state) {
14889
- return /* @__PURE__ */ jsxRuntime.jsx(PortalWrapper, { children, container, state });
14609
+ return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children, container, state });
14890
14610
  }
14891
- function PortalWrapper({ children, container, state }) {
14611
+ function Portal({ children, container, state }) {
14892
14612
  const isRef = React.useCallback((obj) => obj && "current" in obj, []);
14893
- const [resolvedContainer, setResolvedContainer] = React.useState(() => {
14613
+ const [resolvedContainer, _setResolvedContainer] = React.useState(() => {
14894
14614
  if (isRef(container)) return container.current ?? null;
14895
14615
  return container;
14896
14616
  });
14617
+ const setResolvedContainer = React.useCallback(
14618
+ (newContainer) => {
14619
+ if (!newContainer || newContainer === resolvedContainer) return;
14620
+ _setResolvedContainer(isRef(newContainer) ? newContainer.current : newContainer);
14621
+ },
14622
+ [resolvedContainer, _setResolvedContainer, isRef]
14623
+ );
14897
14624
  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);
14625
+ if (isRef(container) && !container.current) {
14626
+ return queueMicrotask(() => {
14627
+ setResolvedContainer(container.current);
14628
+ });
14912
14629
  }
14913
- }, [container, resolvedContainer, isRef]);
14630
+ setResolvedContainer(container);
14631
+ }, [container, isRef, setResolvedContainer]);
14914
14632
  if (!resolvedContainer) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {});
14915
14633
  const portalKey = resolvedContainer.uuid ?? `portal-${resolvedContainer.id ?? "unknown"}`;
14916
- return /* @__PURE__ */ jsxRuntime.jsx(Portal, { children, container: resolvedContainer, state }, portalKey);
14634
+ return /* @__PURE__ */ jsxRuntime.jsx(PortalInner, { children, container: resolvedContainer, state }, portalKey);
14917
14635
  }
14918
- function Portal({ state = {}, children, container }) {
14636
+ function PortalInner({ state = {}, children, container }) {
14919
14637
  const { events, size, injectScene = true, ...rest } = state;
14920
14638
  const previousRoot = useStore();
14921
14639
  const [raycaster] = React.useState(() => new webgpu.Raycaster());
@@ -14936,11 +14654,12 @@ function Portal({ state = {}, children, container }) {
14936
14654
  };
14937
14655
  }, [portalScene, container, injectScene]);
14938
14656
  const inject = useMutableCallback((rootState, injectState) => {
14657
+ const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
14939
14658
  let viewport = void 0;
14940
- if (injectState.camera && size) {
14659
+ if (injectState.camera && (size || injectState.size)) {
14941
14660
  const camera = injectState.camera;
14942
- viewport = rootState.viewport.getCurrentViewport(camera, new webgpu.Vector3(), size);
14943
- if (camera !== rootState.camera) updateCamera(camera, size);
14661
+ viewport = rootState.viewport.getCurrentViewport(camera, new webgpu.Vector3(), resolvedSize);
14662
+ if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
14944
14663
  }
14945
14664
  return {
14946
14665
  // The intersect consists of the previous root state
@@ -14957,7 +14676,7 @@ function Portal({ state = {}, children, container }) {
14957
14676
  previousRoot,
14958
14677
  // Events, size and viewport can be overridden by the inject layer
14959
14678
  events: { ...rootState.events, ...injectState.events, ...events },
14960
- size: { ...rootState.size, ...size },
14679
+ size: resolvedSize,
14961
14680
  viewport: { ...rootState.viewport, ...viewport },
14962
14681
  // Layers are allowed to override events
14963
14682
  setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
@@ -14969,9 +14688,13 @@ function Portal({ state = {}, children, container }) {
14969
14688
  const store = traditional.createWithEqualityFn((set, get) => ({ ...rest, set, get }));
14970
14689
  const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
14971
14690
  onMutate(previousRoot.getState());
14972
- previousRoot.subscribe(onMutate);
14973
14691
  return store;
14974
14692
  }, [previousRoot, container]);
14693
+ useIsomorphicLayoutEffect(() => {
14694
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14695
+ const unsubscribe = previousRoot.subscribe(onMutate);
14696
+ return unsubscribe;
14697
+ }, [previousRoot, usePortalStore]);
14975
14698
  return (
14976
14699
  // @ts-ignore, reconciler types are not maintained
14977
14700
  /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reconciler.createPortal(
@@ -14985,21 +14708,59 @@ function flushSync(fn) {
14985
14708
  return reconciler.flushSyncFromReconciler(fn);
14986
14709
  }
14987
14710
 
14711
+ function parseBackground(background) {
14712
+ if (!background) return null;
14713
+ if (typeof background === "object" && !background.isColor) {
14714
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14715
+ return {
14716
+ ...rest,
14717
+ preset,
14718
+ files: envMap || files,
14719
+ backgroundFiles: backgroundMap,
14720
+ background: true
14721
+ };
14722
+ }
14723
+ if (typeof background === "number") {
14724
+ return { color: background, background: true };
14725
+ }
14726
+ if (typeof background === "string") {
14727
+ if (background in presetsObj) {
14728
+ return { preset: background, background: true };
14729
+ }
14730
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14731
+ return { files: background, background: true };
14732
+ }
14733
+ return { color: background, background: true };
14734
+ }
14735
+ if (background.isColor) {
14736
+ return { color: background, background: true };
14737
+ }
14738
+ return null;
14739
+ }
14740
+
14741
+ function clearHmrCaches(store) {
14742
+ store.setState((state) => ({
14743
+ nodes: {},
14744
+ uniforms: {},
14745
+ buffers: {},
14746
+ gpuStorage: {},
14747
+ _hmrVersion: state._hmrVersion + 1
14748
+ }));
14749
+ }
14750
+
14988
14751
  function CanvasImpl({
14989
14752
  ref,
14990
14753
  children,
14991
14754
  fallback,
14992
14755
  resize,
14993
14756
  style,
14757
+ id,
14994
14758
  gl,
14995
- renderer,
14759
+ renderer: rendererProp,
14996
14760
  events = createPointerEvents,
14997
14761
  eventSource,
14998
14762
  eventPrefix,
14999
14763
  shadows,
15000
- linear,
15001
- flat,
15002
- legacy,
15003
14764
  orthographic,
15004
14765
  frameloop,
15005
14766
  dpr,
@@ -15007,6 +14768,8 @@ function CanvasImpl({
15007
14768
  raycaster,
15008
14769
  camera,
15009
14770
  scene,
14771
+ autoUpdateFrustum,
14772
+ occlusion,
15010
14773
  onPointerMissed,
15011
14774
  onDragOverMissed,
15012
14775
  onDropMissed,
@@ -15014,10 +14777,25 @@ function CanvasImpl({
15014
14777
  hmr,
15015
14778
  width,
15016
14779
  height,
14780
+ background,
14781
+ forceEven,
15017
14782
  ...props
15018
14783
  }) {
14784
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14785
+ let primaryCanvas;
14786
+ let scheduler;
14787
+ let renderer;
14788
+ if (isRendererConfig) {
14789
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14790
+ primaryCanvas = pc;
14791
+ scheduler = sc;
14792
+ renderer = Object.keys(rest).length > 0 ? rest : rendererProp;
14793
+ } else {
14794
+ renderer = rendererProp;
14795
+ }
15019
14796
  React__namespace.useMemo(() => extend(THREE), []);
15020
14797
  const Bridge = useBridge();
14798
+ const backgroundProps = React__namespace.useMemo(() => parseBackground(background), [background]);
15021
14799
  const hasInitialSizeRef = React__namespace.useRef(false);
15022
14800
  const measureConfig = React__namespace.useMemo(() => {
15023
14801
  if (!hasInitialSizeRef.current) {
@@ -15034,15 +14812,20 @@ function CanvasImpl({
15034
14812
  };
15035
14813
  }, [resize, hasInitialSizeRef.current]);
15036
14814
  const [containerRef, containerRect] = useMeasure__default(measureConfig);
15037
- const effectiveSize = React__namespace.useMemo(
15038
- () => ({
15039
- width: width ?? containerRect.width,
15040
- height: height ?? containerRect.height,
14815
+ const effectiveSize = React__namespace.useMemo(() => {
14816
+ let w = width ?? containerRect.width;
14817
+ let h = height ?? containerRect.height;
14818
+ if (forceEven) {
14819
+ w = Math.ceil(w / 2) * 2;
14820
+ h = Math.ceil(h / 2) * 2;
14821
+ }
14822
+ return {
14823
+ width: w,
14824
+ height: h,
15041
14825
  top: containerRect.top,
15042
14826
  left: containerRect.left
15043
- }),
15044
- [width, height, containerRect]
15045
- );
14827
+ };
14828
+ }, [width, height, containerRect, forceEven]);
15046
14829
  if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
15047
14830
  hasInitialSizeRef.current = true;
15048
14831
  }
@@ -15082,23 +14865,26 @@ function CanvasImpl({
15082
14865
  async function run() {
15083
14866
  if (!effectActiveRef.current || !root.current) return;
15084
14867
  await root.current.configure({
14868
+ id,
14869
+ primaryCanvas,
14870
+ scheduler,
15085
14871
  gl,
15086
14872
  renderer,
15087
14873
  scene,
15088
14874
  events,
15089
14875
  shadows,
15090
- linear,
15091
- flat,
15092
- legacy,
15093
14876
  orthographic,
15094
14877
  frameloop,
15095
14878
  dpr,
15096
14879
  performance,
15097
14880
  raycaster,
15098
14881
  camera,
14882
+ autoUpdateFrustum,
14883
+ occlusion,
15099
14884
  size: effectiveSize,
15100
14885
  // Store size props for reset functionality
15101
14886
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
14887
+ forceEven,
15102
14888
  // Pass mutable reference to onPointerMissed so it's free to update
15103
14889
  onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
15104
14890
  onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
@@ -15122,7 +14908,10 @@ function CanvasImpl({
15122
14908
  });
15123
14909
  if (!effectActiveRef.current || !root.current) return;
15124
14910
  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 }) }) })
14911
+ /* @__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: [
14912
+ backgroundProps && /* @__PURE__ */ jsxRuntime.jsx(Environment, { ...backgroundProps }),
14913
+ children ?? null
14914
+ ] }) }) })
15126
14915
  );
15127
14916
  }
15128
14917
  run();
@@ -15149,20 +14938,15 @@ function CanvasImpl({
15149
14938
  const canvas = canvasRef.current;
15150
14939
  if (!canvas) return;
15151
14940
  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
- }
14941
+ queueMicrotask(() => {
14942
+ const rootEntry = _roots.get(canvas);
14943
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
14944
+ });
15160
14945
  };
15161
14946
  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
14947
  const hot = undefined;
15163
14948
  hot.on("vite:afterUpdate", handleHMR);
15164
- return () => hot.dispose?.(() => {
15165
- });
14949
+ return () => hot.off?.("vite:afterUpdate", handleHMR);
15166
14950
  }
15167
14951
  if (typeof module !== "undefined" && module.hot) {
15168
14952
  const hot = module.hot;
@@ -15185,7 +14969,16 @@ function CanvasImpl({
15185
14969
  ...style
15186
14970
  },
15187
14971
  ...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 }) })
14972
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(
14973
+ "canvas",
14974
+ {
14975
+ ref: canvasRef,
14976
+ id,
14977
+ className: "r3f-canvas",
14978
+ style: { display: "block", width: "100%", height: "100%" },
14979
+ children: fallback
14980
+ }
14981
+ ) })
15189
14982
  }
15190
14983
  );
15191
14984
  }
@@ -15258,6 +15051,48 @@ let ScopedStore = _ScopedStore;
15258
15051
  function createScopedStore(data) {
15259
15052
  return new ScopedStore(data);
15260
15053
  }
15054
+ function createLazyCreatorState(state) {
15055
+ let _uniforms = null;
15056
+ let _nodes = null;
15057
+ let _buffers = null;
15058
+ let _gpuStorage = null;
15059
+ return Object.create(state, {
15060
+ uniforms: {
15061
+ get() {
15062
+ return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
15063
+ }
15064
+ },
15065
+ nodes: {
15066
+ get() {
15067
+ return _nodes ?? (_nodes = createScopedStore(state.nodes));
15068
+ }
15069
+ },
15070
+ buffers: {
15071
+ get() {
15072
+ return _buffers ?? (_buffers = createScopedStore(state.buffers));
15073
+ }
15074
+ },
15075
+ gpuStorage: {
15076
+ get() {
15077
+ return _gpuStorage ?? (_gpuStorage = createScopedStore(state.gpuStorage));
15078
+ }
15079
+ }
15080
+ });
15081
+ }
15082
+
15083
+ function resolvePrimary(local) {
15084
+ return local.getState().primaryStore ?? local;
15085
+ }
15086
+ function usePrimaryStore() {
15087
+ const local = useStore();
15088
+ const primary = local.getState().primaryStore;
15089
+ return React.useMemo(() => primary ?? local, [primary, local]);
15090
+ }
15091
+ function usePrimaryThree(selector = (state) => state, equalityFn) {
15092
+ const local = useStore();
15093
+ const primary = resolvePrimary(local);
15094
+ return primary(selector, equalityFn);
15095
+ }
15261
15096
 
15262
15097
  function addTexture(set, key, value) {
15263
15098
  set((state) => {
@@ -15298,6 +15133,27 @@ function createTextureOperations(set) {
15298
15133
  removeMultiple: (keys) => removeTextures(set, keys)
15299
15134
  };
15300
15135
  }
15136
+ function extractTSLValue(value) {
15137
+ if (value === null || value === void 0) return value;
15138
+ if (typeof value !== "object") return value;
15139
+ const node = value;
15140
+ if (!node.isNode) return value;
15141
+ if (node.isConstNode) {
15142
+ return node.value;
15143
+ }
15144
+ if ("value" in node) {
15145
+ let extractedValue = node.value;
15146
+ if (typeof node.traverse === "function") {
15147
+ node.traverse((n) => {
15148
+ if (n.isConstNode) {
15149
+ extractedValue = n.value;
15150
+ }
15151
+ });
15152
+ }
15153
+ return extractedValue;
15154
+ }
15155
+ return value;
15156
+ }
15301
15157
  function vectorize(inObject) {
15302
15158
  if (inObject === null || inObject === void 0) return inObject;
15303
15159
  if (typeof inObject === "string") {
@@ -15310,9 +15166,16 @@ function vectorize(inObject) {
15310
15166
  }
15311
15167
  if (typeof inObject !== "object") return inObject;
15312
15168
  const obj = inObject;
15169
+ if (obj.isNode) {
15170
+ return extractTSLValue(inObject);
15171
+ }
15313
15172
  if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
15314
15173
  if (obj.isMatrix3 || obj.isMatrix4) return inObject;
15315
15174
  if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
15175
+ if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
15176
+ const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
15177
+ return new webgpu.Color(obj.r * scale, obj.g * scale, obj.b * scale);
15178
+ }
15316
15179
  if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
15317
15180
  if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
15318
15181
  return new webgpu.Vector4(obj.x, obj.y, obj.z, obj.w);
@@ -15336,7 +15199,7 @@ function useCompareMemoize(value, deep) {
15336
15199
 
15337
15200
  const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15338
15201
  function useUniforms(creatorOrScope, scope) {
15339
- const store = useStore();
15202
+ const store = usePrimaryStore();
15340
15203
  const removeUniforms2 = React.useCallback(
15341
15204
  (names, targetScope) => {
15342
15205
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15375,17 +15238,14 @@ function useUniforms(creatorOrScope, scope) {
15375
15238
  const rebuildUniforms = React.useCallback(
15376
15239
  (targetScope) => {
15377
15240
  store.setState((state) => {
15378
- let newUniforms = state.uniforms;
15241
+ let newUniforms = {};
15379
15242
  if (targetScope && targetScope !== "root") {
15380
15243
  const { [targetScope]: _, ...rest } = state.uniforms;
15381
15244
  newUniforms = rest;
15382
15245
  } else if (targetScope === "root") {
15383
- newUniforms = {};
15384
15246
  for (const [key, value] of Object.entries(state.uniforms)) {
15385
15247
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15386
15248
  }
15387
- } else {
15388
- newUniforms = {};
15389
15249
  }
15390
15250
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15391
15251
  });
@@ -15393,20 +15253,26 @@ function useUniforms(creatorOrScope, scope) {
15393
15253
  [store]
15394
15254
  );
15395
15255
  const inputForMemoization = React.useMemo(() => {
15256
+ let raw = creatorOrScope;
15396
15257
  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);
15258
+ const wrappedState = createLazyCreatorState(store.getState());
15259
+ raw = creatorOrScope(wrappedState);
15260
+ }
15261
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
15262
+ const normalized = {};
15263
+ for (const [key, value] of Object.entries(raw)) {
15264
+ normalized[key] = vectorize(value);
15265
+ }
15266
+ return normalized;
15404
15267
  }
15405
- return creatorOrScope;
15268
+ return raw;
15406
15269
  }, [creatorOrScope, store]);
15407
15270
  const memoizedInput = useCompareMemoize(inputForMemoization);
15408
15271
  const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
15409
- const storeUniforms = useThree((s) => s.uniforms);
15272
+ const storeUniforms = usePrimaryThree((s) => s.uniforms);
15273
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15274
+ const readerDep = isReader ? storeUniforms : null;
15275
+ const creatorDep = isReader ? null : hmrVersion;
15410
15276
  const uniforms = React.useMemo(() => {
15411
15277
  if (memoizedInput === void 0) {
15412
15278
  return storeUniforms;
@@ -15461,28 +15327,20 @@ function useUniforms(creatorOrScope, scope) {
15461
15327
  }
15462
15328
  }
15463
15329
  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
- ]);
15330
+ }, [store, memoizedInput, scope, readerDep, creatorDep]);
15471
15331
  return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
15472
15332
  }
15473
15333
  function rebuildAllUniforms(store, scope) {
15334
+ store = store.getState().primaryStore ?? store;
15474
15335
  store.setState((state) => {
15475
- let newUniforms = state.uniforms;
15336
+ let newUniforms = {};
15476
15337
  if (scope && scope !== "root") {
15477
15338
  const { [scope]: _, ...rest } = state.uniforms;
15478
15339
  newUniforms = rest;
15479
15340
  } else if (scope === "root") {
15480
- newUniforms = {};
15481
15341
  for (const [key, value] of Object.entries(state.uniforms)) {
15482
15342
  if (!isUniformNode$1(value)) newUniforms[key] = value;
15483
15343
  }
15484
- } else {
15485
- newUniforms = {};
15486
15344
  }
15487
15345
  return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15488
15346
  });
@@ -15550,15 +15408,17 @@ function isSameThreeType(a, b) {
15550
15408
  }
15551
15409
 
15552
15410
  const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15411
+ const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
15553
15412
  function useUniform(name, value) {
15554
15413
  const store = useStore();
15414
+ const hmrVersion = useThree((s) => s._hmrVersion);
15555
15415
  return React.useMemo(() => {
15556
15416
  const state = store.getState();
15557
15417
  const set = store.setState;
15558
15418
  const existing = state.uniforms[name];
15559
15419
  if (existing && isUniformNode(existing)) {
15560
- if (value !== void 0) {
15561
- existing.value = value;
15420
+ if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
15421
+ existing.value = typeof value === "string" ? new webgpu.Color(value) : value;
15562
15422
  }
15563
15423
  return existing;
15564
15424
  }
@@ -15567,7 +15427,24 @@ function useUniform(name, value) {
15567
15427
  `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
15568
15428
  );
15569
15429
  }
15570
- const node = tsl.uniform(value);
15430
+ if (isUniformNode(value)) {
15431
+ const node2 = value;
15432
+ if (typeof node2.setName === "function") {
15433
+ node2.setName(name);
15434
+ }
15435
+ set((s) => ({
15436
+ uniforms: { ...s.uniforms, [name]: node2 }
15437
+ }));
15438
+ return node2;
15439
+ }
15440
+ let node;
15441
+ if (isTSLNode$1(value)) {
15442
+ node = tsl.uniform(value);
15443
+ } else if (typeof value === "string") {
15444
+ node = tsl.uniform(new webgpu.Color(value));
15445
+ } else {
15446
+ node = tsl.uniform(value);
15447
+ }
15571
15448
  if (typeof node.setName === "function") {
15572
15449
  node.setName(name);
15573
15450
  }
@@ -15578,12 +15455,12 @@ function useUniform(name, value) {
15578
15455
  }
15579
15456
  }));
15580
15457
  return node;
15581
- }, [store, name]);
15458
+ }, [store, name, hmrVersion]);
15582
15459
  }
15583
15460
 
15584
15461
  const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
15585
15462
  function useNodes(creatorOrScope, scope) {
15586
- const store = useStore();
15463
+ const store = usePrimaryStore();
15587
15464
  const removeNodes2 = React.useCallback(
15588
15465
  (names, targetScope) => {
15589
15466
  const nameArray = Array.isArray(names) ? names : [names];
@@ -15640,8 +15517,11 @@ function useNodes(creatorOrScope, scope) {
15640
15517
  [store]
15641
15518
  );
15642
15519
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15643
- const storeNodes = useThree((s) => s.nodes);
15644
- const hmrVersion = useThree((s) => s._hmrVersion);
15520
+ const storeNodes = usePrimaryThree((s) => s.nodes);
15521
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15522
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15523
+ const readerDep = isReader ? storeNodes : null;
15524
+ const creatorDep = isReader ? null : hmrVersion;
15645
15525
  const nodes = React.useMemo(() => {
15646
15526
  if (creatorOrScope === void 0) {
15647
15527
  return storeNodes;
@@ -15654,11 +15534,7 @@ function useNodes(creatorOrScope, scope) {
15654
15534
  const state = store.getState();
15655
15535
  const set = store.setState;
15656
15536
  const creator = creatorOrScope;
15657
- const wrappedState = {
15658
- ...state,
15659
- uniforms: createScopedStore(state.uniforms),
15660
- nodes: createScopedStore(state.nodes)
15661
- };
15537
+ const wrappedState = createLazyCreatorState(state);
15662
15538
  const created = creator(wrappedState);
15663
15539
  const result = {};
15664
15540
  let hasNewNodes = false;
@@ -15668,7 +15544,7 @@ function useNodes(creatorOrScope, scope) {
15668
15544
  if (currentScope[name]) {
15669
15545
  result[name] = currentScope[name];
15670
15546
  } else {
15671
- if (typeof node.label === "function") node.setName(`${scope}.${name}`);
15547
+ node.setName?.(`${scope}.${name}`);
15672
15548
  result[name] = node;
15673
15549
  hasNewNodes = true;
15674
15550
  }
@@ -15688,7 +15564,7 @@ function useNodes(creatorOrScope, scope) {
15688
15564
  if (existing && isTSLNode(existing)) {
15689
15565
  result[name] = existing;
15690
15566
  } else {
15691
- if (typeof node.label === "function") node.setName(name);
15567
+ node.setName?.(name);
15692
15568
  result[name] = node;
15693
15569
  hasNewNodes = true;
15694
15570
  }
@@ -15697,18 +15573,11 @@ function useNodes(creatorOrScope, scope) {
15697
15573
  set((s) => ({ nodes: { ...s.nodes, ...result } }));
15698
15574
  }
15699
15575
  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
- ]);
15576
+ }, [store, scopeDep, readerDep, creatorDep]);
15709
15577
  return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
15710
15578
  }
15711
15579
  function rebuildAllNodes(store, scope) {
15580
+ store = store.getState().primaryStore ?? store;
15712
15581
  store.setState((state) => {
15713
15582
  let newNodes = state.nodes;
15714
15583
  if (scope && scope !== "root") {
@@ -15753,22 +15622,364 @@ function clearRootNodes(set) {
15753
15622
  });
15754
15623
  }
15755
15624
  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);
15625
+ const store = usePrimaryStore();
15626
+ const uniforms = usePrimaryThree((s) => s.uniforms);
15627
+ const nodes = usePrimaryThree((s) => s.nodes);
15628
+ const textures = usePrimaryThree((s) => s.textures);
15629
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15760
15630
  return React.useMemo(() => {
15761
- const state = store.getState();
15762
- const wrappedState = {
15763
- ...state,
15764
- uniforms: createScopedStore(state.uniforms),
15765
- nodes: createScopedStore(state.nodes)
15766
- };
15631
+ const wrappedState = createLazyCreatorState(store.getState());
15767
15632
  return creator(wrappedState);
15768
- }, [store, creator, uniforms, nodes, textures]);
15633
+ }, [store, creator, uniforms, nodes, textures, hmrVersion]);
15634
+ }
15635
+
15636
+ const isBufferLike = (value) => {
15637
+ if (value === null || typeof value !== "object") return false;
15638
+ if (ArrayBuffer.isView(value)) return true;
15639
+ if ("isBufferAttribute" in value) return true;
15640
+ if ("uuid" in value || "nodeType" in value) return true;
15641
+ return false;
15642
+ };
15643
+ const disposeBuffer = (buffer) => {
15644
+ if (buffer === null || typeof buffer !== "object") return;
15645
+ if ("dispose" in buffer && typeof buffer.dispose === "function") {
15646
+ buffer.dispose();
15647
+ }
15648
+ };
15649
+ function useBuffers(creatorOrScope, scope) {
15650
+ const store = usePrimaryStore();
15651
+ const removeBuffers = React.useCallback(
15652
+ (names, targetScope) => {
15653
+ const nameArray = Array.isArray(names) ? names : [names];
15654
+ store.setState((state) => {
15655
+ if (targetScope) {
15656
+ const currentScope = { ...state.buffers[targetScope] };
15657
+ for (const name of nameArray) delete currentScope[name];
15658
+ return { buffers: { ...state.buffers, [targetScope]: currentScope } };
15659
+ }
15660
+ const buffers2 = { ...state.buffers };
15661
+ for (const name of nameArray) if (isBufferLike(buffers2[name])) delete buffers2[name];
15662
+ return { buffers: buffers2 };
15663
+ });
15664
+ },
15665
+ [store]
15666
+ );
15667
+ const clearBuffers = React.useCallback(
15668
+ (targetScope) => {
15669
+ store.setState((state) => {
15670
+ if (targetScope && targetScope !== "root") {
15671
+ const { [targetScope]: _, ...rest } = state.buffers;
15672
+ return { buffers: rest };
15673
+ }
15674
+ if (targetScope === "root") {
15675
+ const buffers2 = {};
15676
+ for (const [key, value] of Object.entries(state.buffers)) {
15677
+ if (!isBufferLike(value)) buffers2[key] = value;
15678
+ }
15679
+ return { buffers: buffers2 };
15680
+ }
15681
+ return { buffers: {} };
15682
+ });
15683
+ },
15684
+ [store]
15685
+ );
15686
+ const rebuildBuffers = React.useCallback(
15687
+ (targetScope) => {
15688
+ store.setState((state) => {
15689
+ let newBuffers = state.buffers;
15690
+ if (targetScope && targetScope !== "root") {
15691
+ const { [targetScope]: _, ...rest } = state.buffers;
15692
+ newBuffers = rest;
15693
+ } else if (targetScope === "root") {
15694
+ newBuffers = {};
15695
+ for (const [key, value] of Object.entries(state.buffers)) {
15696
+ if (!isBufferLike(value)) newBuffers[key] = value;
15697
+ }
15698
+ } else {
15699
+ newBuffers = {};
15700
+ }
15701
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15702
+ });
15703
+ },
15704
+ [store]
15705
+ );
15706
+ const disposeBuffers = React.useCallback(
15707
+ (names, targetScope) => {
15708
+ const nameArray = Array.isArray(names) ? names : [names];
15709
+ const state = store.getState();
15710
+ for (const name of nameArray) {
15711
+ const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
15712
+ if (buffer && isBufferLike(buffer)) {
15713
+ disposeBuffer(buffer);
15714
+ }
15715
+ }
15716
+ removeBuffers(names, targetScope);
15717
+ },
15718
+ [store, removeBuffers]
15719
+ );
15720
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15721
+ const storeBuffers = usePrimaryThree((s) => s.buffers);
15722
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15723
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15724
+ const readerDep = isReader ? storeBuffers : null;
15725
+ const creatorDep = isReader ? null : hmrVersion;
15726
+ const buffers = React.useMemo(() => {
15727
+ if (creatorOrScope === void 0) {
15728
+ return storeBuffers;
15729
+ }
15730
+ if (typeof creatorOrScope === "string") {
15731
+ const scopeData = storeBuffers[creatorOrScope];
15732
+ if (scopeData && !isBufferLike(scopeData)) return scopeData;
15733
+ return {};
15734
+ }
15735
+ const state = store.getState();
15736
+ const set = store.setState;
15737
+ const creator = creatorOrScope;
15738
+ const wrappedState = createLazyCreatorState(state);
15739
+ const created = creator(wrappedState);
15740
+ const result = {};
15741
+ let hasNewBuffers = false;
15742
+ if (scope) {
15743
+ const currentScope = state.buffers[scope] ?? {};
15744
+ for (const [name, buffer] of Object.entries(created)) {
15745
+ if (currentScope[name]) {
15746
+ result[name] = currentScope[name];
15747
+ } else {
15748
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15749
+ buffer.setName(`${scope}.${name}`);
15750
+ }
15751
+ result[name] = buffer;
15752
+ hasNewBuffers = true;
15753
+ }
15754
+ }
15755
+ if (hasNewBuffers) {
15756
+ set((s) => ({
15757
+ buffers: {
15758
+ ...s.buffers,
15759
+ [scope]: { ...s.buffers[scope], ...result }
15760
+ }
15761
+ }));
15762
+ }
15763
+ return result;
15764
+ }
15765
+ for (const [name, buffer] of Object.entries(created)) {
15766
+ const existing = state.buffers[name];
15767
+ if (existing && isBufferLike(existing)) {
15768
+ result[name] = existing;
15769
+ } else {
15770
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15771
+ buffer.setName(name);
15772
+ }
15773
+ result[name] = buffer;
15774
+ hasNewBuffers = true;
15775
+ }
15776
+ }
15777
+ if (hasNewBuffers) {
15778
+ set((s) => ({ buffers: { ...s.buffers, ...result } }));
15779
+ }
15780
+ return result;
15781
+ }, [store, scopeDep, readerDep, creatorDep]);
15782
+ return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
15783
+ }
15784
+ function rebuildAllBuffers(store, scope) {
15785
+ store = store.getState().primaryStore ?? store;
15786
+ store.setState((state) => {
15787
+ let newBuffers = state.buffers;
15788
+ if (scope && scope !== "root") {
15789
+ const { [scope]: _, ...rest } = state.buffers;
15790
+ newBuffers = rest;
15791
+ } else if (scope === "root") {
15792
+ newBuffers = {};
15793
+ for (const [key, value] of Object.entries(state.buffers)) {
15794
+ if (!isBufferLike(value)) newBuffers[key] = value;
15795
+ }
15796
+ } else {
15797
+ newBuffers = {};
15798
+ }
15799
+ return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
15800
+ });
15801
+ }
15802
+
15803
+ const isStorageLike = (value) => {
15804
+ if (value === null || typeof value !== "object") return false;
15805
+ if ("isTexture" in value) return true;
15806
+ if ("isData3DTexture" in value) return true;
15807
+ if ("uuid" in value || "nodeType" in value) return true;
15808
+ return false;
15809
+ };
15810
+ const disposeStorage = (storage) => {
15811
+ if (storage === null || typeof storage !== "object") return;
15812
+ if ("dispose" in storage && typeof storage.dispose === "function") {
15813
+ storage.dispose();
15814
+ }
15815
+ };
15816
+ function useGPUStorage(creatorOrScope, scope) {
15817
+ const store = usePrimaryStore();
15818
+ const removeStorage = React.useCallback(
15819
+ (names, targetScope) => {
15820
+ const nameArray = Array.isArray(names) ? names : [names];
15821
+ store.setState((state) => {
15822
+ if (targetScope) {
15823
+ const currentScope = { ...state.gpuStorage[targetScope] };
15824
+ for (const name of nameArray) delete currentScope[name];
15825
+ return { gpuStorage: { ...state.gpuStorage, [targetScope]: currentScope } };
15826
+ }
15827
+ const gpuStorage2 = { ...state.gpuStorage };
15828
+ for (const name of nameArray) if (isStorageLike(gpuStorage2[name])) delete gpuStorage2[name];
15829
+ return { gpuStorage: gpuStorage2 };
15830
+ });
15831
+ },
15832
+ [store]
15833
+ );
15834
+ const clearStorage = React.useCallback(
15835
+ (targetScope) => {
15836
+ store.setState((state) => {
15837
+ if (targetScope && targetScope !== "root") {
15838
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15839
+ return { gpuStorage: rest };
15840
+ }
15841
+ if (targetScope === "root") {
15842
+ const gpuStorage2 = {};
15843
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15844
+ if (!isStorageLike(value)) gpuStorage2[key] = value;
15845
+ }
15846
+ return { gpuStorage: gpuStorage2 };
15847
+ }
15848
+ return { gpuStorage: {} };
15849
+ });
15850
+ },
15851
+ [store]
15852
+ );
15853
+ const rebuildStorage = React.useCallback(
15854
+ (targetScope) => {
15855
+ store.setState((state) => {
15856
+ let newStorage = state.gpuStorage;
15857
+ if (targetScope && targetScope !== "root") {
15858
+ const { [targetScope]: _, ...rest } = state.gpuStorage;
15859
+ newStorage = rest;
15860
+ } else if (targetScope === "root") {
15861
+ newStorage = {};
15862
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15863
+ if (!isStorageLike(value)) newStorage[key] = value;
15864
+ }
15865
+ } else {
15866
+ newStorage = {};
15867
+ }
15868
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
15869
+ });
15870
+ },
15871
+ [store]
15872
+ );
15873
+ const disposeStorageFn = React.useCallback(
15874
+ (names, targetScope) => {
15875
+ const nameArray = Array.isArray(names) ? names : [names];
15876
+ const state = store.getState();
15877
+ for (const name of nameArray) {
15878
+ const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
15879
+ if (storage && isStorageLike(storage)) {
15880
+ disposeStorage(storage);
15881
+ }
15882
+ }
15883
+ removeStorage(names, targetScope);
15884
+ },
15885
+ [store, removeStorage]
15886
+ );
15887
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15888
+ const storeStorage = usePrimaryThree((s) => s.gpuStorage);
15889
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
15890
+ const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
15891
+ const readerDep = isReader ? storeStorage : null;
15892
+ const creatorDep = isReader ? null : hmrVersion;
15893
+ const gpuStorage = React.useMemo(() => {
15894
+ if (creatorOrScope === void 0) {
15895
+ return storeStorage;
15896
+ }
15897
+ if (typeof creatorOrScope === "string") {
15898
+ const scopeData = storeStorage[creatorOrScope];
15899
+ if (scopeData && !isStorageLike(scopeData)) return scopeData;
15900
+ return {};
15901
+ }
15902
+ const state = store.getState();
15903
+ const set = store.setState;
15904
+ const creator = creatorOrScope;
15905
+ const wrappedState = createLazyCreatorState(state);
15906
+ const created = creator(wrappedState);
15907
+ const result = {};
15908
+ let hasNewStorage = false;
15909
+ if (scope) {
15910
+ const currentScope = state.gpuStorage[scope] ?? {};
15911
+ for (const [name, storage] of Object.entries(created)) {
15912
+ if (currentScope[name]) {
15913
+ result[name] = currentScope[name];
15914
+ } else {
15915
+ if ("setName" in storage && typeof storage.setName === "function") {
15916
+ storage.setName(`${scope}.${name}`);
15917
+ }
15918
+ if ("name" in storage && typeof storage.name === "string") {
15919
+ storage.name = `${scope}.${name}`;
15920
+ }
15921
+ result[name] = storage;
15922
+ hasNewStorage = true;
15923
+ }
15924
+ }
15925
+ if (hasNewStorage) {
15926
+ set((s) => ({
15927
+ gpuStorage: {
15928
+ ...s.gpuStorage,
15929
+ [scope]: { ...s.gpuStorage[scope], ...result }
15930
+ }
15931
+ }));
15932
+ }
15933
+ return result;
15934
+ }
15935
+ for (const [name, storage] of Object.entries(created)) {
15936
+ const existing = state.gpuStorage[name];
15937
+ if (existing && isStorageLike(existing)) {
15938
+ result[name] = existing;
15939
+ } else {
15940
+ if ("setName" in storage && typeof storage.setName === "function") {
15941
+ storage.setName(name);
15942
+ }
15943
+ if ("name" in storage && typeof storage.name === "string") {
15944
+ storage.name = name;
15945
+ }
15946
+ result[name] = storage;
15947
+ hasNewStorage = true;
15948
+ }
15949
+ }
15950
+ if (hasNewStorage) {
15951
+ set((s) => ({ gpuStorage: { ...s.gpuStorage, ...result } }));
15952
+ }
15953
+ return result;
15954
+ }, [store, scopeDep, readerDep, creatorDep]);
15955
+ return {
15956
+ ...gpuStorage,
15957
+ removeStorage,
15958
+ clearStorage,
15959
+ rebuildStorage,
15960
+ disposeStorage: disposeStorageFn
15961
+ };
15962
+ }
15963
+ function rebuildAllStorage(store, scope) {
15964
+ store = store.getState().primaryStore ?? store;
15965
+ store.setState((state) => {
15966
+ let newStorage = state.gpuStorage;
15967
+ if (scope && scope !== "root") {
15968
+ const { [scope]: _, ...rest } = state.gpuStorage;
15969
+ newStorage = rest;
15970
+ } else if (scope === "root") {
15971
+ newStorage = {};
15972
+ for (const [key, value] of Object.entries(state.gpuStorage)) {
15973
+ if (!isStorageLike(value)) newStorage[key] = value;
15974
+ }
15975
+ } else {
15976
+ newStorage = {};
15977
+ }
15978
+ return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
15979
+ });
15769
15980
  }
15770
15981
 
15771
- function usePostProcessing(mainCB, setupCB) {
15982
+ function useRenderPipeline(mainCB, setupCB) {
15772
15983
  const store = useStore();
15773
15984
  const { scene, camera, renderer, isLegacy } = useThree();
15774
15985
  const callbacksRanRef = React.useRef(false);
@@ -15787,7 +15998,7 @@ function usePostProcessing(mainCB, setupCB) {
15787
15998
  }, [store]);
15788
15999
  const reset = React.useCallback(() => {
15789
16000
  store.setState({
15790
- postProcessing: null,
16001
+ renderPipeline: null,
15791
16002
  passes: {}
15792
16003
  });
15793
16004
  callbacksRanRef.current = false;
@@ -15800,13 +16011,13 @@ function usePostProcessing(mainCB, setupCB) {
15800
16011
  }, []);
15801
16012
  React.useLayoutEffect(() => {
15802
16013
  if (isLegacy) {
15803
- throw new Error("usePostProcessing is only available with WebGPU renderer. Set renderer prop on Canvas.");
16014
+ throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
15804
16015
  }
15805
16016
  if (!renderer || !scene || !camera) return;
15806
16017
  const state = store.getState();
15807
16018
  const set = store.setState;
15808
16019
  try {
15809
- let pp = state.postProcessing;
16020
+ let pp = state.renderPipeline;
15810
16021
  let currentPasses = { ...state.passes };
15811
16022
  let justCreatedPP = false;
15812
16023
  if (!pp) {
@@ -15823,7 +16034,7 @@ function usePostProcessing(mainCB, setupCB) {
15823
16034
  }
15824
16035
  currentPasses.scenePass = scenePass;
15825
16036
  if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
15826
- set({ postProcessing: pp, passes: currentPasses });
16037
+ set({ renderPipeline: pp, passes: currentPasses });
15827
16038
  const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
15828
16039
  if (shouldRunCallbacks) {
15829
16040
  if (setupCBRef.current) {
@@ -15845,33 +16056,41 @@ function usePostProcessing(mainCB, setupCB) {
15845
16056
  callbacksRanRef.current = true;
15846
16057
  }
15847
16058
  } catch (error) {
15848
- console.error("[usePostProcessing] Setup error:", error);
16059
+ console.error("[useRenderPipeline] Setup error:", error);
15849
16060
  }
15850
16061
  }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
15851
16062
  const passes = useThree((s) => s.passes);
15852
- const postProcessing = useThree((s) => s.postProcessing);
16063
+ const renderPipeline = useThree((s) => s.renderPipeline);
15853
16064
  return {
15854
16065
  passes,
15855
- postProcessing,
16066
+ renderPipeline,
15856
16067
  clearPasses,
15857
16068
  reset,
15858
16069
  rebuild,
15859
- // isReady indicates if PostProcessing is configured and ready for rendering
15860
- isReady: postProcessing !== null
16070
+ // isReady indicates if RenderPipeline is configured and ready for rendering
16071
+ isReady: renderPipeline !== null
15861
16072
  };
15862
16073
  }
15863
16074
 
15864
16075
  extend(THREE);
15865
16076
 
16077
+ exports.Scheduler = scheduler.Scheduler;
16078
+ exports.getScheduler = scheduler.getScheduler;
15866
16079
  exports.Block = Block;
15867
16080
  exports.Canvas = Canvas;
16081
+ exports.Environment = Environment;
16082
+ exports.EnvironmentCube = EnvironmentCube;
16083
+ exports.EnvironmentMap = EnvironmentMap;
16084
+ exports.EnvironmentPortal = EnvironmentPortal;
15868
16085
  exports.ErrorBoundary = ErrorBoundary;
16086
+ exports.FROM_REF = FROM_REF;
15869
16087
  exports.IsObject = IsObject;
16088
+ exports.ONCE = ONCE;
16089
+ exports.Portal = Portal;
15870
16090
  exports.R3F_BUILD_LEGACY = R3F_BUILD_LEGACY;
15871
16091
  exports.R3F_BUILD_WEBGPU = R3F_BUILD_WEBGPU;
15872
16092
  exports.REACT_INTERNAL_PROPS = REACT_INTERNAL_PROPS;
15873
16093
  exports.RESERVED_PROPS = RESERVED_PROPS;
15874
- exports.Scheduler = Scheduler;
15875
16094
  exports.Texture = Texture;
15876
16095
  exports._roots = _roots;
15877
16096
  exports.act = act;
@@ -15902,35 +16121,49 @@ exports.events = createPointerEvents;
15902
16121
  exports.extend = extend;
15903
16122
  exports.findInitialRoot = findInitialRoot;
15904
16123
  exports.flushSync = flushSync;
16124
+ exports.fromRef = fromRef;
15905
16125
  exports.getInstanceProps = getInstanceProps;
16126
+ exports.getPrimary = getPrimary;
16127
+ exports.getPrimaryIds = getPrimaryIds;
15906
16128
  exports.getRootState = getRootState;
15907
- exports.getScheduler = getScheduler;
15908
16129
  exports.getUuidPrefix = getUuidPrefix;
15909
16130
  exports.hasConstructor = hasConstructor;
16131
+ exports.hasPrimary = hasPrimary;
15910
16132
  exports.invalidate = invalidate;
15911
16133
  exports.invalidateInstance = invalidateInstance;
15912
16134
  exports.is = is;
15913
16135
  exports.isColorRepresentation = isColorRepresentation;
15914
16136
  exports.isCopyable = isCopyable;
16137
+ exports.isFromRef = isFromRef;
15915
16138
  exports.isObject3D = isObject3D;
16139
+ exports.isOnce = isOnce;
15916
16140
  exports.isOrthographicCamera = isOrthographicCamera;
15917
16141
  exports.isRef = isRef;
15918
16142
  exports.isRenderer = isRenderer;
15919
16143
  exports.isTexture = isTexture;
15920
16144
  exports.isVectorLike = isVectorLike;
16145
+ exports.once = once;
15921
16146
  exports.prepare = prepare;
16147
+ exports.presetsObj = presetsObj;
16148
+ exports.rebuildAllBuffers = rebuildAllBuffers;
15922
16149
  exports.rebuildAllNodes = rebuildAllNodes;
16150
+ exports.rebuildAllStorage = rebuildAllStorage;
15923
16151
  exports.rebuildAllUniforms = rebuildAllUniforms;
15924
16152
  exports.reconciler = reconciler;
16153
+ exports.registerPrimary = registerPrimary;
15925
16154
  exports.removeInteractivity = removeInteractivity;
15926
16155
  exports.removeNodes = removeNodes;
15927
16156
  exports.removeUniforms = removeUniforms;
15928
16157
  exports.resolve = resolve;
15929
16158
  exports.unmountComponentAtNode = unmountComponentAtNode;
16159
+ exports.unregisterPrimary = unregisterPrimary;
15930
16160
  exports.updateCamera = updateCamera;
15931
16161
  exports.updateFrustum = updateFrustum;
15932
16162
  exports.useBridge = useBridge;
16163
+ exports.useBuffers = useBuffers;
16164
+ exports.useEnvironment = useEnvironment;
15933
16165
  exports.useFrame = useFrame;
16166
+ exports.useGPUStorage = useGPUStorage;
15934
16167
  exports.useGraph = useGraph;
15935
16168
  exports.useInstanceHandle = useInstanceHandle;
15936
16169
  exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
@@ -15938,7 +16171,7 @@ exports.useLoader = useLoader;
15938
16171
  exports.useLocalNodes = useLocalNodes;
15939
16172
  exports.useMutableCallback = useMutableCallback;
15940
16173
  exports.useNodes = useNodes;
15941
- exports.usePostProcessing = usePostProcessing;
16174
+ exports.useRenderPipeline = useRenderPipeline;
15942
16175
  exports.useRenderTarget = useRenderTarget;
15943
16176
  exports.useStore = useStore;
15944
16177
  exports.useTexture = useTexture;
@@ -15946,3 +16179,4 @@ exports.useTextures = useTextures;
15946
16179
  exports.useThree = useThree;
15947
16180
  exports.useUniform = useUniform;
15948
16181
  exports.useUniforms = useUniforms;
16182
+ exports.waitForPrimary = waitForPrimary;