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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,14 +1,22 @@
1
1
  import * as webgpu from 'three/webgpu';
2
- import { RenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, SRGBColorSpace, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFSoftShadowMap, VSMShadowMap, PCFShadowMap, BasicShadowMap, ColorManagement, LinearSRGBColorSpace, NoToneMapping, ACESFilmicToneMapping, WebGPURenderer } from 'three/webgpu';
2
+ import { RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene, WebGLCubeRenderTarget, HalfFloatType, Color, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, ShaderMaterial, Uniform, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer } from 'three/webgpu';
3
3
  import { WebGLRenderTarget, WebGLRenderer } from 'three';
4
4
  import { Inspector } from 'three/addons/inspector/Inspector.js';
5
- import { jsx, Fragment } from 'react/jsx-runtime';
5
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
6
  import * as React from 'react';
7
- import React__default, { useMemo, useLayoutEffect, useEffect, useContext, useRef, useImperativeHandle, useCallback, useState } from 'react';
7
+ import React__default, { useLayoutEffect, useRef, useMemo, useEffect, useContext, useImperativeHandle, useCallback, useState } from 'react';
8
8
  import useMeasure from 'react-use-measure';
9
9
  import { useFiber, useContextBridge, traverseFiber, FiberProvider } from 'its-fine';
10
+ import { useThree as useThree$1, useLoader as useLoader$1, useFrame as useFrame$1, createPortal as createPortal$1, applyProps as applyProps$1, extend as extend$1 } from '@react-three/fiber';
11
+ import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
12
+ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
13
+ import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
14
+ import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
15
+ import { GainMapLoader } from '@monogrid/gainmap-js';
10
16
  import Tb, { unstable_scheduleCallback, unstable_IdlePriority } from 'scheduler';
11
17
  import { createWithEqualityFn } from 'zustand/traditional';
18
+ import { getScheduler } from '@pmndrs/scheduler';
19
+ export { Scheduler, getScheduler } from '@pmndrs/scheduler';
12
20
  import { suspend, preload, clear } from 'suspend-react';
13
21
 
14
22
  function _mergeNamespaces(n, m) {
@@ -36,9 +44,392 @@ const THREE = /*#__PURE__*/_mergeNamespaces({
36
44
  WebGLRenderer: WebGLRenderer
37
45
  }, [webgpu]);
38
46
 
39
- var __defProp$2 = Object.defineProperty;
40
- var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
41
- var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
47
+ const primaryRegistry = /* @__PURE__ */ new Map();
48
+ const pendingSubscribers = /* @__PURE__ */ new Map();
49
+ function registerPrimary(id, renderer, store) {
50
+ if (primaryRegistry.has(id)) {
51
+ console.warn(`Canvas with id="${id}" already registered. Overwriting.`);
52
+ }
53
+ const entry = { renderer, store };
54
+ primaryRegistry.set(id, entry);
55
+ const subscribers = pendingSubscribers.get(id);
56
+ if (subscribers) {
57
+ subscribers.forEach((callback) => callback(entry));
58
+ pendingSubscribers.delete(id);
59
+ }
60
+ return () => {
61
+ const currentEntry = primaryRegistry.get(id);
62
+ if (currentEntry?.renderer === renderer) {
63
+ primaryRegistry.delete(id);
64
+ }
65
+ };
66
+ }
67
+ function getPrimary(id) {
68
+ return primaryRegistry.get(id);
69
+ }
70
+ function waitForPrimary(id, timeout = 5e3) {
71
+ const existing = primaryRegistry.get(id);
72
+ if (existing) {
73
+ return Promise.resolve(existing);
74
+ }
75
+ return new Promise((resolve, reject) => {
76
+ const timeoutId = setTimeout(() => {
77
+ const subscribers = pendingSubscribers.get(id);
78
+ if (subscribers) {
79
+ const index = subscribers.indexOf(callback);
80
+ if (index !== -1) subscribers.splice(index, 1);
81
+ if (subscribers.length === 0) pendingSubscribers.delete(id);
82
+ }
83
+ reject(new Error(`Timeout waiting for canvas with id="${id}". Make sure a <Canvas id="${id}"> is mounted.`));
84
+ }, timeout);
85
+ const callback = (entry) => {
86
+ clearTimeout(timeoutId);
87
+ resolve(entry);
88
+ };
89
+ if (!pendingSubscribers.has(id)) {
90
+ pendingSubscribers.set(id, []);
91
+ }
92
+ pendingSubscribers.get(id).push(callback);
93
+ });
94
+ }
95
+ function hasPrimary(id) {
96
+ return primaryRegistry.has(id);
97
+ }
98
+ function unregisterPrimary(id) {
99
+ primaryRegistry.delete(id);
100
+ }
101
+ function getPrimaryIds() {
102
+ return Array.from(primaryRegistry.keys());
103
+ }
104
+
105
+ const presetsObj = {
106
+ apartment: "lebombo_1k.hdr",
107
+ city: "potsdamer_platz_1k.hdr",
108
+ dawn: "kiara_1_dawn_1k.hdr",
109
+ forest: "forest_slope_1k.hdr",
110
+ lobby: "st_fagans_interior_1k.hdr",
111
+ night: "dikhololo_night_1k.hdr",
112
+ park: "rooitou_park_1k.hdr",
113
+ studio: "studio_small_03_1k.hdr",
114
+ sunset: "venice_sunset_1k.hdr",
115
+ warehouse: "empty_warehouse_01_1k.hdr"
116
+ };
117
+
118
+ const CUBEMAP_ROOT = "https://raw.githack.com/pmndrs/drei-assets/456060a26bbeb8fdf79326f224b6d99b8bcce736/hdri/";
119
+ const isArray = (arr) => Array.isArray(arr);
120
+ const defaultFiles = ["/px.png", "/nx.png", "/py.png", "/ny.png", "/pz.png", "/nz.png"];
121
+ function useEnvironment({
122
+ files = defaultFiles,
123
+ path = "",
124
+ preset = void 0,
125
+ colorSpace = void 0,
126
+ extensions
127
+ } = {}) {
128
+ if (preset) {
129
+ validatePreset(preset);
130
+ files = presetsObj[preset];
131
+ path = CUBEMAP_ROOT;
132
+ }
133
+ const multiFile = isArray(files);
134
+ const { extension, isCubemap } = getExtension(files);
135
+ const loader = getLoader$1(extension);
136
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
137
+ const renderer = useThree$1((state) => state.renderer);
138
+ useLayoutEffect(() => {
139
+ if (extension !== "webp" && extension !== "jpg" && extension !== "jpeg") return;
140
+ function clearGainmapTexture() {
141
+ useLoader$1.clear(loader, multiFile ? [files] : files);
142
+ }
143
+ renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
144
+ }, [extension, files, loader, multiFile, renderer.domElement]);
145
+ const loaderResult = useLoader$1(
146
+ loader,
147
+ multiFile ? [files] : files,
148
+ (loader2) => {
149
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
150
+ loader2.setRenderer?.(renderer);
151
+ }
152
+ loader2.setPath?.(path);
153
+ if (extensions) extensions(loader2);
154
+ }
155
+ );
156
+ let texture = multiFile ? (
157
+ // @ts-ignore
158
+ loaderResult[0]
159
+ ) : loaderResult;
160
+ if (extension === "jpg" || extension === "jpeg" || extension === "webp") {
161
+ texture = texture.renderTarget?.texture;
162
+ }
163
+ texture.mapping = isCubemap ? CubeReflectionMapping : EquirectangularReflectionMapping;
164
+ texture.colorSpace = colorSpace ?? (isCubemap ? "srgb" : "srgb-linear");
165
+ return texture;
166
+ }
167
+ const preloadDefaultOptions = {
168
+ files: defaultFiles,
169
+ path: "",
170
+ preset: void 0,
171
+ extensions: void 0
172
+ };
173
+ useEnvironment.preload = (preloadOptions) => {
174
+ const options = { ...preloadDefaultOptions, ...preloadOptions };
175
+ let { files, path = "" } = options;
176
+ const { preset, extensions } = options;
177
+ if (preset) {
178
+ validatePreset(preset);
179
+ files = presetsObj[preset];
180
+ path = CUBEMAP_ROOT;
181
+ }
182
+ const { extension } = getExtension(files);
183
+ if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
184
+ throw new Error("useEnvironment: Preloading gainmaps is not supported");
185
+ }
186
+ const loader = getLoader$1(extension);
187
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
188
+ useLoader$1.preload(loader, isArray(files) ? [files] : files, (loader2) => {
189
+ loader2.setPath?.(path);
190
+ if (extensions) extensions(loader2);
191
+ });
192
+ };
193
+ const clearDefaultOptins = {
194
+ files: defaultFiles,
195
+ preset: void 0
196
+ };
197
+ useEnvironment.clear = (clearOptions) => {
198
+ const options = { ...clearDefaultOptins, ...clearOptions };
199
+ let { files } = options;
200
+ const { preset } = options;
201
+ if (preset) {
202
+ validatePreset(preset);
203
+ files = presetsObj[preset];
204
+ }
205
+ const { extension } = getExtension(files);
206
+ const loader = getLoader$1(extension);
207
+ if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
208
+ useLoader$1.clear(loader, isArray(files) ? [files] : files);
209
+ };
210
+ function validatePreset(preset) {
211
+ if (!(preset in presetsObj)) throw new Error("Preset must be one of: " + Object.keys(presetsObj).join(", "));
212
+ }
213
+ function getExtension(files) {
214
+ const isCubemap = isArray(files) && files.length === 6;
215
+ const isGainmap = isArray(files) && files.length === 3 && files.some((file) => file.endsWith("json"));
216
+ const firstEntry = isArray(files) ? files[0] : files;
217
+ 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();
218
+ return { extension, isCubemap, isGainmap };
219
+ }
220
+ function getLoader$1(extension) {
221
+ const loader = extension === "cube" ? CubeTextureLoader : extension === "hdr" ? HDRLoader : extension === "exr" ? EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader : extension === "webp" ? GainMapLoader : null;
222
+ return loader;
223
+ }
224
+
225
+ const isRef$1 = (obj) => obj.current && obj.current.isScene;
226
+ const resolveScene = (scene) => isRef$1(scene) ? scene.current : scene;
227
+ function setEnvProps(background, scene, defaultScene, texture, sceneProps = {}) {
228
+ sceneProps = {
229
+ backgroundBlurriness: 0,
230
+ backgroundIntensity: 1,
231
+ backgroundRotation: [0, 0, 0],
232
+ environmentIntensity: 1,
233
+ environmentRotation: [0, 0, 0],
234
+ ...sceneProps
235
+ };
236
+ const target = resolveScene(scene || defaultScene);
237
+ const oldbg = target.background;
238
+ const oldenv = target.environment;
239
+ const oldSceneProps = {
240
+ // @ts-ignore
241
+ backgroundBlurriness: target.backgroundBlurriness,
242
+ // @ts-ignore
243
+ backgroundIntensity: target.backgroundIntensity,
244
+ // @ts-ignore
245
+ backgroundRotation: target.backgroundRotation?.clone?.() ?? [0, 0, 0],
246
+ // @ts-ignore
247
+ environmentIntensity: target.environmentIntensity,
248
+ // @ts-ignore
249
+ environmentRotation: target.environmentRotation?.clone?.() ?? [0, 0, 0]
250
+ };
251
+ if (background !== "only") target.environment = texture;
252
+ if (background) target.background = texture;
253
+ applyProps$1(target, sceneProps);
254
+ return () => {
255
+ if (background !== "only") target.environment = oldenv;
256
+ if (background) target.background = oldbg;
257
+ applyProps$1(target, oldSceneProps);
258
+ };
259
+ }
260
+ function EnvironmentMap({ scene, background = false, map, ...config }) {
261
+ const defaultScene = useThree$1((state) => state.scene);
262
+ React.useLayoutEffect(() => {
263
+ if (map) return setEnvProps(background, scene, defaultScene, map, config);
264
+ });
265
+ return null;
266
+ }
267
+ function EnvironmentCube({
268
+ background = false,
269
+ scene,
270
+ blur,
271
+ backgroundBlurriness,
272
+ backgroundIntensity,
273
+ backgroundRotation,
274
+ environmentIntensity,
275
+ environmentRotation,
276
+ ...rest
277
+ }) {
278
+ const texture = useEnvironment(rest);
279
+ const defaultScene = useThree$1((state) => state.scene);
280
+ React.useLayoutEffect(() => {
281
+ return setEnvProps(background, scene, defaultScene, texture, {
282
+ backgroundBlurriness: blur ?? backgroundBlurriness,
283
+ backgroundIntensity,
284
+ backgroundRotation,
285
+ environmentIntensity,
286
+ environmentRotation
287
+ });
288
+ });
289
+ React.useEffect(() => {
290
+ return () => {
291
+ texture.dispose();
292
+ };
293
+ }, [texture]);
294
+ return null;
295
+ }
296
+ function EnvironmentPortal({
297
+ children,
298
+ near = 0.1,
299
+ far = 1e3,
300
+ resolution = 256,
301
+ frames = 1,
302
+ map,
303
+ background = false,
304
+ blur,
305
+ backgroundBlurriness,
306
+ backgroundIntensity,
307
+ backgroundRotation,
308
+ environmentIntensity,
309
+ environmentRotation,
310
+ scene,
311
+ files,
312
+ path,
313
+ preset = void 0,
314
+ extensions
315
+ }) {
316
+ const gl = useThree$1((state) => state.gl);
317
+ const defaultScene = useThree$1((state) => state.scene);
318
+ const camera = React.useRef(null);
319
+ const [virtualScene] = React.useState(() => new Scene());
320
+ const fbo = React.useMemo(() => {
321
+ const fbo2 = new WebGLCubeRenderTarget(resolution);
322
+ fbo2.texture.type = HalfFloatType;
323
+ return fbo2;
324
+ }, [resolution]);
325
+ React.useEffect(() => {
326
+ return () => {
327
+ fbo.dispose();
328
+ };
329
+ }, [fbo]);
330
+ React.useLayoutEffect(() => {
331
+ if (frames === 1) {
332
+ const autoClear = gl.autoClear;
333
+ gl.autoClear = true;
334
+ camera.current.update(gl, virtualScene);
335
+ gl.autoClear = autoClear;
336
+ }
337
+ return setEnvProps(background, scene, defaultScene, fbo.texture, {
338
+ backgroundBlurriness: blur ?? backgroundBlurriness,
339
+ backgroundIntensity,
340
+ backgroundRotation,
341
+ environmentIntensity,
342
+ environmentRotation
343
+ });
344
+ }, [
345
+ children,
346
+ virtualScene,
347
+ fbo.texture,
348
+ scene,
349
+ defaultScene,
350
+ background,
351
+ frames,
352
+ gl,
353
+ blur,
354
+ backgroundBlurriness,
355
+ backgroundIntensity,
356
+ backgroundRotation,
357
+ environmentIntensity,
358
+ environmentRotation
359
+ ]);
360
+ let count = 1;
361
+ useFrame$1(() => {
362
+ if (frames === Infinity || count < frames) {
363
+ const autoClear = gl.autoClear;
364
+ gl.autoClear = true;
365
+ camera.current.update(gl, virtualScene);
366
+ gl.autoClear = autoClear;
367
+ count++;
368
+ }
369
+ });
370
+ return /* @__PURE__ */ jsx(Fragment, { children: createPortal$1(
371
+ /* @__PURE__ */ jsxs(Fragment, { children: [
372
+ children,
373
+ /* @__PURE__ */ jsx("cubeCamera", { ref: camera, args: [near, far, fbo] }),
374
+ files || preset ? /* @__PURE__ */ jsx(EnvironmentCube, { background: true, files, preset, path, extensions }) : map ? /* @__PURE__ */ jsx(EnvironmentMap, { background: true, map, extensions }) : null
375
+ ] }),
376
+ virtualScene
377
+ ) });
378
+ }
379
+ function EnvironmentGround(props) {
380
+ const textureDefault = useEnvironment(props);
381
+ const texture = props.map || textureDefault;
382
+ React.useMemo(() => extend$1({ GroundProjectedEnvImpl: GroundedSkybox }), []);
383
+ React.useEffect(() => {
384
+ return () => {
385
+ textureDefault.dispose();
386
+ };
387
+ }, [textureDefault]);
388
+ const height = props.ground?.height ?? 15;
389
+ const radius = props.ground?.radius ?? 60;
390
+ const scale = props.ground?.scale ?? 1e3;
391
+ const args = React.useMemo(
392
+ () => [texture, height, radius],
393
+ [texture, height, radius]
394
+ );
395
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
396
+ /* @__PURE__ */ jsx(EnvironmentMap, { ...props, map: texture }),
397
+ /* @__PURE__ */ jsx("groundProjectedEnvImpl", { args, scale })
398
+ ] });
399
+ }
400
+ function EnvironmentColor({ color, scene }) {
401
+ const defaultScene = useThree$1((state) => state.scene);
402
+ React.useLayoutEffect(() => {
403
+ if (color === void 0) return;
404
+ const target = resolveScene(scene || defaultScene);
405
+ const oldBg = target.background;
406
+ target.background = new Color(color);
407
+ return () => {
408
+ target.background = oldBg;
409
+ };
410
+ });
411
+ return null;
412
+ }
413
+ function EnvironmentDualSource(props) {
414
+ const { backgroundFiles, ...envProps } = props;
415
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
416
+ /* @__PURE__ */ jsx(EnvironmentCube, { ...envProps, background: false }),
417
+ /* @__PURE__ */ jsx(EnvironmentCube, { ...props, files: backgroundFiles, background: "only" })
418
+ ] });
419
+ }
420
+ function Environment(props) {
421
+ if (props.color && !props.files && !props.preset && !props.map) {
422
+ return /* @__PURE__ */ jsx(EnvironmentColor, { ...props });
423
+ }
424
+ if (props.backgroundFiles && props.backgroundFiles !== props.files) {
425
+ return /* @__PURE__ */ jsx(EnvironmentDualSource, { ...props });
426
+ }
427
+ return props.ground ? /* @__PURE__ */ jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsx(EnvironmentCube, { ...props });
428
+ }
429
+
430
+ var __defProp = Object.defineProperty;
431
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
432
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
42
433
  const act = React["act"];
43
434
  const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React.useLayoutEffect : React.useEffect;
44
435
  function useMutableCallback(fn) {
@@ -70,7 +461,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
70
461
  return _a = class extends React.Component {
71
462
  constructor() {
72
463
  super(...arguments);
73
- __publicField$2(this, "state", { error: false });
464
+ __publicField(this, "state", { error: false });
74
465
  }
75
466
  componentDidCatch(err) {
76
467
  this.props.set(err);
@@ -78,7 +469,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
78
469
  render() {
79
470
  return this.state.error ? null : this.props.children;
80
471
  }
81
- }, __publicField$2(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
472
+ }, __publicField(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
82
473
  })();
83
474
 
84
475
  const is = {
@@ -264,7 +655,7 @@ function createOcclusionObserverNode(store, uniform) {
264
655
  let occlusionSetupPromise = null;
265
656
  function enableOcclusion(store) {
266
657
  const state = store.getState();
267
- const { internal, renderer, rootScene } = state;
658
+ const { internal, renderer } = state;
268
659
  if (internal.occlusionEnabled || occlusionSetupPromise) return;
269
660
  const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
270
661
  if (!hasOcclusionSupport) {
@@ -372,6 +763,17 @@ function unregisterVisibility(store, object) {
372
763
  internal.visibilityRegistry.delete(object.uuid);
373
764
  internal.occlusionCache.delete(object);
374
765
  }
766
+ function updateVisibilityHandlers(store, object, handlers) {
767
+ const { internal } = store.getState();
768
+ const entry = internal.visibilityRegistry.get(object.uuid);
769
+ if (!entry) return false;
770
+ entry.handlers = handlers;
771
+ if ((handlers.onOccluded || handlers.onVisible) && !internal.occlusionEnabled) {
772
+ object.occlusionTest = true;
773
+ enableOcclusion(store);
774
+ }
775
+ return true;
776
+ }
375
777
  function checkVisibility(state) {
376
778
  const { internal, camera } = state;
377
779
  const registry = internal.visibilityRegistry;
@@ -427,6 +829,22 @@ function hasVisibilityHandlers(handlers) {
427
829
  return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
428
830
  }
429
831
 
832
+ const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
833
+ function fromRef(ref) {
834
+ return { [FROM_REF]: ref };
835
+ }
836
+ function isFromRef(value) {
837
+ return value !== null && typeof value === "object" && FROM_REF in value;
838
+ }
839
+
840
+ const ONCE = Symbol.for("@react-three/fiber.once");
841
+ function once(...args) {
842
+ return { [ONCE]: args.length ? args : true };
843
+ }
844
+ function isOnce(value) {
845
+ return value !== null && typeof value === "object" && ONCE in value;
846
+ }
847
+
430
848
  const RESERVED_PROPS = [
431
849
  "children",
432
850
  "key",
@@ -497,7 +915,7 @@ function getMemoizedPrototype(root) {
497
915
  ctor = new root.constructor();
498
916
  MEMOIZED_PROTOTYPES.set(root.constructor, ctor);
499
917
  }
500
- } catch (e) {
918
+ } catch {
501
919
  }
502
920
  return ctor;
503
921
  }
@@ -543,6 +961,22 @@ function applyProps(object, props) {
543
961
  continue;
544
962
  }
545
963
  if (value === void 0) continue;
964
+ if (isFromRef(value)) continue;
965
+ if (isOnce(value)) {
966
+ if (instance?.appliedOnce?.has(prop)) continue;
967
+ if (instance) {
968
+ instance.appliedOnce ?? (instance.appliedOnce = /* @__PURE__ */ new Set());
969
+ instance.appliedOnce.add(prop);
970
+ }
971
+ const { root: targetRoot, key: targetKey } = resolve(object, prop);
972
+ const args = value[ONCE];
973
+ if (typeof targetRoot[targetKey] === "function") {
974
+ targetRoot[targetKey](...args === true ? [] : args);
975
+ } else if (args !== true && args.length > 0) {
976
+ targetRoot[targetKey] = args[0];
977
+ }
978
+ continue;
979
+ }
546
980
  let { root, key, target } = resolve(object, prop);
547
981
  if (target === void 0 && (typeof root !== "object" || root === null)) {
548
982
  throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`);
@@ -563,9 +997,25 @@ function applyProps(object, props) {
563
997
  } else if (isVectorLike(target) && is.num(value)) {
564
998
  if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
565
999
  else target.set(value);
1000
+ } else if (root instanceof ShaderMaterial && key === "uniforms" && is.obj(value)) {
1001
+ if (!is.obj(root.uniforms)) root.uniforms = {};
1002
+ const uniforms = root.uniforms;
1003
+ const nextUniforms = value;
1004
+ for (const name in nextUniforms) {
1005
+ const uniform = nextUniforms[name];
1006
+ const targetUniform = uniforms[name];
1007
+ if (targetUniform) Object.assign(targetUniform, uniform);
1008
+ else {
1009
+ const nextUniform = uniform instanceof Uniform ? uniform.clone() : new Uniform(uniform.value);
1010
+ uniforms[name] = nextUniform;
1011
+ }
1012
+ }
566
1013
  } else {
567
1014
  root[key] = value;
568
- 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
1015
+ if (key.endsWith("Node") && root.isMaterial) {
1016
+ root.needsUpdate = true;
1017
+ }
1018
+ if (rootState && rootState.renderer?.outputColorSpace === SRGBColorSpace && colorMaps.includes(key) && isTexture(value) && root[key]?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
569
1019
  root[key].format === RGBAFormat && root[key].type === UnsignedByteType) {
570
1020
  root[key].colorSpace = rootState.textureColorSpace;
571
1021
  }
@@ -578,6 +1028,9 @@ function applyProps(object, props) {
578
1028
  if (instance.eventCount && object2.raycast !== null) {
579
1029
  rootState.internal.interaction.push(object2);
580
1030
  }
1031
+ }
1032
+ if (instance?.parent && rootState?.internal && instance.object?.isObject3D) {
1033
+ const object2 = instance.object;
581
1034
  const root = findInitialRoot(instance);
582
1035
  const visibilityHandlers = {
583
1036
  onFramed: instance.handlers.onFramed,
@@ -585,7 +1038,9 @@ function applyProps(object, props) {
585
1038
  onVisible: instance.handlers.onVisible
586
1039
  };
587
1040
  if (hasVisibilityHandlers(visibilityHandlers)) {
588
- registerVisibility(root, object2, visibilityHandlers);
1041
+ if (!updateVisibilityHandlers(root, object2, visibilityHandlers)) {
1042
+ registerVisibility(root, object2, visibilityHandlers);
1043
+ }
589
1044
  } else {
590
1045
  unregisterVisibility(root, object2);
591
1046
  }
@@ -598,38 +1053,60 @@ function applyProps(object, props) {
598
1053
  return object;
599
1054
  }
600
1055
 
1056
+ const DEFAULT_POINTER_ID = 0;
1057
+ const XR_POINTER_ID_START = 1e3;
1058
+ function getPointerState(internal, pointerId) {
1059
+ let state = internal.pointerMap.get(pointerId);
1060
+ if (!state) {
1061
+ state = {
1062
+ hovered: /* @__PURE__ */ new Map(),
1063
+ captured: /* @__PURE__ */ new Map(),
1064
+ initialClick: [0, 0],
1065
+ initialHits: []
1066
+ };
1067
+ internal.pointerMap.set(pointerId, state);
1068
+ }
1069
+ return state;
1070
+ }
1071
+ function getPointerId(event) {
1072
+ return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
1073
+ }
601
1074
  function makeId(event) {
602
1075
  return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
603
1076
  }
604
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
605
- const captureData = captures.get(obj);
1077
+ function releaseInternalPointerCapture(internal, obj, pointerId) {
1078
+ const pointerState = internal.pointerMap.get(pointerId);
1079
+ if (!pointerState) return;
1080
+ const captureData = pointerState.captured.get(obj);
606
1081
  if (captureData) {
607
- captures.delete(obj);
608
- if (captures.size === 0) {
609
- capturedMap.delete(pointerId);
610
- captureData.target.releasePointerCapture(pointerId);
611
- }
1082
+ pointerState.captured.delete(obj);
1083
+ captureData.target.releasePointerCapture(pointerId);
612
1084
  }
613
1085
  }
614
1086
  function removeInteractivity(store, object) {
615
1087
  const { internal } = store.getState();
616
1088
  internal.interaction = internal.interaction.filter((o) => o !== object);
617
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
618
- internal.hovered.forEach((value, key) => {
619
- if (value.eventObject === object || value.object === object) {
620
- internal.hovered.delete(key);
1089
+ for (const [pointerId, pointerState] of internal.pointerMap) {
1090
+ pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
1091
+ pointerState.hovered.forEach((value, key) => {
1092
+ if (value.eventObject === object || value.object === object) {
1093
+ pointerState.hovered.delete(key);
1094
+ }
1095
+ });
1096
+ if (pointerState.captured.has(object)) {
1097
+ releaseInternalPointerCapture(internal, object, pointerId);
621
1098
  }
622
- });
623
- internal.capturedMap.forEach((captures, pointerId) => {
624
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
625
- });
1099
+ }
626
1100
  unregisterVisibility(store, object);
627
1101
  }
628
1102
  function createEvents(store) {
629
- function calculateDistance(event) {
1103
+ function calculateDistance(event, pointerId) {
630
1104
  const { internal } = store.getState();
631
- const dx = event.offsetX - internal.initialClick[0];
632
- const dy = event.offsetY - internal.initialClick[1];
1105
+ const pointerState = internal.pointerMap.get(pointerId);
1106
+ if (!pointerState) return 0;
1107
+ const [initialX, initialY] = pointerState.initialClick;
1108
+ const dx = event.offsetX - initialX;
1109
+ const dy = event.offsetY - initialY;
633
1110
  return Math.round(Math.sqrt(dx * dx + dy * dy));
634
1111
  }
635
1112
  function filterPointerEvents(objects) {
@@ -665,6 +1142,15 @@ function createEvents(store) {
665
1142
  return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
666
1143
  }
667
1144
  let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
1145
+ const aInteractivePriority = a.object.userData?.interactivePriority;
1146
+ const bInteractivePriority = b.object.userData?.interactivePriority;
1147
+ if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
1148
+ if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
1149
+ if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
1150
+ if (aInteractivePriority !== bInteractivePriority) {
1151
+ return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
1152
+ }
1153
+ }
668
1154
  const aState = getRootState(a.object);
669
1155
  const bState = getRootState(b.object);
670
1156
  const aPriority = aState?.events?.priority ?? 1;
@@ -686,9 +1172,13 @@ function createEvents(store) {
686
1172
  eventObject = eventObject.parent;
687
1173
  }
688
1174
  }
689
- if ("pointerId" in event && state.internal.capturedMap.has(event.pointerId)) {
690
- for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
691
- if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1175
+ if ("pointerId" in event) {
1176
+ const pointerId = event.pointerId;
1177
+ const pointerState = state.internal.pointerMap.get(pointerId);
1178
+ if (pointerState?.captured.size) {
1179
+ for (const captureData of pointerState.captured.values()) {
1180
+ if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1181
+ }
692
1182
  }
693
1183
  }
694
1184
  return intersections;
@@ -701,27 +1191,25 @@ function createEvents(store) {
701
1191
  if (state) {
702
1192
  const { raycaster, pointer, camera, internal } = state;
703
1193
  const unprojectedPoint = new Vector3(pointer.x, pointer.y, 0).unproject(camera);
704
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1194
+ const hasPointerCapture = (id) => {
1195
+ const pointerState = internal.pointerMap.get(id);
1196
+ return pointerState?.captured.has(hit.eventObject) ?? false;
1197
+ };
705
1198
  const setPointerCapture = (id) => {
706
1199
  const captureData = { intersection: hit, target: event.target };
707
- if (internal.capturedMap.has(id)) {
708
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
709
- } else {
710
- internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
711
- }
1200
+ const pointerState = getPointerState(internal, id);
1201
+ pointerState.captured.set(hit.eventObject, captureData);
712
1202
  event.target.setPointerCapture(id);
713
1203
  };
714
1204
  const releasePointerCapture = (id) => {
715
- const captures = internal.capturedMap.get(id);
716
- if (captures) {
717
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
718
- }
1205
+ releaseInternalPointerCapture(internal, hit.eventObject, id);
719
1206
  };
720
1207
  const extractEventProps = {};
721
1208
  for (const prop in event) {
722
1209
  const property = event[prop];
723
1210
  if (typeof property !== "function") extractEventProps[prop] = property;
724
1211
  }
1212
+ const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
725
1213
  const raycastEvent = {
726
1214
  ...hit,
727
1215
  ...extractEventProps,
@@ -732,18 +1220,19 @@ function createEvents(store) {
732
1220
  unprojectedPoint,
733
1221
  ray: raycaster.ray,
734
1222
  camera,
1223
+ pointerId: eventPointerId,
735
1224
  // Hijack stopPropagation, which just sets a flag
736
1225
  stopPropagation() {
737
- const capturesForPointer = "pointerId" in event && internal.capturedMap.get(event.pointerId);
1226
+ const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
738
1227
  if (
739
1228
  // ...if this pointer hasn't been captured
740
- !capturesForPointer || // ... or if the hit object is capturing the pointer
741
- capturesForPointer.has(hit.eventObject)
1229
+ !pointerState?.captured.size || // ... or if the hit object is capturing the pointer
1230
+ pointerState.captured.has(hit.eventObject)
742
1231
  ) {
743
1232
  raycastEvent.stopped = localState.stopped = true;
744
- if (internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1233
+ if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
745
1234
  const higher = intersections.slice(0, intersections.indexOf(hit));
746
- cancelPointer([...higher, hit]);
1235
+ cancelPointer([...higher, hit], eventPointerId);
747
1236
  }
748
1237
  }
749
1238
  },
@@ -759,15 +1248,18 @@ function createEvents(store) {
759
1248
  }
760
1249
  return intersections;
761
1250
  }
762
- function cancelPointer(intersections) {
1251
+ function cancelPointer(intersections, pointerId) {
763
1252
  const { internal } = store.getState();
764
- for (const hoveredObj of internal.hovered.values()) {
1253
+ const pid = pointerId ?? DEFAULT_POINTER_ID;
1254
+ const pointerState = internal.pointerMap.get(pid);
1255
+ if (!pointerState) return;
1256
+ for (const [hoveredId, hoveredObj] of pointerState.hovered) {
765
1257
  if (!intersections.length || !intersections.find(
766
1258
  (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
767
1259
  )) {
768
1260
  const eventObject = hoveredObj.eventObject;
769
1261
  const instance = eventObject.__r3f;
770
- internal.hovered.delete(makeId(hoveredObj));
1262
+ pointerState.hovered.delete(hoveredId);
771
1263
  if (instance?.eventCount) {
772
1264
  const handlers = instance.handlers;
773
1265
  const data = { ...hoveredObj, intersections };
@@ -796,41 +1288,118 @@ function createEvents(store) {
796
1288
  instance?.handlers.onDropMissed?.(event);
797
1289
  }
798
1290
  }
1291
+ function cleanupPointer(pointerId) {
1292
+ const { internal } = store.getState();
1293
+ const pointerState = internal.pointerMap.get(pointerId);
1294
+ if (pointerState) {
1295
+ for (const [, hoveredObj] of pointerState.hovered) {
1296
+ const eventObject = hoveredObj.eventObject;
1297
+ const instance = eventObject.__r3f;
1298
+ if (instance?.eventCount) {
1299
+ const handlers = instance.handlers;
1300
+ const data = { ...hoveredObj, intersections: [] };
1301
+ handlers.onPointerOut?.(data);
1302
+ handlers.onPointerLeave?.(data);
1303
+ }
1304
+ }
1305
+ internal.pointerMap.delete(pointerId);
1306
+ }
1307
+ internal.pointerDirty.delete(pointerId);
1308
+ }
1309
+ function processDeferredPointer(event, pointerId) {
1310
+ const state = store.getState();
1311
+ const { internal } = state;
1312
+ if (!state.events.enabled) return;
1313
+ const filter = filterPointerEvents;
1314
+ const hits = intersect(event, filter);
1315
+ cancelPointer(hits, pointerId);
1316
+ function onIntersect(data) {
1317
+ const eventObject = data.eventObject;
1318
+ const instance = eventObject.__r3f;
1319
+ if (!instance?.eventCount) return;
1320
+ const handlers = instance.handlers;
1321
+ if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1322
+ const id = makeId(data);
1323
+ const pointerState = getPointerState(internal, pointerId);
1324
+ const hoveredItem = pointerState.hovered.get(id);
1325
+ if (!hoveredItem) {
1326
+ pointerState.hovered.set(id, data);
1327
+ handlers.onPointerOver?.(data);
1328
+ handlers.onPointerEnter?.(data);
1329
+ } else if (hoveredItem.stopped) {
1330
+ data.stopPropagation();
1331
+ }
1332
+ }
1333
+ handlers.onPointerMove?.(data);
1334
+ }
1335
+ handleIntersects(hits, event, 0, onIntersect);
1336
+ }
799
1337
  function handlePointer(name) {
800
1338
  switch (name) {
801
1339
  case "onPointerLeave":
802
- case "onPointerCancel":
803
1340
  case "onDragLeave":
804
1341
  return () => cancelPointer([]);
1342
+ // Global cancel of these events
1343
+ case "onPointerCancel":
1344
+ return (event) => {
1345
+ const pointerId = getPointerId(event);
1346
+ cleanupPointer(pointerId);
1347
+ };
805
1348
  case "onLostPointerCapture":
806
1349
  return (event) => {
807
1350
  const { internal } = store.getState();
808
- if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) {
1351
+ const pointerId = getPointerId(event);
1352
+ const pointerState = internal.pointerMap.get(pointerId);
1353
+ if (pointerState?.captured.size) {
809
1354
  requestAnimationFrame(() => {
810
- if (internal.capturedMap.has(event.pointerId)) {
811
- internal.capturedMap.delete(event.pointerId);
812
- cancelPointer([]);
1355
+ const pointerState2 = internal.pointerMap.get(pointerId);
1356
+ if (pointerState2?.captured.size) {
1357
+ pointerState2.captured.clear();
813
1358
  }
1359
+ cancelPointer([], pointerId);
814
1360
  });
815
1361
  }
816
1362
  };
817
1363
  }
818
1364
  return function handleEvent(event) {
819
1365
  const state = store.getState();
820
- const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
1366
+ const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
1367
+ const pointerId = getPointerId(event);
821
1368
  internal.lastEvent.current = event;
822
- if (!state.events.enabled) return;
1369
+ if (!events.enabled) return;
823
1370
  const isPointerMove = name === "onPointerMove";
824
1371
  const isDragOver = name === "onDragOver";
825
1372
  const isDrop = name === "onDrop";
826
1373
  const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
1374
+ const isPointerDown = name === "onPointerDown";
1375
+ const isPointerUp = name === "onPointerUp";
1376
+ const isWheel = name === "onWheel";
1377
+ const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
1378
+ if (isPointerMove && canDeferRaycasts) {
1379
+ events.compute?.(event, state);
1380
+ internal.pointerDirty.set(pointerId, event);
1381
+ return;
1382
+ }
1383
+ if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
1384
+ events.compute?.(event, state);
1385
+ internal.pointerDirty.set(pointerId, event);
1386
+ return;
1387
+ }
1388
+ if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
1389
+ const deferredEvent = internal.pointerDirty.get(pointerId);
1390
+ internal.pointerDirty.delete(pointerId);
1391
+ processDeferredPointer(deferredEvent, pointerId);
1392
+ }
827
1393
  const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
828
1394
  const hits = intersect(event, filter);
829
- const delta = isClickEvent ? calculateDistance(event) : 0;
830
- if (name === "onPointerDown") {
831
- internal.initialClick = [event.offsetX, event.offsetY];
832
- internal.initialHits = hits.map((hit) => hit.eventObject);
833
- }
1395
+ const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
1396
+ if (isPointerDown) {
1397
+ const pointerState2 = getPointerState(internal, pointerId);
1398
+ pointerState2.initialClick = [event.offsetX, event.offsetY];
1399
+ pointerState2.initialHits = hits.map((hit) => hit.eventObject);
1400
+ }
1401
+ const pointerState = internal.pointerMap.get(pointerId);
1402
+ const initialHits = pointerState?.initialHits ?? [];
834
1403
  if (isClickEvent && !hits.length) {
835
1404
  if (delta <= 2) {
836
1405
  pointerMissed(event, internal.interaction);
@@ -845,7 +1414,9 @@ function createEvents(store) {
845
1414
  dropMissed(event, internal.interaction);
846
1415
  if (onDropMissed) onDropMissed(event);
847
1416
  }
848
- if (isPointerMove || isDragOver) cancelPointer(hits);
1417
+ if (isPointerMove || isDragOver) {
1418
+ cancelPointer(hits, pointerId);
1419
+ }
849
1420
  function onIntersect(data) {
850
1421
  const eventObject = data.eventObject;
851
1422
  const instance = eventObject.__r3f;
@@ -854,9 +1425,10 @@ function createEvents(store) {
854
1425
  if (isPointerMove) {
855
1426
  if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
856
1427
  const id = makeId(data);
857
- const hoveredItem = internal.hovered.get(id);
1428
+ const pointerState2 = getPointerState(internal, pointerId);
1429
+ const hoveredItem = pointerState2.hovered.get(id);
858
1430
  if (!hoveredItem) {
859
- internal.hovered.set(id, data);
1431
+ pointerState2.hovered.set(id, data);
860
1432
  handlers.onPointerOver?.(data);
861
1433
  handlers.onPointerEnter?.(data);
862
1434
  } else if (hoveredItem.stopped) {
@@ -866,9 +1438,10 @@ function createEvents(store) {
866
1438
  handlers.onPointerMove?.(data);
867
1439
  } else if (isDragOver) {
868
1440
  const id = makeId(data);
869
- const hoveredItem = internal.hovered.get(id);
1441
+ const pointerState2 = getPointerState(internal, pointerId);
1442
+ const hoveredItem = pointerState2.hovered.get(id);
870
1443
  if (!hoveredItem) {
871
- internal.hovered.set(id, data);
1444
+ pointerState2.hovered.set(id, data);
872
1445
  handlers.onDragOverEnter?.(data);
873
1446
  } else if (hoveredItem.stopped) {
874
1447
  data.stopPropagation();
@@ -879,18 +1452,18 @@ function createEvents(store) {
879
1452
  } else {
880
1453
  const handler = handlers[name];
881
1454
  if (handler) {
882
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1455
+ if (!isClickEvent || initialHits.includes(eventObject)) {
883
1456
  pointerMissed(
884
1457
  event,
885
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1458
+ internal.interaction.filter((object) => !initialHits.includes(object))
886
1459
  );
887
1460
  handler(data);
888
1461
  }
889
1462
  } else {
890
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1463
+ if (isClickEvent && initialHits.includes(eventObject)) {
891
1464
  pointerMissed(
892
1465
  event,
893
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1466
+ internal.interaction.filter((object) => !initialHits.includes(object))
894
1467
  );
895
1468
  }
896
1469
  }
@@ -899,7 +1472,15 @@ function createEvents(store) {
899
1472
  handleIntersects(hits, event, delta, onIntersect);
900
1473
  };
901
1474
  }
902
- return { handlePointer };
1475
+ function flushDeferredPointers() {
1476
+ const { internal, events } = store.getState();
1477
+ if (!events.frameTimedRaycasts) return;
1478
+ for (const [pointerId, event] of internal.pointerDirty) {
1479
+ processDeferredPointer(event, pointerId);
1480
+ }
1481
+ internal.pointerDirty.clear();
1482
+ }
1483
+ return { handlePointer, flushDeferredPointers, processDeferredPointer };
903
1484
  }
904
1485
  const DOM_EVENTS = {
905
1486
  onClick: ["click", false],
@@ -918,11 +1499,16 @@ const DOM_EVENTS = {
918
1499
  onLostPointerCapture: ["lostpointercapture", true]
919
1500
  };
920
1501
  function createPointerEvents(store) {
921
- const { handlePointer } = createEvents(store);
1502
+ const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
1503
+ let nextXRPointerId = XR_POINTER_ID_START;
1504
+ const xrPointers = /* @__PURE__ */ new Map();
922
1505
  return {
923
1506
  priority: 1,
924
1507
  enabled: true,
925
- compute(event, state, previous) {
1508
+ frameTimedRaycasts: true,
1509
+ alwaysFireOnScroll: true,
1510
+ updateOnFrame: false,
1511
+ compute(event, state) {
926
1512
  state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
927
1513
  state.raycaster.setFromCamera(state.pointer, state.camera);
928
1514
  },
@@ -931,11 +1517,33 @@ function createPointerEvents(store) {
931
1517
  (acc, key) => ({ ...acc, [key]: handlePointer(key) }),
932
1518
  {}
933
1519
  ),
934
- update: () => {
1520
+ update: (pointerId) => {
935
1521
  const { events, internal } = store.getState();
936
- if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current);
1522
+ if (!events.handlers) return;
1523
+ if (pointerId !== void 0) {
1524
+ const event = internal.pointerDirty.get(pointerId);
1525
+ if (event) {
1526
+ internal.pointerDirty.delete(pointerId);
1527
+ processDeferredPointer(event, pointerId);
1528
+ } else if (internal.lastEvent?.current) {
1529
+ processDeferredPointer(internal.lastEvent.current, pointerId);
1530
+ }
1531
+ } else {
1532
+ flushDeferredPointers();
1533
+ if (internal.lastEvent?.current) {
1534
+ events.handlers.onPointerMove(internal.lastEvent.current);
1535
+ }
1536
+ }
1537
+ },
1538
+ flush: () => {
1539
+ const { events, internal } = store.getState();
1540
+ flushDeferredPointers();
1541
+ if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
1542
+ events.handlers.onPointerMove(internal.lastEvent.current);
1543
+ }
937
1544
  },
938
1545
  connect: (target) => {
1546
+ if (!target) return;
939
1547
  const { set, events } = store.getState();
940
1548
  events.disconnect?.();
941
1549
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -959,6 +1567,33 @@ function createPointerEvents(store) {
959
1567
  }
960
1568
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
961
1569
  }
1570
+ },
1571
+ // PREVIEW (A8): the XR pointer registration API may change before stable.
1572
+ registerPointer: (config) => {
1573
+ const pointerId = nextXRPointerId++;
1574
+ xrPointers.set(pointerId, config);
1575
+ const { internal } = store.getState();
1576
+ getPointerState(internal, pointerId);
1577
+ return pointerId;
1578
+ },
1579
+ unregisterPointer: (pointerId) => {
1580
+ xrPointers.delete(pointerId);
1581
+ const { internal } = store.getState();
1582
+ const pointerState = internal.pointerMap.get(pointerId);
1583
+ if (pointerState) {
1584
+ for (const [, hoveredObj] of pointerState.hovered) {
1585
+ const eventObject = hoveredObj.eventObject;
1586
+ const instance = eventObject.__r3f;
1587
+ if (instance?.eventCount) {
1588
+ const handlers = instance.handlers;
1589
+ const data = { ...hoveredObj, intersections: [] };
1590
+ handlers.onPointerOut?.(data);
1591
+ handlers.onPointerLeave?.(data);
1592
+ }
1593
+ }
1594
+ internal.pointerMap.delete(pointerId);
1595
+ }
1596
+ internal.pointerDirty.delete(pointerId);
962
1597
  }
963
1598
  };
964
1599
  }
@@ -1020,6 +1655,14 @@ function notifyAlpha({ message, link }) {
1020
1655
  }
1021
1656
  }
1022
1657
 
1658
+ const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
1659
+ const INTERNAL_FIBER_MARKERS = ["packages/fiber/src", "packages/fiber/dist", "@react-three/fiber", "@react-three_fiber"];
1660
+ function isInternalRendererAccess(stack, callerDepth = 2) {
1661
+ if (!stack) return false;
1662
+ const caller = stack.split("\n")[callerDepth] ?? "";
1663
+ return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m)) || INTERNAL_FIBER_MARKERS.some((m) => caller.includes(m));
1664
+ }
1665
+
1023
1666
  const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
1024
1667
  const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
1025
1668
  const createStore = (invalidate, advance) => {
@@ -1049,23 +1692,29 @@ const createStore = (invalidate, advance) => {
1049
1692
  set,
1050
1693
  get,
1051
1694
  // Mock objects that have to be configured
1695
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
1696
+ primaryStore: null,
1052
1697
  gl: null,
1053
1698
  renderer: null,
1054
1699
  camera: null,
1055
1700
  frustum: new Frustum(),
1056
1701
  autoUpdateFrustum: true,
1057
1702
  raycaster: null,
1058
- events: { priority: 1, enabled: true, connected: false },
1703
+ events: {
1704
+ priority: 1,
1705
+ enabled: true,
1706
+ connected: false,
1707
+ frameTimedRaycasts: true,
1708
+ alwaysFireOnScroll: true,
1709
+ updateOnFrame: false
1710
+ },
1059
1711
  scene: null,
1060
1712
  rootScene: null,
1061
1713
  xr: null,
1062
1714
  inspector: null,
1063
1715
  invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
1064
1716
  advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
1065
- legacy: false,
1066
- linear: false,
1067
- flat: false,
1068
- textureColorSpace: "srgb",
1717
+ textureColorSpace: SRGBColorSpace,
1069
1718
  isLegacy: false,
1070
1719
  webGPUSupported: false,
1071
1720
  isNative: false,
@@ -1123,6 +1772,7 @@ const createStore = (invalidate, advance) => {
1123
1772
  size: newSize,
1124
1773
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
1125
1774
  }));
1775
+ getScheduler().invalidate();
1126
1776
  }
1127
1777
  }
1128
1778
  return;
@@ -1137,6 +1787,7 @@ const createStore = (invalidate, advance) => {
1137
1787
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
1138
1788
  _sizeImperative: true
1139
1789
  }));
1790
+ getScheduler().invalidate();
1140
1791
  },
1141
1792
  setDpr: (dpr) => set((state2) => {
1142
1793
  const resolved = calculateDpr(dpr);
@@ -1144,14 +1795,18 @@ const createStore = (invalidate, advance) => {
1144
1795
  }),
1145
1796
  setFrameloop: (frameloop = "always") => {
1146
1797
  set(() => ({ frameloop }));
1798
+ getScheduler().frameloop = frameloop;
1147
1799
  },
1148
1800
  setError: (error) => set(() => ({ error })),
1149
1801
  error: null,
1150
- //* TSL State (managed via hooks: useUniforms, useNodes, useTextures, usePostProcessing) ==============================
1802
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
1151
1803
  uniforms: {},
1152
1804
  nodes: {},
1805
+ buffers: {},
1806
+ gpuStorage: {},
1153
1807
  textures: /* @__PURE__ */ new Map(),
1154
- postProcessing: null,
1808
+ _textureRefs: /* @__PURE__ */ new Map(),
1809
+ renderPipeline: null,
1155
1810
  passes: {},
1156
1811
  _hmrVersion: 0,
1157
1812
  _sizeImperative: false,
@@ -1160,12 +1815,16 @@ const createStore = (invalidate, advance) => {
1160
1815
  internal: {
1161
1816
  // Events
1162
1817
  interaction: [],
1163
- hovered: /* @__PURE__ */ new Map(),
1164
1818
  subscribers: [],
1819
+ // Per-pointer state (new unified structure)
1820
+ pointerMap: /* @__PURE__ */ new Map(),
1821
+ pointerDirty: /* @__PURE__ */ new Map(),
1822
+ lastEvent: React.createRef(),
1823
+ // Deprecated but kept for backwards compatibility
1824
+ hovered: /* @__PURE__ */ new Map(),
1165
1825
  initialClick: [0, 0],
1166
1826
  initialHits: [],
1167
1827
  capturedMap: /* @__PURE__ */ new Map(),
1168
- lastEvent: React.createRef(),
1169
1828
  // Visibility tracking (onFramed, onOccluded, onVisible)
1170
1829
  visibilityRegistry: /* @__PURE__ */ new Map(),
1171
1830
  // Occlusion system (WebGPU only)
@@ -1204,8 +1863,7 @@ const createStore = (invalidate, advance) => {
1204
1863
  const currentState = rootStore.getState();
1205
1864
  if (!currentState.isLegacy && currentState.internal.actualRenderer) {
1206
1865
  const stack = new Error().stack || "";
1207
- const isInternalAccess = stack.includes("zustand") || stack.includes("setState") || stack.includes("Object.assign") || stack.includes("react-three-fiber/packages/fiber/src/core");
1208
- if (!isInternalAccess) {
1866
+ if (!isInternalRendererAccess(stack)) {
1209
1867
  const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
1210
1868
  notifyDepreciated({
1211
1869
  heading: "Accessing state.gl in WebGPU mode",
@@ -1248,13 +1906,22 @@ const createStore = (invalidate, advance) => {
1248
1906
  rootStore.subscribe(() => {
1249
1907
  const { camera, size, viewport, set, internal } = rootStore.getState();
1250
1908
  const actualRenderer = internal.actualRenderer;
1909
+ const canvasTarget = internal.canvasTarget;
1251
1910
  if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1252
1911
  oldSize = size;
1253
1912
  oldDpr = viewport.dpr;
1254
1913
  updateCamera(camera, size);
1255
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1256
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && actualRenderer.domElement instanceof HTMLCanvasElement;
1257
- actualRenderer.setSize(size.width, size.height, updateStyle);
1914
+ if (internal.isSecondary && canvasTarget) {
1915
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1916
+ canvasTarget.setSize(size.width, size.height, false);
1917
+ } else {
1918
+ if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1919
+ actualRenderer.setSize(size.width, size.height, false);
1920
+ if (canvasTarget) {
1921
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1922
+ canvasTarget.setSize(size.width, size.height, false);
1923
+ }
1924
+ }
1258
1925
  }
1259
1926
  if (camera !== oldCamera) {
1260
1927
  oldCamera = camera;
@@ -1309,1054 +1976,28 @@ function loadingFn(extensions, onProgress) {
1309
1976
  );
1310
1977
  };
1311
1978
  }
1312
- function useLoader(loader, input, extensions, onProgress) {
1313
- const keys = Array.isArray(input) ? input : [input];
1979
+ function useLoader(loader, input, extensions, onProgress, cacheKey) {
1980
+ const inputs = Array.isArray(input) ? input : [input];
1981
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1314
1982
  const fn = loadingFn(extensions, onProgress);
1315
- const results = keys.map((key) => suspend(fn, [loader, key], { equal: is.equ }));
1983
+ const results = cacheKeys.map(
1984
+ (key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
1985
+ );
1316
1986
  return Array.isArray(input) ? results : results[0];
1317
1987
  }
1318
- useLoader.preload = function(loader, input, extensions, onProgress) {
1319
- const keys = Array.isArray(input) ? input : [input];
1320
- keys.forEach((key) => preload(loadingFn(extensions, onProgress), [loader, key]));
1988
+ useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
1989
+ const inputs = Array.isArray(input) ? input : [input];
1990
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1991
+ cacheKeys.forEach(
1992
+ (key, index) => preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
1993
+ );
1321
1994
  };
1322
- useLoader.clear = function(loader, input) {
1323
- const keys = Array.isArray(input) ? input : [input];
1324
- keys.forEach((key) => clear([loader, key]));
1995
+ useLoader.clear = function(loader, input, cacheKey) {
1996
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1997
+ cacheKeys.forEach((key) => clear([loader, key]));
1325
1998
  };
1326
1999
  useLoader.loader = getLoader;
1327
2000
 
1328
- var __defProp$1 = Object.defineProperty;
1329
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1330
- var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1331
- const DEFAULT_PHASES = ["start", "input", "physics", "update", "render", "finish"];
1332
- class PhaseGraph {
1333
- constructor() {
1334
- /** Ordered list of phase nodes */
1335
- __publicField$1(this, "phases", []);
1336
- /** Quick lookup by name */
1337
- __publicField$1(this, "phaseMap", /* @__PURE__ */ new Map());
1338
- /** Cached ordered names (invalidated on changes) */
1339
- __publicField$1(this, "orderedNamesCache", null);
1340
- this.initializeDefaultPhases();
1341
- }
1342
- //* Initialization --------------------------------
1343
- initializeDefaultPhases() {
1344
- for (const name of DEFAULT_PHASES) {
1345
- const node = { name, isAutoGenerated: false };
1346
- this.phases.push(node);
1347
- this.phaseMap.set(name, node);
1348
- }
1349
- this.invalidateCache();
1350
- }
1351
- //* Public API --------------------------------
1352
- /**
1353
- * Add a named phase to the graph
1354
- * @param name - Phase name (must be unique)
1355
- * @param options - Position options (before or after another phase)
1356
- */
1357
- addPhase(name, options = {}) {
1358
- if (this.phaseMap.has(name)) {
1359
- console.warn(`[useFrame] Phase "${name}" already exists`);
1360
- return;
1361
- }
1362
- const { before, after } = options;
1363
- const node = { name, isAutoGenerated: false };
1364
- let insertIndex = this.phases.length;
1365
- const targetIndex = this.getPhaseIndex(before ?? after);
1366
- if (targetIndex !== -1) {
1367
- insertIndex = before ? targetIndex : targetIndex + 1;
1368
- } else {
1369
- const constraintType = before ? "before" : "after";
1370
- console.warn(`[useFrame] Phase "${before ?? after}" not found for '${constraintType}' constraint`);
1371
- }
1372
- this.phases.splice(insertIndex, 0, node);
1373
- this.phaseMap.set(name, node);
1374
- this.invalidateCache();
1375
- }
1376
- /**
1377
- * Get ordered list of phase names
1378
- */
1379
- getOrderedPhases() {
1380
- if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
1381
- return this.orderedNamesCache;
1382
- }
1383
- /**
1384
- * Check if a phase exists
1385
- */
1386
- hasPhase(name) {
1387
- return this.phaseMap.has(name);
1388
- }
1389
- /**
1390
- * Get the index of a phase (-1 if not found)
1391
- */
1392
- getPhaseIndex(name) {
1393
- if (!name) return -1;
1394
- return this.phases.findIndex((p) => p.name === name);
1395
- }
1396
- /**
1397
- * Ensure a phase exists, creating an auto-generated one if needed.
1398
- * Used for resolving before/after constraints.
1399
- *
1400
- * @param name - The phase name to ensure exists
1401
- * @returns The phase name (may be auto-generated like 'before:render')
1402
- */
1403
- ensurePhase(name) {
1404
- if (this.phaseMap.has(name)) return name;
1405
- const node = { name, isAutoGenerated: true };
1406
- this.phases.push(node);
1407
- this.phaseMap.set(name, node);
1408
- this.invalidateCache();
1409
- return name;
1410
- }
1411
- /**
1412
- * Resolve where a job with before/after constraints should go.
1413
- * Creates auto-generated phases if needed.
1414
- *
1415
- * @param before - Phase(s) to run before
1416
- * @param after - Phase(s) to run after
1417
- * @returns The resolved phase name
1418
- */
1419
- resolveConstraintPhase(before, after) {
1420
- const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
1421
- const afterArr = after ? Array.isArray(after) ? after : [after] : [];
1422
- if (beforeArr.length > 0) {
1423
- return this.ensureAutoPhase(beforeArr[0], "before", 0);
1424
- }
1425
- if (afterArr.length > 0) {
1426
- return this.ensureAutoPhase(afterArr[0], "after", 1);
1427
- }
1428
- return "update";
1429
- }
1430
- /**
1431
- * Ensure an auto-generated phase exists relative to a target phase.
1432
- * Creates the phase if it doesn't exist, inserting it at the correct position.
1433
- *
1434
- * @param target - The target phase name to position relative to
1435
- * @param prefix - Prefix for auto-generated phase name ('before' or 'after')
1436
- * @param offset - Insertion offset (0 for before, 1 for after)
1437
- * @returns The auto-generated phase name
1438
- */
1439
- ensureAutoPhase(target, prefix, offset) {
1440
- const autoName = `${prefix}:${target}`;
1441
- if (this.phaseMap.has(autoName)) return autoName;
1442
- const node = { name: autoName, isAutoGenerated: true };
1443
- const targetIndex = this.getPhaseIndex(target);
1444
- if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
1445
- else this.phases.push(node);
1446
- this.phaseMap.set(autoName, node);
1447
- this.invalidateCache();
1448
- return autoName;
1449
- }
1450
- // Internal --------------------------------
1451
- invalidateCache() {
1452
- this.orderedNamesCache = null;
1453
- }
1454
- }
1455
-
1456
- function rebuildSortedJobs(jobs, phaseGraph) {
1457
- const orderedPhases = phaseGraph.getOrderedPhases();
1458
- const buckets = /* @__PURE__ */ new Map();
1459
- for (const phase of orderedPhases) {
1460
- buckets.set(phase, []);
1461
- }
1462
- for (const job of jobs.values()) {
1463
- if (!job.enabled) continue;
1464
- let bucket = buckets.get(job.phase);
1465
- if (!bucket) {
1466
- bucket = [];
1467
- buckets.set(job.phase, bucket);
1468
- }
1469
- bucket.push(job);
1470
- }
1471
- const sortedBuckets = [];
1472
- for (const phase of orderedPhases) {
1473
- const bucket = buckets.get(phase);
1474
- if (!bucket || bucket.length === 0) continue;
1475
- bucket.sort((a, b) => {
1476
- if (a.priority !== b.priority) return b.priority - a.priority;
1477
- return a.index - b.index;
1478
- });
1479
- sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
1480
- }
1481
- for (const [phase, bucket] of buckets) {
1482
- if (!orderedPhases.includes(phase) && bucket.length > 0) {
1483
- bucket.sort((a, b) => {
1484
- if (a.priority !== b.priority) return b.priority - a.priority;
1485
- return a.index - b.index;
1486
- });
1487
- sortedBuckets.push(bucket);
1488
- }
1489
- }
1490
- return sortedBuckets.flat();
1491
- }
1492
- function hasCrossJobConstraints(bucket) {
1493
- const jobIds = new Set(bucket.map((j) => j.id));
1494
- for (const job of bucket) {
1495
- for (const ref of job.before) {
1496
- if (jobIds.has(ref)) return true;
1497
- }
1498
- for (const ref of job.after) {
1499
- if (jobIds.has(ref)) return true;
1500
- }
1501
- }
1502
- return false;
1503
- }
1504
- function topologicalSort(jobs) {
1505
- const n = jobs.length;
1506
- if (n <= 1) return jobs;
1507
- const jobMap = /* @__PURE__ */ new Map();
1508
- const inDegree = /* @__PURE__ */ new Map();
1509
- const adjacency = /* @__PURE__ */ new Map();
1510
- for (const job of jobs) {
1511
- jobMap.set(job.id, job);
1512
- inDegree.set(job.id, 0);
1513
- adjacency.set(job.id, []);
1514
- }
1515
- for (const job of jobs) {
1516
- for (const ref of job.before) {
1517
- if (jobMap.has(ref)) {
1518
- adjacency.get(job.id).push(ref);
1519
- inDegree.set(ref, inDegree.get(ref) + 1);
1520
- }
1521
- }
1522
- for (const ref of job.after) {
1523
- if (jobMap.has(ref)) {
1524
- adjacency.get(ref).push(job.id);
1525
- inDegree.set(job.id, inDegree.get(job.id) + 1);
1526
- }
1527
- }
1528
- }
1529
- const queue = [];
1530
- for (const job of jobs) {
1531
- if (inDegree.get(job.id) === 0) {
1532
- queue.push(job);
1533
- }
1534
- }
1535
- queue.sort((a, b) => {
1536
- if (a.priority !== b.priority) return b.priority - a.priority;
1537
- return a.index - b.index;
1538
- });
1539
- const result = [];
1540
- while (queue.length > 0) {
1541
- const job = queue.shift();
1542
- result.push(job);
1543
- const neighbors = adjacency.get(job.id) || [];
1544
- for (const neighborId of neighbors) {
1545
- const newDegree = inDegree.get(neighborId) - 1;
1546
- inDegree.set(neighborId, newDegree);
1547
- if (newDegree === 0) {
1548
- const neighbor = jobMap.get(neighborId);
1549
- insertSorted(queue, neighbor);
1550
- }
1551
- }
1552
- }
1553
- if (result.length !== n) {
1554
- console.warn("[useFrame] Circular dependency detected in job constraints");
1555
- const resultIds = new Set(result.map((j) => j.id));
1556
- for (const job of jobs) {
1557
- if (!resultIds.has(job.id)) result.push(job);
1558
- }
1559
- }
1560
- return result;
1561
- }
1562
- function insertSorted(arr, job) {
1563
- let i = 0;
1564
- while (i < arr.length) {
1565
- const cmp = arr[i];
1566
- if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
1567
- break;
1568
- }
1569
- i++;
1570
- }
1571
- arr.splice(i, 0, job);
1572
- }
1573
-
1574
- function shouldRun(job, now) {
1575
- if (!job.enabled) return false;
1576
- if (!job.fps) return true;
1577
- const minInterval = 1e3 / job.fps;
1578
- const lastRun = job.lastRun ?? 0;
1579
- const elapsed = now - lastRun;
1580
- if (elapsed < minInterval) return false;
1581
- if (job.drop) {
1582
- job.lastRun = now;
1583
- } else {
1584
- const steps = Math.floor(elapsed / minInterval);
1585
- job.lastRun = lastRun + steps * minInterval;
1586
- if (job.lastRun < now - minInterval) {
1587
- job.lastRun = now;
1588
- }
1589
- }
1590
- return true;
1591
- }
1592
- function resetJobTiming(job) {
1593
- job.lastRun = void 0;
1594
- }
1595
-
1596
- var __defProp = Object.defineProperty;
1597
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1598
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1599
- const hmrData = (() => {
1600
- if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
1601
- if (typeof import_meta_hot !== "undefined") return import_meta_hot;
1602
- try {
1603
- return (0, eval)("import.meta.hot");
1604
- } catch {
1605
- return void 0;
1606
- }
1607
- })();
1608
- const _Scheduler = class _Scheduler {
1609
- //* Constructor ================================
1610
- constructor() {
1611
- //* Critical State ================================
1612
- __publicField(this, "roots", /* @__PURE__ */ new Map());
1613
- __publicField(this, "phaseGraph");
1614
- __publicField(this, "loopState", {
1615
- running: false,
1616
- rafHandle: null,
1617
- lastTime: null,
1618
- // null = uninitialized, 0+ = valid timestamp
1619
- frameCount: 0,
1620
- elapsedTime: 0,
1621
- createdAt: performance.now()
1622
- });
1623
- __publicField(this, "stoppedTime", 0);
1624
- //* Private State ================================
1625
- __publicField(this, "nextRootIndex", 0);
1626
- __publicField(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
1627
- __publicField(this, "globalAfterJobs", /* @__PURE__ */ new Map());
1628
- __publicField(this, "nextGlobalIndex", 0);
1629
- __publicField(this, "idleCallbacks", /* @__PURE__ */ new Set());
1630
- __publicField(this, "nextJobIndex", 0);
1631
- __publicField(this, "jobStateListeners", /* @__PURE__ */ new Map());
1632
- __publicField(this, "pendingFrames", 0);
1633
- __publicField(this, "_frameloop", "always");
1634
- //* Independent Mode & Error Handling State ================================
1635
- __publicField(this, "_independent", false);
1636
- __publicField(this, "errorHandler", null);
1637
- __publicField(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1638
- //* Core Loop Execution Methods ================================
1639
- /**
1640
- * Main RAF loop callback.
1641
- * Executes frame, handles demand mode, and schedules next frame.
1642
- * @param {number} timestamp - RAF timestamp in milliseconds
1643
- * @returns {void}
1644
- * @private
1645
- */
1646
- __publicField(this, "loop", (timestamp) => {
1647
- if (!this.loopState.running) return;
1648
- this.executeFrame(timestamp);
1649
- if (this._frameloop === "demand") {
1650
- this.pendingFrames = Math.max(0, this.pendingFrames - 1);
1651
- if (this.pendingFrames === 0) {
1652
- this.notifyIdle(timestamp);
1653
- return this.stop();
1654
- }
1655
- }
1656
- this.loopState.rafHandle = requestAnimationFrame(this.loop);
1657
- });
1658
- this.phaseGraph = new PhaseGraph();
1659
- }
1660
- static get instance() {
1661
- return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1662
- }
1663
- static set instance(value) {
1664
- globalThis[_Scheduler.INSTANCE_KEY] = value;
1665
- }
1666
- /**
1667
- * Get the global scheduler instance (creates if doesn't exist).
1668
- * Uses HMR data to preserve instance across hot reloads.
1669
- * @returns {Scheduler} The singleton scheduler instance
1670
- */
1671
- static get() {
1672
- if (!_Scheduler.instance && hmrData?.data?.scheduler) {
1673
- _Scheduler.instance = hmrData.data.scheduler;
1674
- }
1675
- if (!_Scheduler.instance) {
1676
- _Scheduler.instance = new _Scheduler();
1677
- if (hmrData?.data) {
1678
- hmrData.data.scheduler = _Scheduler.instance;
1679
- }
1680
- }
1681
- return _Scheduler.instance;
1682
- }
1683
- /**
1684
- * Reset the singleton instance. Stops the loop and clears all state.
1685
- * Primarily used for testing to ensure clean state between tests.
1686
- * @returns {void}
1687
- */
1688
- static reset() {
1689
- if (_Scheduler.instance) {
1690
- _Scheduler.instance.stop();
1691
- _Scheduler.instance = null;
1692
- }
1693
- if (hmrData?.data) {
1694
- hmrData.data.scheduler = null;
1695
- }
1696
- }
1697
- //* Getters & Setters ================================
1698
- get phases() {
1699
- return this.phaseGraph.getOrderedPhases();
1700
- }
1701
- get frameloop() {
1702
- return this._frameloop;
1703
- }
1704
- set frameloop(mode) {
1705
- if (this._frameloop === mode) return;
1706
- const wasAlways = this._frameloop === "always";
1707
- this._frameloop = mode;
1708
- if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
1709
- else if (mode !== "always" && wasAlways) this.stop();
1710
- }
1711
- get isRunning() {
1712
- return this.loopState.running;
1713
- }
1714
- get isReady() {
1715
- return this.roots.size > 0;
1716
- }
1717
- get independent() {
1718
- return this._independent;
1719
- }
1720
- set independent(value) {
1721
- this._independent = value;
1722
- if (value) this.ensureDefaultRoot();
1723
- }
1724
- //* Root Management Methods ================================
1725
- /**
1726
- * Register a root (Canvas) with the scheduler.
1727
- * The first root to register starts the RAF loop (if frameloop='always').
1728
- * @param {string} id - Unique identifier for this root
1729
- * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
1730
- * @returns {() => void} Unsubscribe function to remove this root
1731
- */
1732
- registerRoot(id, options = {}) {
1733
- if (this.roots.has(id)) {
1734
- console.warn(`[Scheduler] Root "${id}" already registered`);
1735
- return () => this.unregisterRoot(id);
1736
- }
1737
- const entry = {
1738
- id,
1739
- getState: options.getState ?? (() => ({})),
1740
- jobs: /* @__PURE__ */ new Map(),
1741
- sortedJobs: [],
1742
- needsRebuild: false
1743
- };
1744
- if (options.onError) {
1745
- this.errorHandler = options.onError;
1746
- }
1747
- this.roots.set(id, entry);
1748
- if (this.roots.size === 1) {
1749
- this.notifyRootReady();
1750
- if (this._frameloop === "always") this.start();
1751
- }
1752
- return () => this.unregisterRoot(id);
1753
- }
1754
- /**
1755
- * Unregister a root from the scheduler.
1756
- * Cleans up all job state listeners for this root's jobs.
1757
- * The last root to unregister stops the RAF loop.
1758
- * @param {string} id - The root ID to unregister
1759
- * @returns {void}
1760
- */
1761
- unregisterRoot(id) {
1762
- const root = this.roots.get(id);
1763
- if (!root) return;
1764
- for (const jobId of root.jobs.keys()) {
1765
- this.jobStateListeners.delete(jobId);
1766
- }
1767
- this.roots.delete(id);
1768
- if (this.roots.size === 0) {
1769
- this.stop();
1770
- this.errorHandler = null;
1771
- }
1772
- }
1773
- /**
1774
- * Subscribe to be notified when a root becomes available.
1775
- * Fires immediately if a root already exists.
1776
- * @param {() => void} callback - Function called when first root registers
1777
- * @returns {() => void} Unsubscribe function
1778
- */
1779
- onRootReady(callback) {
1780
- if (this.roots.size > 0) {
1781
- callback();
1782
- return () => {
1783
- };
1784
- }
1785
- this.rootReadyCallbacks.add(callback);
1786
- return () => this.rootReadyCallbacks.delete(callback);
1787
- }
1788
- /**
1789
- * Notify all registered root-ready callbacks.
1790
- * Called when the first root registers.
1791
- * @returns {void}
1792
- * @private
1793
- */
1794
- notifyRootReady() {
1795
- for (const cb of this.rootReadyCallbacks) {
1796
- try {
1797
- cb();
1798
- } catch (error) {
1799
- console.error("[Scheduler] Error in root-ready callback:", error);
1800
- }
1801
- }
1802
- this.rootReadyCallbacks.clear();
1803
- }
1804
- /**
1805
- * Ensure a default root exists for independent mode.
1806
- * Creates a minimal root with no state provider.
1807
- * @returns {void}
1808
- * @private
1809
- */
1810
- ensureDefaultRoot() {
1811
- if (!this.roots.has("__default__")) {
1812
- this.registerRoot("__default__");
1813
- }
1814
- }
1815
- /**
1816
- * Trigger error handling for job errors.
1817
- * Uses the bound error handler if available, otherwise logs to console.
1818
- * @param {Error} error - The error to handle
1819
- * @returns {void}
1820
- */
1821
- triggerError(error) {
1822
- if (this.errorHandler) this.errorHandler(error);
1823
- else console.error("[Scheduler]", error);
1824
- }
1825
- //* Phase Management Methods ================================
1826
- /**
1827
- * Add a named phase to the scheduler's execution order.
1828
- * Marks all roots for rebuild to incorporate the new phase.
1829
- * @param {string} name - The phase name (e.g., 'physics', 'postprocess')
1830
- * @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
1831
- * @returns {void}
1832
- * @example
1833
- * scheduler.addPhase('physics', { before: 'update' });
1834
- * scheduler.addPhase('postprocess', { after: 'render' });
1835
- */
1836
- addPhase(name, options) {
1837
- this.phaseGraph.addPhase(name, options);
1838
- for (const root of this.roots.values()) {
1839
- root.needsRebuild = true;
1840
- }
1841
- }
1842
- /**
1843
- * Check if a phase exists in the scheduler.
1844
- * @param {string} name - The phase name to check
1845
- * @returns {boolean} True if the phase exists
1846
- */
1847
- hasPhase(name) {
1848
- return this.phaseGraph.hasPhase(name);
1849
- }
1850
- //* Global Job Registration Methods (Deprecated APIs) ================================
1851
- /**
1852
- * Register a global job that runs once per frame (not per-root).
1853
- * Used internally by deprecated addEffect/addAfterEffect APIs.
1854
- * @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
1855
- * @param {string} id - Unique identifier for this global job
1856
- * @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
1857
- * @returns {() => void} Unsubscribe function to remove this global job
1858
- * @deprecated Use useFrame with phases instead
1859
- */
1860
- registerGlobal(phase, id, callback) {
1861
- const job = { id, callback };
1862
- if (phase === "before") {
1863
- this.globalBeforeJobs.set(id, job);
1864
- } else {
1865
- this.globalAfterJobs.set(id, job);
1866
- }
1867
- return () => {
1868
- if (phase === "before") this.globalBeforeJobs.delete(id);
1869
- else this.globalAfterJobs.delete(id);
1870
- };
1871
- }
1872
- //* Idle Callback Methods (Deprecated API) ================================
1873
- /**
1874
- * Register an idle callback that fires when the loop stops.
1875
- * Used internally by deprecated addTail API.
1876
- * @param {(timestamp: number) => void} callback - Function called when loop becomes idle
1877
- * @returns {() => void} Unsubscribe function to remove this idle callback
1878
- * @deprecated Use demand mode with invalidate() instead
1879
- */
1880
- onIdle(callback) {
1881
- this.idleCallbacks.add(callback);
1882
- return () => this.idleCallbacks.delete(callback);
1883
- }
1884
- /**
1885
- * Notify all registered idle callbacks.
1886
- * Called when the loop stops in demand mode.
1887
- * @param {number} timestamp - The RAF timestamp when idle occurred
1888
- * @returns {void}
1889
- * @private
1890
- */
1891
- notifyIdle(timestamp) {
1892
- for (const cb of this.idleCallbacks) {
1893
- try {
1894
- cb(timestamp);
1895
- } catch (error) {
1896
- console.error("[Scheduler] Error in idle callback:", error);
1897
- }
1898
- }
1899
- }
1900
- //* Job Registration & Management Methods ================================
1901
- /**
1902
- * Register a job (frame callback) with a specific root.
1903
- * This is the core registration method used by useFrame internally.
1904
- * @param {FrameNextCallback} callback - The function to call each frame
1905
- * @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
1906
- * @param {string} [options.rootId] - Target root ID (defaults to first registered root)
1907
- * @param {string} [options.id] - Unique job ID (auto-generated if not provided)
1908
- * @param {string} [options.phase] - Execution phase (defaults to 'update')
1909
- * @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
1910
- * @param {number} [options.fps] - FPS throttle limit
1911
- * @param {boolean} [options.drop] - Drop frames when behind (default true)
1912
- * @param {boolean} [options.enabled] - Whether job is active (default true)
1913
- * @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
1914
- * @returns {() => void} Unsubscribe function to remove this job
1915
- */
1916
- register(callback, options = {}) {
1917
- const rootId = options.rootId;
1918
- const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
1919
- if (!root) {
1920
- console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
1921
- return () => {
1922
- };
1923
- }
1924
- const id = options.id ?? this.generateJobId();
1925
- let phase = options.phase ?? "update";
1926
- if (!options.phase && (options.before || options.after)) {
1927
- phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
1928
- }
1929
- const before = this.normalizeConstraints(options.before);
1930
- const after = this.normalizeConstraints(options.after);
1931
- const job = {
1932
- id,
1933
- callback,
1934
- phase,
1935
- before,
1936
- after,
1937
- priority: options.priority ?? 0,
1938
- index: this.nextJobIndex++,
1939
- fps: options.fps,
1940
- drop: options.drop ?? true,
1941
- enabled: options.enabled ?? true,
1942
- system: options.system ?? false
1943
- };
1944
- if (root.jobs.has(id)) {
1945
- console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
1946
- }
1947
- root.jobs.set(id, job);
1948
- root.needsRebuild = true;
1949
- return () => this.unregister(id, root.id);
1950
- }
1951
- /**
1952
- * Unregister a job by its ID.
1953
- * Searches all roots if rootId is not provided.
1954
- * @param {string} id - The job ID to unregister
1955
- * @param {string} [rootId] - Optional root ID to search (searches all if not provided)
1956
- * @returns {void}
1957
- */
1958
- unregister(id, rootId) {
1959
- const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
1960
- if (root?.jobs.delete(id)) {
1961
- root.needsRebuild = true;
1962
- this.jobStateListeners.delete(id);
1963
- }
1964
- }
1965
- /**
1966
- * Update a job's options dynamically.
1967
- * Searches all roots to find the job by ID.
1968
- * Phase/constraint changes trigger a rebuild of the sorted job list.
1969
- * @param {string} id - The job ID to update
1970
- * @param {Partial<JobOptions>} options - The options to update
1971
- * @returns {void}
1972
- */
1973
- updateJob(id, options) {
1974
- let job;
1975
- let root;
1976
- for (const r of this.roots.values()) {
1977
- job = r.jobs.get(id);
1978
- if (job) {
1979
- root = r;
1980
- break;
1981
- }
1982
- }
1983
- if (!job || !root) return;
1984
- if (options.priority !== void 0) job.priority = options.priority;
1985
- if (options.fps !== void 0) job.fps = options.fps;
1986
- if (options.drop !== void 0) job.drop = options.drop;
1987
- if (options.enabled !== void 0) {
1988
- const wasEnabled = job.enabled;
1989
- job.enabled = options.enabled;
1990
- if (!wasEnabled && job.enabled) resetJobTiming(job);
1991
- if (wasEnabled !== job.enabled) root.needsRebuild = true;
1992
- }
1993
- if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
1994
- if (options.phase) job.phase = options.phase;
1995
- if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
1996
- if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
1997
- root.needsRebuild = true;
1998
- }
1999
- }
2000
- //* Job State Management Methods ================================
2001
- /**
2002
- * Check if a job is currently paused (disabled).
2003
- * @param {string} id - The job ID to check
2004
- * @returns {boolean} True if the job exists and is paused
2005
- */
2006
- isJobPaused(id) {
2007
- for (const root of this.roots.values()) {
2008
- const job = root.jobs.get(id);
2009
- if (job) return !job.enabled;
2010
- }
2011
- return false;
2012
- }
2013
- /**
2014
- * Subscribe to state changes for a specific job.
2015
- * Listener is called when job is paused or resumed.
2016
- * @param {string} id - The job ID to subscribe to
2017
- * @param {() => void} listener - Callback invoked on state changes
2018
- * @returns {() => void} Unsubscribe function
2019
- */
2020
- subscribeJobState(id, listener) {
2021
- if (!this.jobStateListeners.has(id)) {
2022
- this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
2023
- }
2024
- this.jobStateListeners.get(id).add(listener);
2025
- return () => {
2026
- this.jobStateListeners.get(id)?.delete(listener);
2027
- if (this.jobStateListeners.get(id)?.size === 0) {
2028
- this.jobStateListeners.delete(id);
2029
- }
2030
- };
2031
- }
2032
- /**
2033
- * Notify all listeners that a job's state has changed.
2034
- * @param {string} id - The job ID that changed
2035
- * @returns {void}
2036
- * @private
2037
- */
2038
- notifyJobStateChange(id) {
2039
- this.jobStateListeners.get(id)?.forEach((listener) => listener());
2040
- }
2041
- /**
2042
- * Pause a job by ID (sets enabled=false).
2043
- * Notifies any subscribed state listeners.
2044
- * @param {string} id - The job ID to pause
2045
- * @returns {void}
2046
- */
2047
- pauseJob(id) {
2048
- this.updateJob(id, { enabled: false });
2049
- this.notifyJobStateChange(id);
2050
- }
2051
- /**
2052
- * Resume a paused job by ID (sets enabled=true).
2053
- * Resets job timing to prevent frame accumulation.
2054
- * Notifies any subscribed state listeners.
2055
- * @param {string} id - The job ID to resume
2056
- * @returns {void}
2057
- */
2058
- resumeJob(id) {
2059
- this.updateJob(id, { enabled: true });
2060
- this.notifyJobStateChange(id);
2061
- }
2062
- //* Frame Loop Control Methods ================================
2063
- /**
2064
- * Start the requestAnimationFrame loop.
2065
- * Resets timing state (elapsedTime, frameCount) on start.
2066
- * No-op if already running.
2067
- * @returns {void}
2068
- */
2069
- start() {
2070
- if (this.loopState.running) return;
2071
- const { elapsedTime, createdAt } = this.loopState;
2072
- let adjustedCreated = 0;
2073
- if (this.stoppedTime > 0) {
2074
- adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
2075
- this.stoppedTime = 0;
2076
- }
2077
- Object.assign(this.loopState, {
2078
- running: true,
2079
- elapsedTime: elapsedTime ?? 0,
2080
- lastTime: performance.now(),
2081
- createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
2082
- frameCount: 0,
2083
- rafHandle: requestAnimationFrame(this.loop)
2084
- });
2085
- }
2086
- /**
2087
- * Stop the requestAnimationFrame loop.
2088
- * Cancels any pending RAF callback.
2089
- * No-op if not running.
2090
- * @returns {void}
2091
- */
2092
- stop() {
2093
- if (!this.loopState.running) return;
2094
- this.loopState.running = false;
2095
- if (this.loopState.rafHandle !== null) {
2096
- cancelAnimationFrame(this.loopState.rafHandle);
2097
- this.loopState.rafHandle = null;
2098
- }
2099
- this.stoppedTime = performance.now();
2100
- }
2101
- /**
2102
- * Request frames to be rendered in demand mode.
2103
- * Accumulates pending frames (capped at 60) and starts the loop if not running.
2104
- * No-op if frameloop is not 'demand'.
2105
- * @param {number} [frames=1] - Number of frames to request
2106
- * @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
2107
- * - `false` (default): Sets pending frames to the specified value (replaces existing count)
2108
- * - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
2109
- * @returns {void}
2110
- * @example
2111
- * // Request a single frame render
2112
- * scheduler.invalidate();
2113
- *
2114
- * @example
2115
- * // Request 5 frames (e.g., for animations)
2116
- * scheduler.invalidate(5);
2117
- *
2118
- * @example
2119
- * // Set pending frames to exactly 3 (don't stack with existing)
2120
- * scheduler.invalidate(3, false);
2121
- *
2122
- * @example
2123
- * // Add 2 more frames to existing pending count
2124
- * scheduler.invalidate(2, true);
2125
- */
2126
- invalidate(frames = 1, stackFrames = false) {
2127
- if (this._frameloop !== "demand") return;
2128
- const baseFrames = stackFrames ? this.pendingFrames : 0;
2129
- this.pendingFrames = Math.min(60, baseFrames + frames);
2130
- if (!this.loopState.running && this.pendingFrames > 0) this.start();
2131
- }
2132
- /**
2133
- * Reset timing state for deterministic testing.
2134
- * Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
2135
- * @returns {void}
2136
- */
2137
- resetTiming() {
2138
- this.loopState.lastTime = null;
2139
- this.loopState.frameCount = 0;
2140
- this.loopState.elapsedTime = 0;
2141
- this.loopState.createdAt = performance.now();
2142
- }
2143
- //* Manual Stepping Methods ================================
2144
- /**
2145
- * Manually execute a single frame for all roots.
2146
- * Useful for frameloop='never' mode or testing scenarios.
2147
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2148
- * @returns {void}
2149
- * @example
2150
- * // Manual control mode
2151
- * scheduler.frameloop = 'never';
2152
- * scheduler.step(); // Execute one frame
2153
- */
2154
- step(timestamp) {
2155
- const now = timestamp ?? performance.now();
2156
- this.executeFrame(now);
2157
- }
2158
- /**
2159
- * Manually execute a single job by its ID.
2160
- * Useful for testing individual job callbacks in isolation.
2161
- * @param {string} id - The job ID to step
2162
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2163
- * @returns {void}
2164
- */
2165
- stepJob(id, timestamp) {
2166
- let job;
2167
- let root;
2168
- for (const r of this.roots.values()) {
2169
- job = r.jobs.get(id);
2170
- if (job) {
2171
- root = r;
2172
- break;
2173
- }
2174
- }
2175
- if (!job || !root) {
2176
- console.warn(`[Scheduler] Job "${id}" not found`);
2177
- return;
2178
- }
2179
- const now = timestamp ?? performance.now();
2180
- const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
2181
- const delta = deltaMs / 1e3;
2182
- const elapsed = now - this.loopState.createdAt;
2183
- const providedState = root.getState?.() ?? {};
2184
- const frameState = {
2185
- ...providedState,
2186
- time: now,
2187
- delta,
2188
- elapsed,
2189
- frame: this.loopState.frameCount
2190
- };
2191
- try {
2192
- job.callback(frameState, delta);
2193
- } catch (error) {
2194
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2195
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2196
- }
2197
- }
2198
- /**
2199
- * Execute a single frame across all roots.
2200
- * Order: globalBefore → each root's jobs → globalAfter
2201
- * @param {number} timestamp - RAF timestamp in milliseconds
2202
- * @returns {void}
2203
- * @private
2204
- */
2205
- executeFrame(timestamp) {
2206
- const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
2207
- const delta = deltaMs / 1e3;
2208
- this.loopState.lastTime = timestamp;
2209
- this.loopState.frameCount++;
2210
- this.loopState.elapsedTime += deltaMs;
2211
- this.runGlobalJobs(this.globalBeforeJobs, timestamp);
2212
- for (const root of this.roots.values()) {
2213
- this.tickRoot(root, timestamp, delta);
2214
- }
2215
- this.runGlobalJobs(this.globalAfterJobs, timestamp);
2216
- }
2217
- /**
2218
- * Run all global jobs from a job map.
2219
- * Catches and logs errors without stopping execution.
2220
- * @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
2221
- * @param {number} timestamp - RAF timestamp in milliseconds
2222
- * @returns {void}
2223
- * @private
2224
- */
2225
- runGlobalJobs(jobs, timestamp) {
2226
- for (const job of jobs.values()) {
2227
- try {
2228
- job.callback(timestamp);
2229
- } catch (error) {
2230
- console.error(`[Scheduler] Error in global job "${job.id}":`, error);
2231
- }
2232
- }
2233
- }
2234
- /**
2235
- * Execute all jobs for a single root in sorted order.
2236
- * Rebuilds sorted job list if needed, then dispatches each job.
2237
- * Errors are caught and propagated via triggerError.
2238
- * @param {RootEntry} root - The root entry to tick
2239
- * @param {number} timestamp - RAF timestamp in milliseconds
2240
- * @param {number} delta - Time since last frame in seconds
2241
- * @returns {void}
2242
- * @private
2243
- */
2244
- tickRoot(root, timestamp, delta) {
2245
- if (root.needsRebuild) {
2246
- root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
2247
- root.needsRebuild = false;
2248
- }
2249
- const providedState = root.getState?.() ?? {};
2250
- const frameState = {
2251
- ...providedState,
2252
- time: timestamp,
2253
- delta,
2254
- elapsed: this.loopState.elapsedTime / 1e3,
2255
- // Convert ms to seconds
2256
- frame: this.loopState.frameCount
2257
- };
2258
- for (const job of root.sortedJobs) {
2259
- if (!shouldRun(job, timestamp)) continue;
2260
- try {
2261
- job.callback(frameState, delta);
2262
- } catch (error) {
2263
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2264
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2265
- }
2266
- }
2267
- }
2268
- //* Debug & Inspection Methods ================================
2269
- /**
2270
- * Get the total number of registered jobs across all roots.
2271
- * Includes both per-root jobs and global before/after jobs.
2272
- * @returns {number} Total job count
2273
- */
2274
- getJobCount() {
2275
- let count = 0;
2276
- for (const root of this.roots.values()) {
2277
- count += root.jobs.size;
2278
- }
2279
- return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
2280
- }
2281
- /**
2282
- * Get all registered job IDs across all roots.
2283
- * Includes both per-root jobs and global before/after jobs.
2284
- * @returns {string[]} Array of all job IDs
2285
- */
2286
- getJobIds() {
2287
- const ids = [];
2288
- for (const root of this.roots.values()) {
2289
- ids.push(...root.jobs.keys());
2290
- }
2291
- ids.push(...this.globalBeforeJobs.keys());
2292
- ids.push(...this.globalAfterJobs.keys());
2293
- return ids;
2294
- }
2295
- /**
2296
- * Get the number of registered roots (Canvas instances).
2297
- * @returns {number} Number of registered roots
2298
- */
2299
- getRootCount() {
2300
- return this.roots.size;
2301
- }
2302
- /**
2303
- * Check if any user (non-system) jobs are registered in a specific phase.
2304
- * Used by the default render job to know if a user has taken over rendering.
2305
- *
2306
- * @param phase The phase to check
2307
- * @param rootId Optional root ID to check (checks all roots if not provided)
2308
- * @returns true if any user jobs exist in the phase
2309
- */
2310
- hasUserJobsInPhase(phase, rootId) {
2311
- const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
2312
- return rootsToCheck.some((root) => {
2313
- if (!root) return false;
2314
- for (const job of root.jobs.values()) {
2315
- if (job.phase === phase && !job.system && job.enabled) return true;
2316
- }
2317
- return false;
2318
- });
2319
- }
2320
- //* Utility Methods ================================
2321
- /**
2322
- * Generate a unique root ID for automatic root registration.
2323
- * @returns {string} A unique root ID in the format 'root_N'
2324
- */
2325
- generateRootId() {
2326
- return `root_${this.nextRootIndex++}`;
2327
- }
2328
- /**
2329
- * Generate a unique job ID.
2330
- * @returns {string} A unique job ID in the format 'job_N'
2331
- * @private
2332
- */
2333
- generateJobId() {
2334
- return `job_${this.nextJobIndex}`;
2335
- }
2336
- /**
2337
- * Normalize before/after constraints to a Set.
2338
- * Handles undefined, single string, or array inputs.
2339
- * @param {string | string[] | undefined} value - The constraint value(s)
2340
- * @returns {Set<string>} Normalized Set of constraint strings
2341
- * @private
2342
- */
2343
- normalizeConstraints(value) {
2344
- if (!value) return /* @__PURE__ */ new Set();
2345
- if (Array.isArray(value)) return new Set(value);
2346
- return /* @__PURE__ */ new Set([value]);
2347
- }
2348
- };
2349
- //* Static State & Methods (Singleton Usage) ================================
2350
- //* Cross-Bundle Singleton Key ==============================
2351
- // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2352
- // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2353
- __publicField(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
2354
- let Scheduler = _Scheduler;
2355
- const getScheduler = () => Scheduler.get();
2356
- if (hmrData) {
2357
- hmrData.accept?.();
2358
- }
2359
-
2360
2001
  function useFrame(callback, priorityOrOptions) {
2361
2002
  const store = React.useContext(context);
2362
2003
  const isInsideCanvas = store !== null;
@@ -2429,20 +2070,14 @@ function useFrame(callback, priorityOrOptions) {
2429
2070
  }
2430
2071
  };
2431
2072
  } else {
2432
- const registerOutside = () => {
2433
- return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
2434
- };
2435
- if (scheduler.independent || scheduler.isReady) {
2436
- return registerOutside();
2437
- }
2438
- let unregisterJob = null;
2439
- const unsubReady = scheduler.onRootReady(() => {
2440
- unregisterJob = registerOutside();
2441
- });
2442
- return () => {
2443
- unsubReady();
2444
- unregisterJob?.();
2445
- };
2073
+ return scheduler.register(
2074
+ (state, delta) => {
2075
+ const frameState = state;
2076
+ if (!frameState.renderer) return;
2077
+ callbackRef.current?.(frameState, delta);
2078
+ },
2079
+ { id, ...options }
2080
+ );
2446
2081
  }
2447
2082
  }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2448
2083
  const isPaused = React.useSyncExternalStore(
@@ -2533,22 +2168,29 @@ function buildFromCache(input, textureCache) {
2533
2168
  function useTexture(input, optionsOrOnLoad) {
2534
2169
  const renderer = useThree((state) => state.internal.actualRenderer);
2535
2170
  const store = useStore();
2536
- const textureCache = useThree((state) => state.textures);
2537
2171
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
2538
- const { onLoad, cache = false } = options;
2172
+ const { onLoad, cache = true } = options;
2173
+ const onLoadRef = useRef(onLoad);
2174
+ onLoadRef.current = onLoad;
2175
+ const onLoadCalledForRef = useRef(null);
2539
2176
  const urls = useMemo(() => getUrls(input), [input]);
2540
2177
  const cachedResult = useMemo(() => {
2541
2178
  if (!cache) return null;
2542
- if (!allUrlsCached(urls, textureCache)) return null;
2543
- return buildFromCache(input, textureCache);
2544
- }, [cache, urls, textureCache, input]);
2179
+ const textures = store.getState().textures;
2180
+ if (!allUrlsCached(urls, textures)) return null;
2181
+ return buildFromCache(input, textures);
2182
+ }, [cache, urls, input, store]);
2545
2183
  const loadedTextures = useLoader(
2546
2184
  TextureLoader,
2547
2185
  IsObject(input) ? Object.values(input) : input
2548
2186
  );
2187
+ const inputKey = urls.join("\0");
2549
2188
  useLayoutEffect(() => {
2550
- if (!cachedResult) onLoad?.(loadedTextures);
2551
- }, [onLoad, cachedResult, loadedTextures]);
2189
+ if (cachedResult) return;
2190
+ if (onLoadCalledForRef.current === inputKey) return;
2191
+ onLoadCalledForRef.current = inputKey;
2192
+ onLoadRef.current?.(loadedTextures);
2193
+ }, [cachedResult, loadedTextures, inputKey]);
2552
2194
  useEffect(() => {
2553
2195
  if (cachedResult) return;
2554
2196
  if ("initTexture" in renderer) {
@@ -2581,8 +2223,6 @@ function useTexture(input, optionsOrOnLoad) {
2581
2223
  }, [input, loadedTextures, cachedResult]);
2582
2224
  useEffect(() => {
2583
2225
  if (!cache) return;
2584
- if (cachedResult) return;
2585
- const set = store.setState;
2586
2226
  const urlTextureMap = [];
2587
2227
  if (typeof input === "string") {
2588
2228
  urlTextureMap.push([input, mappedTextures]);
@@ -2596,18 +2236,32 @@ function useTexture(input, optionsOrOnLoad) {
2596
2236
  urlTextureMap.push([url, textureRecord[key]]);
2597
2237
  }
2598
2238
  }
2599
- set((state) => {
2600
- const newMap = new Map(state.textures);
2601
- let changed = false;
2239
+ store.setState((state) => {
2240
+ const refs = new Map(state._textureRefs);
2241
+ let textures = state.textures;
2242
+ let added = false;
2602
2243
  for (const [url, texture] of urlTextureMap) {
2603
- if (!newMap.has(url)) {
2604
- newMap.set(url, texture);
2605
- changed = true;
2244
+ if (!textures.has(url)) {
2245
+ if (!added) {
2246
+ textures = new Map(textures);
2247
+ added = true;
2248
+ }
2249
+ textures.set(url, texture);
2606
2250
  }
2251
+ refs.set(url, (refs.get(url) ?? 0) + 1);
2607
2252
  }
2608
- return changed ? { textures: newMap } : state;
2253
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
2254
+ });
2255
+ return () => store.setState((state) => {
2256
+ const refs = new Map(state._textureRefs);
2257
+ for (const [url] of urlTextureMap) {
2258
+ const next = (refs.get(url) ?? 0) - 1;
2259
+ if (next <= 0) refs.delete(url);
2260
+ else refs.set(url, next);
2261
+ }
2262
+ return { _textureRefs: refs };
2609
2263
  });
2610
- }, [cache, input, mappedTextures, store, cachedResult]);
2264
+ }, [cache, input, mappedTextures, store]);
2611
2265
  return mappedTextures;
2612
2266
  }
2613
2267
  useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
@@ -2623,108 +2277,92 @@ const Texture = ({
2623
2277
  return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
2624
2278
  };
2625
2279
 
2626
- function getTextureValue(entry) {
2627
- if (entry instanceof Texture$1) return entry;
2628
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof Texture$1) {
2629
- return entry.value;
2630
- }
2631
- return null;
2632
- }
2633
- function useTextures() {
2280
+ function useTextures(selector) {
2634
2281
  const store = useStore();
2635
- return useMemo(() => {
2636
- const set = store.setState;
2282
+ const registry = useMemo(() => {
2637
2283
  const getState = store.getState;
2638
- const add = (key, value) => {
2639
- set((state) => {
2640
- const newMap = new Map(state.textures);
2641
- newMap.set(key, value);
2642
- return { textures: newMap };
2643
- });
2644
- };
2645
- const addMultiple = (items) => {
2646
- set((state) => {
2647
- const newMap = new Map(state.textures);
2648
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
2649
- for (const [key, value] of entries) {
2650
- newMap.set(key, value);
2651
- }
2652
- return { textures: newMap };
2653
- });
2654
- };
2655
- const remove = (key) => {
2656
- set((state) => {
2657
- const newMap = new Map(state.textures);
2658
- newMap.delete(key);
2659
- return { textures: newMap };
2660
- });
2661
- };
2662
- const removeMultiple = (keys) => {
2663
- set((state) => {
2664
- const newMap = new Map(state.textures);
2665
- for (const key of keys) newMap.delete(key);
2666
- return { textures: newMap };
2667
- });
2668
- };
2669
- const dispose = (key) => {
2670
- const entry = getState().textures.get(key);
2671
- if (entry) {
2672
- const tex = getTextureValue(entry);
2673
- tex?.dispose();
2674
- }
2675
- remove(key);
2676
- };
2677
- const disposeMultiple = (keys) => {
2678
- const textures = getState().textures;
2679
- for (const key of keys) {
2680
- const entry = textures.get(key);
2681
- if (entry) {
2682
- const tex = getTextureValue(entry);
2683
- tex?.dispose();
2684
- }
2685
- }
2686
- removeMultiple(keys);
2687
- };
2688
- const disposeAll = () => {
2689
- const textures = getState().textures;
2690
- for (const entry of textures.values()) {
2691
- const tex = getTextureValue(entry);
2692
- tex?.dispose();
2693
- }
2694
- set({ textures: /* @__PURE__ */ new Map() });
2695
- };
2284
+ const setState = store.setState;
2285
+ const getOne = (key) => getState().textures.get(key);
2696
2286
  return {
2697
- // Getter for the textures Map (reactive via getState)
2698
- get textures() {
2287
+ get all() {
2699
2288
  return getState().textures;
2700
2289
  },
2701
- // Read
2702
- get: (key) => getState().textures.get(key),
2290
+ get(input) {
2291
+ if (typeof input === "string") return getOne(input);
2292
+ if (Array.isArray(input)) return input.map(getOne);
2293
+ const out = {};
2294
+ for (const name in input) out[name] = getOne(input[name]);
2295
+ return out;
2296
+ },
2703
2297
  has: (key) => getState().textures.has(key),
2704
- // Write
2705
- add,
2706
- addMultiple,
2707
- // Remove (cache only)
2708
- remove,
2709
- removeMultiple,
2710
- // Dispose (GPU + cache)
2711
- dispose,
2712
- disposeMultiple,
2713
- disposeAll
2298
+ add(keyOrRecord, texture) {
2299
+ setState((state) => {
2300
+ const textures = new Map(state.textures);
2301
+ if (typeof keyOrRecord === "string") {
2302
+ textures.set(keyOrRecord, texture);
2303
+ } else {
2304
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2305
+ }
2306
+ return { textures };
2307
+ });
2308
+ },
2309
+ dispose(key, options) {
2310
+ const state = getState();
2311
+ const refs = state._textureRefs.get(key) ?? 0;
2312
+ if (refs > 0 && !options?.force) {
2313
+ console.warn(
2314
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2315
+ );
2316
+ return false;
2317
+ }
2318
+ state.textures.get(key)?.dispose();
2319
+ setState((s) => {
2320
+ const textures = new Map(s.textures);
2321
+ textures.delete(key);
2322
+ const nextRefs = new Map(s._textureRefs);
2323
+ nextRefs.delete(key);
2324
+ return { textures, _textureRefs: nextRefs };
2325
+ });
2326
+ return true;
2327
+ },
2328
+ disposeAll() {
2329
+ for (const texture of getState().textures.values()) texture.dispose();
2330
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2331
+ }
2714
2332
  };
2715
2333
  }, [store]);
2334
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2335
+ const selected = useThree(subscribe);
2336
+ return selector ? selected : registry;
2716
2337
  }
2717
2338
 
2718
- function useRenderTarget(width, height, options) {
2339
+ function useRenderTarget(widthOrOptions, heightOrOptions, options) {
2719
2340
  const isLegacy = useThree((s) => s.isLegacy);
2720
2341
  const size = useThree((s) => s.size);
2342
+ let width;
2343
+ let height;
2344
+ let opts;
2345
+ if (typeof widthOrOptions === "object") {
2346
+ opts = widthOrOptions;
2347
+ } else if (typeof widthOrOptions === "number") {
2348
+ width = widthOrOptions;
2349
+ if (typeof heightOrOptions === "object") {
2350
+ height = widthOrOptions;
2351
+ opts = heightOrOptions;
2352
+ } else if (typeof heightOrOptions === "number") {
2353
+ height = heightOrOptions;
2354
+ opts = options;
2355
+ } else {
2356
+ height = widthOrOptions;
2357
+ }
2358
+ }
2721
2359
  return useMemo(() => {
2722
2360
  const w = width ?? size.width;
2723
2361
  const h = height ?? size.height;
2724
2362
  {
2725
- return isLegacy ? new WebGLRenderTarget(w, h, options) : new RenderTarget(w, h, options);
2363
+ return isLegacy ? new WebGLRenderTarget(w, h, opts) : new RenderTarget(w, h, opts);
2726
2364
  }
2727
- }, [width, height, size.width, size.height, options, isLegacy]);
2365
+ }, [width, height, size.width, size.height, opts, isLegacy]);
2728
2366
  }
2729
2367
 
2730
2368
  function useStore() {
@@ -2774,11 +2412,11 @@ function addTail(callback) {
2774
2412
  function invalidate(state, frames = 1, stackFrames = false) {
2775
2413
  getScheduler().invalidate(frames, stackFrames);
2776
2414
  }
2777
- function advance(timestamp, runGlobalEffects = true, state, frame) {
2415
+ function advance(timestamp) {
2778
2416
  getScheduler().step(timestamp);
2779
2417
  }
2780
2418
 
2781
- const version = "10.0.0-alpha.2";
2419
+ const version = "10.0.0-alpha.3";
2782
2420
  const packageData = {
2783
2421
  version: version};
2784
2422
 
@@ -14217,6 +13855,7 @@ function swapInstances() {
14217
13855
  }
14218
13856
  }
14219
13857
  if (instance.isHidden) unhideInstance(instance);
13858
+ if (isObject3D(instance.object)) removeInteractivity(findInitialRoot(instance), instance.object);
14220
13859
  if (instance.object.__r3f) delete instance.object.__r3f;
14221
13860
  if (instance.type !== "primitive") disposeOnIdle(instance.object);
14222
13861
  }
@@ -14228,6 +13867,9 @@ function swapInstances() {
14228
13867
  instance.object = instance.props.object ?? new target(...instance.props.args ?? []);
14229
13868
  instance.object.__r3f = instance;
14230
13869
  setFiberRef(fiber, instance.object);
13870
+ delete instance.appliedOnce;
13871
+ instance.eventCount = 0;
13872
+ instance.handlers = {};
14231
13873
  applyProps(instance.object, instance.props);
14232
13874
  if (instance.props.attach) {
14233
13875
  attach(parent, instance);
@@ -14301,8 +13943,22 @@ const reconciler = /* @__PURE__ */ createReconciler({
14301
13943
  const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
14302
13944
  if (isTailSibling) swapInstances();
14303
13945
  },
14304
- finalizeInitialChildren: () => false,
14305
- commitMount() {
13946
+ finalizeInitialChildren: (instance) => {
13947
+ for (const prop in instance.props) {
13948
+ if (isFromRef(instance.props[prop])) return true;
13949
+ }
13950
+ return false;
13951
+ },
13952
+ commitMount(instance) {
13953
+ const resolved = {};
13954
+ for (const prop in instance.props) {
13955
+ const value = instance.props[prop];
13956
+ if (isFromRef(value)) {
13957
+ const ref = value[FROM_REF];
13958
+ if (ref.current != null) resolved[prop] = ref.current;
13959
+ }
13960
+ }
13961
+ if (Object.keys(resolved).length) applyProps(instance.object, resolved);
14306
13962
  },
14307
13963
  getPublicInstance: (instance) => instance?.object,
14308
13964
  prepareForCommit: () => null,
@@ -14429,11 +14085,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
14429
14085
  cloneMutableTextInstance(textInstance) {
14430
14086
  return textInstance;
14431
14087
  },
14088
+ // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
14089
+ // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
14090
+ // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
14091
+ // React never calls them. They stay as no-ops rather than throws: the host config is shared by
14092
+ // every root, and a throw here would turn a React-internal probe into a hard crash for users.
14093
+ // Guarded by a test asserting a full render + unmount never reaches them.
14432
14094
  cloneRootViewTransitionContainer(_rootContainer) {
14433
- throw new Error("Not implemented.");
14434
14095
  },
14435
14096
  removeRootViewTransitionClone(_rootContainer, _clone) {
14436
- throw new Error("Not implemented.");
14437
14097
  },
14438
14098
  // https://github.com/facebook/react/pull/32465
14439
14099
  createFragmentInstance: (_fiber) => null,
@@ -14518,11 +14178,15 @@ function createRoot(canvas) {
14518
14178
  const lastConfiguredProps = {};
14519
14179
  let configured = false;
14520
14180
  let pending = null;
14181
+ let rendererSetup = null;
14521
14182
  return {
14522
14183
  async configure(props = {}) {
14523
14184
  let resolve;
14524
14185
  pending = new Promise((_resolve) => resolve = _resolve);
14525
14186
  const {
14187
+ id: canvasId,
14188
+ primaryCanvas,
14189
+ scheduler: schedulerConfig,
14526
14190
  gl: glConfig,
14527
14191
  renderer: rendererConfig,
14528
14192
  size: propsSize,
@@ -14530,10 +14194,6 @@ function createRoot(canvas) {
14530
14194
  events,
14531
14195
  onCreated: onCreatedCallback,
14532
14196
  shadows = false,
14533
- linear = false,
14534
- flat = false,
14535
- textureColorSpace = SRGBColorSpace,
14536
- legacy = false,
14537
14197
  orthographic = false,
14538
14198
  frameloop = "always",
14539
14199
  dpr = [1, 2],
@@ -14545,9 +14205,11 @@ function createRoot(canvas) {
14545
14205
  onDropMissed,
14546
14206
  autoUpdateFrustum = true,
14547
14207
  occlusion = false,
14548
- _sizeProps
14208
+ _sizeProps,
14209
+ forceEven
14549
14210
  } = props;
14550
- const state = store.getState();
14211
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || SRGBColorSpace;
14212
+ let state = store.getState();
14551
14213
  const defaultGLProps = {
14552
14214
  canvas,
14553
14215
  powerPreference: "high-performance",
@@ -14555,7 +14217,8 @@ function createRoot(canvas) {
14555
14217
  alpha: true
14556
14218
  };
14557
14219
  const defaultGPUProps = {
14558
- canvas
14220
+ canvas,
14221
+ antialias: true
14559
14222
  };
14560
14223
  const wantsGL = (state.isLegacy || glConfig || !R3F_BUILD_WEBGPU || !rendererConfig);
14561
14224
  if (glConfig && rendererConfig) {
@@ -14569,19 +14232,73 @@ function createRoot(canvas) {
14569
14232
  });
14570
14233
  }
14571
14234
  let renderer = state.internal.actualRenderer;
14572
- if (wantsGL && !state.internal.actualRenderer) {
14573
- renderer = await resolveRenderer(glConfig, defaultGLProps, WebGLRenderer);
14574
- state.internal.actualRenderer = renderer;
14575
- state.set({ isLegacy: true, gl: renderer, renderer });
14576
- } else if (!wantsGL && !state.internal.actualRenderer) {
14577
- renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
14578
- if (!renderer.hasInitialized?.()) {
14579
- await renderer.init();
14580
- }
14581
- const backend = renderer.backend;
14582
- const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14583
- state.internal.actualRenderer = renderer;
14584
- state.set({ webGPUSupported: isWebGPUBackend, renderer });
14235
+ if (primaryCanvas && wantsGL) {
14236
+ throw new Error(
14237
+ "The `primaryCanvas` prop for multi-canvas rendering cannot be used with WebGL. Remove the `gl` prop or use WebGPU."
14238
+ );
14239
+ }
14240
+ if (!state.internal.actualRenderer) {
14241
+ if (!rendererSetup) {
14242
+ rendererSetup = (async () => {
14243
+ if (wantsGL) {
14244
+ renderer = await resolveRenderer(glConfig, defaultGLProps, WebGLRenderer);
14245
+ state.internal.actualRenderer = renderer;
14246
+ state.set({ isLegacy: true, gl: renderer, renderer, primaryStore: store });
14247
+ } else if (!wantsGL && primaryCanvas) {
14248
+ const primary = await waitForPrimary(primaryCanvas);
14249
+ renderer = primary.renderer;
14250
+ state.internal.actualRenderer = renderer;
14251
+ const canvasTarget = new CanvasTarget(canvas);
14252
+ primary.store.setState((prev) => ({
14253
+ internal: { ...prev.internal, isMultiCanvas: true }
14254
+ }));
14255
+ state.set((prev) => ({
14256
+ webGPUSupported: primary.store.getState().webGPUSupported,
14257
+ renderer,
14258
+ primaryStore: primary.store,
14259
+ internal: {
14260
+ ...prev.internal,
14261
+ canvasTarget,
14262
+ isMultiCanvas: true,
14263
+ isSecondary: true,
14264
+ targetId: primaryCanvas
14265
+ }
14266
+ }));
14267
+ } else if (!wantsGL) {
14268
+ renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
14269
+ if (!renderer.hasInitialized?.()) {
14270
+ const size2 = computeInitialSize(canvas, propsSize);
14271
+ if (size2.width > 0 && size2.height > 0) {
14272
+ const pixelRatio = calculateDpr(dpr);
14273
+ canvas.width = size2.width * pixelRatio;
14274
+ canvas.height = size2.height * pixelRatio;
14275
+ }
14276
+ await renderer.init();
14277
+ }
14278
+ const backend = renderer.backend;
14279
+ const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14280
+ state.internal.actualRenderer = renderer;
14281
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14282
+ if (canvasId && !state.internal.isSecondary) {
14283
+ const canvasTarget = new CanvasTarget(canvas);
14284
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14285
+ state.set((prev) => ({
14286
+ internal: {
14287
+ ...prev.internal,
14288
+ canvasTarget,
14289
+ unregisterPrimary
14290
+ }
14291
+ }));
14292
+ }
14293
+ }
14294
+ })().catch((err) => {
14295
+ rendererSetup = null;
14296
+ throw err;
14297
+ });
14298
+ }
14299
+ await rendererSetup;
14300
+ state = store.getState();
14301
+ renderer = state.internal.actualRenderer;
14585
14302
  }
14586
14303
  let raycaster = state.raycaster;
14587
14304
  if (!raycaster) state.set({ raycaster: raycaster = new Raycaster() });
@@ -14590,6 +14307,7 @@ function createRoot(canvas) {
14590
14307
  if (!is.equ(params, raycaster.params, shallowLoose)) {
14591
14308
  applyProps(raycaster, { params: { ...raycaster.params, ...params } });
14592
14309
  }
14310
+ let tempCamera = state.camera;
14593
14311
  if (!state.camera || state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose)) {
14594
14312
  lastCamera = cameraOptions;
14595
14313
  const isCamera = cameraOptions?.isCamera;
@@ -14609,6 +14327,7 @@ function createRoot(canvas) {
14609
14327
  if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0);
14610
14328
  }
14611
14329
  state.set({ camera });
14330
+ tempCamera = camera;
14612
14331
  raycaster.camera = camera;
14613
14332
  }
14614
14333
  if (!state.scene) {
@@ -14626,7 +14345,7 @@ function createRoot(canvas) {
14626
14345
  rootScene: scene,
14627
14346
  internal: { ...prev.internal, container: scene }
14628
14347
  }));
14629
- const camera = state.camera;
14348
+ const camera = tempCamera;
14630
14349
  if (camera && !camera.parent) scene.add(camera);
14631
14350
  }
14632
14351
  if (events && !state.events.handlers) {
@@ -14643,6 +14362,9 @@ function createRoot(canvas) {
14643
14362
  if (_sizeProps !== void 0) {
14644
14363
  state.set({ _sizeProps });
14645
14364
  }
14365
+ if (forceEven !== void 0 && state.internal.forceEven !== forceEven) {
14366
+ state.set((prev) => ({ internal: { ...prev.internal, forceEven } }));
14367
+ }
14646
14368
  const size = computeInitialSize(canvas, propsSize);
14647
14369
  if (!state._sizeImperative && !is.equ(size, state.size, shallowLoose)) {
14648
14370
  const wasImperative = state._sizeImperative;
@@ -14669,10 +14391,10 @@ function createRoot(canvas) {
14669
14391
  lastConfiguredProps.performance = performance;
14670
14392
  }
14671
14393
  if (!state.xr) {
14672
- const handleXRFrame = (timestamp, frame) => {
14394
+ const handleXRFrame = (timestamp, _frame) => {
14673
14395
  const state2 = store.getState();
14674
14396
  if (state2.frameloop === "never") return;
14675
- advance(timestamp, true);
14397
+ advance(timestamp);
14676
14398
  };
14677
14399
  const actualRenderer = state.internal.actualRenderer;
14678
14400
  const handleSessionChange = () => {
@@ -14684,16 +14406,16 @@ function createRoot(canvas) {
14684
14406
  };
14685
14407
  const xr = {
14686
14408
  connect() {
14687
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14688
- const actualRenderer2 = renderer2 || gl;
14689
- actualRenderer2.xr.addEventListener("sessionstart", handleSessionChange);
14690
- actualRenderer2.xr.addEventListener("sessionend", handleSessionChange);
14409
+ const { gl, renderer: renderer2 } = store.getState();
14410
+ const xrManager = (renderer2 || gl).xr;
14411
+ xrManager.addEventListener("sessionstart", handleSessionChange);
14412
+ xrManager.addEventListener("sessionend", handleSessionChange);
14691
14413
  },
14692
14414
  disconnect() {
14693
- const { gl, renderer: renderer2, isLegacy } = store.getState();
14694
- const actualRenderer2 = renderer2 || gl;
14695
- actualRenderer2.xr.removeEventListener("sessionstart", handleSessionChange);
14696
- actualRenderer2.xr.removeEventListener("sessionend", handleSessionChange);
14415
+ const { gl, renderer: renderer2 } = store.getState();
14416
+ const xrManager = (renderer2 || gl).xr;
14417
+ xrManager.removeEventListener("sessionstart", handleSessionChange);
14418
+ xrManager.removeEventListener("sessionend", handleSessionChange);
14697
14419
  }
14698
14420
  };
14699
14421
  if (typeof renderer.xr?.addEventListener === "function") xr.connect();
@@ -14705,15 +14427,22 @@ function createRoot(canvas) {
14705
14427
  const oldType = renderer.shadowMap.type;
14706
14428
  renderer.shadowMap.enabled = !!shadows;
14707
14429
  if (is.boo(shadows)) {
14708
- renderer.shadowMap.type = PCFSoftShadowMap;
14430
+ renderer.shadowMap.type = PCFShadowMap;
14709
14431
  } else if (is.str(shadows)) {
14432
+ if (shadows === "soft") {
14433
+ notifyDepreciated({
14434
+ heading: 'shadows="soft" is deprecated',
14435
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14436
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14437
+ });
14438
+ }
14710
14439
  const types = {
14711
14440
  basic: BasicShadowMap,
14712
14441
  percentage: PCFShadowMap,
14713
- soft: PCFSoftShadowMap,
14442
+ soft: PCFShadowMap,
14714
14443
  variance: VSMShadowMap
14715
14444
  };
14716
- renderer.shadowMap.type = types[shadows] ?? PCFSoftShadowMap;
14445
+ renderer.shadowMap.type = types[shadows] ?? PCFShadowMap;
14717
14446
  } else if (is.obj(shadows)) {
14718
14447
  Object.assign(renderer.shadowMap, shadows);
14719
14448
  }
@@ -14721,56 +14450,69 @@ function createRoot(canvas) {
14721
14450
  renderer.shadowMap.needsUpdate = true;
14722
14451
  }
14723
14452
  }
14724
- {
14725
- const legacyChanged = legacy !== lastConfiguredProps.legacy;
14726
- const linearChanged = linear !== lastConfiguredProps.linear;
14727
- const flatChanged = flat !== lastConfiguredProps.flat;
14728
- if (legacyChanged) {
14729
- if (legacy) {
14730
- notifyDepreciated({
14731
- heading: "Legacy Color Management",
14732
- body: "Legacy color management is deprecated and will be removed in a future version.",
14733
- link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
14734
- });
14735
- }
14736
- }
14737
- if (legacyChanged) {
14738
- ColorManagement.enabled = !legacy;
14739
- lastConfiguredProps.legacy = legacy;
14740
- }
14741
- if (!configured || linearChanged) {
14742
- renderer.outputColorSpace = linear ? LinearSRGBColorSpace : SRGBColorSpace;
14743
- lastConfiguredProps.linear = linear;
14744
- }
14745
- if (!configured || flatChanged) {
14746
- renderer.toneMapping = flat ? NoToneMapping : ACESFilmicToneMapping;
14747
- lastConfiguredProps.flat = flat;
14748
- }
14749
- if (legacyChanged && state.legacy !== legacy) state.set(() => ({ legacy }));
14750
- if (linearChanged && state.linear !== linear) state.set(() => ({ linear }));
14751
- if (flatChanged && state.flat !== flat) state.set(() => ({ flat }));
14453
+ if (!configured) {
14454
+ renderer.outputColorSpace = SRGBColorSpace;
14455
+ renderer.toneMapping = ACESFilmicToneMapping;
14752
14456
  }
14753
14457
  if (textureColorSpace !== lastConfiguredProps.textureColorSpace) {
14754
14458
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
14755
14459
  lastConfiguredProps.textureColorSpace = textureColorSpace;
14756
14460
  }
14461
+ const r3fProps = ["textureColorSpace"];
14462
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14463
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
14757
14464
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
14758
- applyProps(renderer, glConfig);
14465
+ const glProps = {};
14466
+ for (const key in glConfig) {
14467
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14468
+ }
14469
+ applyProps(renderer, glProps);
14759
14470
  }
14760
14471
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
14761
14472
  const currentRenderer = state.renderer;
14762
14473
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
14763
- applyProps(currentRenderer, rendererConfig);
14474
+ const rendererProps = {};
14475
+ for (const key in rendererConfig) {
14476
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14477
+ }
14478
+ applyProps(currentRenderer, rendererProps);
14764
14479
  }
14765
14480
  }
14766
14481
  const scheduler = getScheduler();
14767
14482
  const rootId = state.internal.rootId;
14768
14483
  if (!rootId) {
14769
- const newRootId = scheduler.generateRootId();
14484
+ const newRootId = canvasId || scheduler.generateRootId();
14770
14485
  const unregisterRoot = scheduler.registerRoot(newRootId, {
14771
14486
  getState: () => store.getState(),
14772
14487
  onError: (err) => store.getState().setError(err)
14773
14488
  });
14489
+ const unregisterCanvasTarget = scheduler.register(
14490
+ () => {
14491
+ const state2 = store.getState();
14492
+ if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14493
+ const renderer2 = state2.internal.actualRenderer;
14494
+ renderer2.setCanvasTarget(state2.internal.canvasTarget);
14495
+ }
14496
+ },
14497
+ {
14498
+ id: `${newRootId}_canvasTarget`,
14499
+ rootId: newRootId,
14500
+ phase: "start",
14501
+ system: true
14502
+ }
14503
+ );
14504
+ const unregisterEventsFlush = scheduler.register(
14505
+ () => {
14506
+ const state2 = store.getState();
14507
+ state2.events.flush?.();
14508
+ },
14509
+ {
14510
+ id: `${newRootId}_events`,
14511
+ rootId: newRootId,
14512
+ phase: "input",
14513
+ system: true
14514
+ }
14515
+ );
14774
14516
  const unregisterFrustum = scheduler.register(
14775
14517
  () => {
14776
14518
  const state2 = store.getState();
@@ -14781,7 +14523,7 @@ function createRoot(canvas) {
14781
14523
  {
14782
14524
  id: `${newRootId}_frustum`,
14783
14525
  rootId: newRootId,
14784
- phase: "preRender",
14526
+ before: "render",
14785
14527
  system: true
14786
14528
  }
14787
14529
  );
@@ -14793,7 +14535,7 @@ function createRoot(canvas) {
14793
14535
  {
14794
14536
  id: `${newRootId}_visibility`,
14795
14537
  rootId: newRootId,
14796
- phase: "preRender",
14538
+ before: "render",
14797
14539
  system: true,
14798
14540
  after: `${newRootId}_frustum`
14799
14541
  }
@@ -14805,18 +14547,22 @@ function createRoot(canvas) {
14805
14547
  const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14806
14548
  if (userHandlesRender || state2.internal.priority) return;
14807
14549
  try {
14808
- if (state2.postProcessing?.render) state2.postProcessing.render();
14550
+ if (state2.renderPipeline?.render) state2.renderPipeline.render();
14809
14551
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
14810
14552
  } catch (error) {
14811
14553
  state2.setError(error instanceof Error ? error : new Error(String(error)));
14812
14554
  }
14813
14555
  },
14814
14556
  {
14815
- id: `${newRootId}_render`,
14557
+ // Use canvas ID directly as job ID if available, otherwise use generated rootId
14558
+ id: canvasId || `${newRootId}_render`,
14816
14559
  rootId: newRootId,
14817
14560
  phase: "render",
14818
- system: true
14561
+ system: true,
14819
14562
  // Internal flag: this is a system job, not user-controlled
14563
+ // Apply scheduler config for render ordering and rate limiting
14564
+ ...schedulerConfig?.after && { after: schedulerConfig.after },
14565
+ ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14820
14566
  }
14821
14567
  );
14822
14568
  state.set((state2) => ({
@@ -14825,6 +14571,8 @@ function createRoot(canvas) {
14825
14571
  rootId: newRootId,
14826
14572
  unregisterRoot: () => {
14827
14573
  unregisterRoot();
14574
+ unregisterCanvasTarget();
14575
+ unregisterEventsFlush();
14828
14576
  unregisterFrustum();
14829
14577
  unregisterVisibility();
14830
14578
  unregisterRender();
@@ -14883,15 +14631,24 @@ function unmountComponentAtNode(canvas, callback) {
14883
14631
  const renderer = state.internal.actualRenderer;
14884
14632
  const unregisterRoot = state.internal.unregisterRoot;
14885
14633
  if (unregisterRoot) unregisterRoot();
14634
+ const unregisterPrimary = state.internal.unregisterPrimary;
14635
+ if (unregisterPrimary) unregisterPrimary();
14636
+ const canvasTarget = state.internal.canvasTarget;
14637
+ if (canvasTarget?.dispose) canvasTarget.dispose();
14886
14638
  state.events.disconnect?.();
14887
14639
  cleanupHelperGroup(root.store);
14888
- renderer?.renderLists?.dispose?.();
14889
- renderer?.forceContextLoss?.();
14890
- if (renderer?.xr) state.xr.disconnect();
14640
+ if (state.isLegacy && renderer) {
14641
+ ;
14642
+ renderer.renderLists?.dispose?.();
14643
+ renderer.forceContextLoss?.();
14644
+ }
14645
+ if (!state.internal.isSecondary) {
14646
+ if (renderer?.xr) state.xr.disconnect();
14647
+ }
14891
14648
  dispose(state.scene);
14892
14649
  _roots.delete(canvas);
14893
14650
  if (callback) callback(canvas);
14894
- } catch (e) {
14651
+ } catch {
14895
14652
  }
14896
14653
  }, 500);
14897
14654
  }
@@ -14899,36 +14656,34 @@ function unmountComponentAtNode(canvas, callback) {
14899
14656
  }
14900
14657
  }
14901
14658
  function createPortal(children, container, state) {
14902
- return /* @__PURE__ */ jsx(PortalWrapper, { children, container, state });
14659
+ return /* @__PURE__ */ jsx(Portal, { children, container, state });
14903
14660
  }
14904
- function PortalWrapper({ children, container, state }) {
14661
+ function Portal({ children, container, state }) {
14905
14662
  const isRef = useCallback((obj) => obj && "current" in obj, []);
14906
- const [resolvedContainer, setResolvedContainer] = useState(() => {
14663
+ const [resolvedContainer, _setResolvedContainer] = useState(() => {
14907
14664
  if (isRef(container)) return container.current ?? null;
14908
14665
  return container;
14909
14666
  });
14667
+ const setResolvedContainer = useCallback(
14668
+ (newContainer) => {
14669
+ if (!newContainer || newContainer === resolvedContainer) return;
14670
+ _setResolvedContainer(isRef(newContainer) ? newContainer.current : newContainer);
14671
+ },
14672
+ [resolvedContainer, _setResolvedContainer, isRef]
14673
+ );
14910
14674
  useMemo(() => {
14911
- if (isRef(container)) {
14912
- const current = container.current;
14913
- if (!current) {
14914
- queueMicrotask(() => {
14915
- const updated = container.current;
14916
- if (updated && updated !== resolvedContainer) {
14917
- setResolvedContainer(updated);
14918
- }
14919
- });
14920
- } else if (current !== resolvedContainer) {
14921
- setResolvedContainer(current);
14922
- }
14923
- } else if (container !== resolvedContainer) {
14924
- setResolvedContainer(container);
14675
+ if (isRef(container) && !container.current) {
14676
+ return queueMicrotask(() => {
14677
+ setResolvedContainer(container.current);
14678
+ });
14925
14679
  }
14926
- }, [container, resolvedContainer, isRef]);
14680
+ setResolvedContainer(container);
14681
+ }, [container, isRef, setResolvedContainer]);
14927
14682
  if (!resolvedContainer) return /* @__PURE__ */ jsx(Fragment, {});
14928
14683
  const portalKey = resolvedContainer.uuid ?? `portal-${resolvedContainer.id ?? "unknown"}`;
14929
- return /* @__PURE__ */ jsx(Portal, { children, container: resolvedContainer, state }, portalKey);
14684
+ return /* @__PURE__ */ jsx(PortalInner, { children, container: resolvedContainer, state }, portalKey);
14930
14685
  }
14931
- function Portal({ state = {}, children, container }) {
14686
+ function PortalInner({ state = {}, children, container }) {
14932
14687
  const { events, size, injectScene = true, ...rest } = state;
14933
14688
  const previousRoot = useStore();
14934
14689
  const [raycaster] = useState(() => new Raycaster());
@@ -14949,12 +14704,14 @@ function Portal({ state = {}, children, container }) {
14949
14704
  };
14950
14705
  }, [portalScene, container, injectScene]);
14951
14706
  const inject = useMutableCallback((rootState, injectState) => {
14707
+ const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
14952
14708
  let viewport = void 0;
14953
- if (injectState.camera && size) {
14709
+ if (injectState.camera && (size || injectState.size)) {
14954
14710
  const camera = injectState.camera;
14955
- viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), size);
14956
- if (camera !== rootState.camera) updateCamera(camera, size);
14711
+ viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
14712
+ if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
14957
14713
  }
14714
+ const set = injectState.set;
14958
14715
  return {
14959
14716
  // The intersect consists of the previous root state
14960
14717
  ...rootState,
@@ -14970,10 +14727,10 @@ function Portal({ state = {}, children, container }) {
14970
14727
  previousRoot,
14971
14728
  // Events, size and viewport can be overridden by the inject layer
14972
14729
  events: { ...rootState.events, ...injectState.events, ...events },
14973
- size: { ...rootState.size, ...size },
14730
+ size: resolvedSize,
14974
14731
  viewport: { ...rootState.viewport, ...viewport },
14975
14732
  // Layers are allowed to override events
14976
- setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14733
+ setEvents: (events2) => set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14977
14734
  // Container for child attachment - the portalScene (injected or container itself)
14978
14735
  internal: { ...rootState.internal, ...injectState.internal, container: portalScene }
14979
14736
  };
@@ -14982,9 +14739,13 @@ function Portal({ state = {}, children, container }) {
14982
14739
  const store = createWithEqualityFn((set, get) => ({ ...rest, set, get }));
14983
14740
  const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
14984
14741
  onMutate(previousRoot.getState());
14985
- previousRoot.subscribe(onMutate);
14986
14742
  return store;
14987
14743
  }, [previousRoot, container]);
14744
+ useIsomorphicLayoutEffect(() => {
14745
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14746
+ const unsubscribe = previousRoot.subscribe(onMutate);
14747
+ return unsubscribe;
14748
+ }, [previousRoot, usePortalStore]);
14988
14749
  return (
14989
14750
  // @ts-ignore, reconciler types are not maintained
14990
14751
  /* @__PURE__ */ jsx(Fragment, { children: reconciler.createPortal(
@@ -14998,21 +14759,72 @@ function flushSync(fn) {
14998
14759
  return reconciler.flushSyncFromReconciler(fn);
14999
14760
  }
15000
14761
 
14762
+ function parseBackground(background) {
14763
+ if (!background) return null;
14764
+ if (typeof background === "object" && !background.isColor) {
14765
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14766
+ return {
14767
+ ...rest,
14768
+ preset,
14769
+ files: envMap || files,
14770
+ backgroundFiles: backgroundMap,
14771
+ background: true
14772
+ };
14773
+ }
14774
+ if (typeof background === "number") {
14775
+ return { color: background, background: true };
14776
+ }
14777
+ if (typeof background === "string") {
14778
+ if (background in presetsObj) {
14779
+ return { preset: background, background: true };
14780
+ }
14781
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14782
+ return { files: background, background: true };
14783
+ }
14784
+ return { color: background, background: true };
14785
+ }
14786
+ if (background.isColor) {
14787
+ return { color: background, background: true };
14788
+ }
14789
+ return null;
14790
+ }
14791
+
14792
+ function parseRendererConfig(rendererProp) {
14793
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14794
+ if (isRendererConfig) {
14795
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14796
+ return {
14797
+ primaryCanvas: pc,
14798
+ scheduler: sc,
14799
+ renderer: Object.keys(rest).length > 0 ? rest : rendererProp
14800
+ };
14801
+ }
14802
+ return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
14803
+ }
14804
+
14805
+ function clearHmrCaches(store) {
14806
+ store.setState((state) => ({
14807
+ nodes: {},
14808
+ uniforms: {},
14809
+ buffers: {},
14810
+ gpuStorage: {},
14811
+ _hmrVersion: state._hmrVersion + 1
14812
+ }));
14813
+ }
14814
+
15001
14815
  function CanvasImpl({
15002
14816
  ref,
15003
14817
  children,
15004
14818
  fallback,
15005
14819
  resize,
15006
14820
  style,
14821
+ id,
15007
14822
  gl,
15008
- renderer,
14823
+ renderer: rendererProp,
15009
14824
  events = createPointerEvents,
15010
14825
  eventSource,
15011
14826
  eventPrefix,
15012
14827
  shadows,
15013
- linear,
15014
- flat,
15015
- legacy,
15016
14828
  orthographic,
15017
14829
  frameloop,
15018
14830
  dpr,
@@ -15020,6 +14832,8 @@ function CanvasImpl({
15020
14832
  raycaster,
15021
14833
  camera,
15022
14834
  scene,
14835
+ autoUpdateFrustum,
14836
+ occlusion,
15023
14837
  onPointerMissed,
15024
14838
  onDragOverMissed,
15025
14839
  onDropMissed,
@@ -15027,10 +14841,14 @@ function CanvasImpl({
15027
14841
  hmr,
15028
14842
  width,
15029
14843
  height,
14844
+ background,
14845
+ forceEven,
15030
14846
  ...props
15031
14847
  }) {
14848
+ const { primaryCanvas, scheduler, renderer } = parseRendererConfig(rendererProp);
15032
14849
  React.useMemo(() => extend(THREE), []);
15033
14850
  const Bridge = useBridge();
14851
+ const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
15034
14852
  const hasInitialSizeRef = React.useRef(false);
15035
14853
  const measureConfig = React.useMemo(() => {
15036
14854
  if (!hasInitialSizeRef.current) {
@@ -15047,15 +14865,20 @@ function CanvasImpl({
15047
14865
  };
15048
14866
  }, [resize, hasInitialSizeRef.current]);
15049
14867
  const [containerRef, containerRect] = useMeasure(measureConfig);
15050
- const effectiveSize = React.useMemo(
15051
- () => ({
15052
- width: width ?? containerRect.width,
15053
- height: height ?? containerRect.height,
14868
+ const effectiveSize = React.useMemo(() => {
14869
+ let w = width ?? containerRect.width;
14870
+ let h = height ?? containerRect.height;
14871
+ if (forceEven) {
14872
+ w = Math.ceil(w / 2) * 2;
14873
+ h = Math.ceil(h / 2) * 2;
14874
+ }
14875
+ return {
14876
+ width: w,
14877
+ height: h,
15054
14878
  top: containerRect.top,
15055
14879
  left: containerRect.left
15056
- }),
15057
- [width, height, containerRect]
15058
- );
14880
+ };
14881
+ }, [width, height, containerRect, forceEven]);
15059
14882
  if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
15060
14883
  hasInitialSizeRef.current = true;
15061
14884
  }
@@ -15067,6 +14890,7 @@ function CanvasImpl({
15067
14890
  const handleDropMissed = useMutableCallback(onDropMissed);
15068
14891
  const [block, setBlock] = React.useState(false);
15069
14892
  const [error, setError] = React.useState(false);
14893
+ const [fallbackVisible, setFallbackVisible] = React.useState(false);
15070
14894
  if (block) throw block;
15071
14895
  if (error) throw error;
15072
14896
  const root = React.useRef(null);
@@ -15074,6 +14898,7 @@ function CanvasImpl({
15074
14898
  const unsubscribeErrorRef = React.useRef(null);
15075
14899
  useIsomorphicLayoutEffect(() => {
15076
14900
  effectActiveRef.current = true;
14901
+ if (fallbackVisible) return;
15077
14902
  const canvas = canvasRef.current;
15078
14903
  if (effectiveSize.width > 0 && effectiveSize.height > 0 && canvas) {
15079
14904
  if (!root.current) {
@@ -15094,24 +14919,27 @@ function CanvasImpl({
15094
14919
  }
15095
14920
  async function run() {
15096
14921
  if (!effectActiveRef.current || !root.current) return;
15097
- await root.current.configure({
14922
+ const configured = await root.current.configure({
14923
+ id,
14924
+ primaryCanvas,
14925
+ scheduler,
15098
14926
  gl,
15099
14927
  renderer,
15100
14928
  scene,
15101
14929
  events,
15102
14930
  shadows,
15103
- linear,
15104
- flat,
15105
- legacy,
15106
14931
  orthographic,
15107
14932
  frameloop,
15108
14933
  dpr,
15109
14934
  performance,
15110
14935
  raycaster,
15111
14936
  camera,
14937
+ autoUpdateFrustum,
14938
+ occlusion,
15112
14939
  size: effectiveSize,
15113
14940
  // Store size props for reset functionality
15114
14941
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
14942
+ forceEven,
15115
14943
  // Pass mutable reference to onPointerMissed so it's free to update
15116
14944
  onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
15117
14945
  onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
@@ -15132,10 +14960,25 @@ function CanvasImpl({
15132
14960
  }
15133
14961
  onCreated?.(state);
15134
14962
  }
15135
- });
15136
- if (!effectActiveRef.current || !root.current) return;
14963
+ }).then(
14964
+ () => true,
14965
+ // Renderer setup failed (e.g. no WebGL/WebGPU support). The `fallback` prop lives
14966
+ // inside <canvas>, which browsers don't display, so surface it as visible DOM
14967
+ // instead; with no fallback, rethrow to an external error boundary. (#3757)
14968
+ (setupError) => {
14969
+ if (effectActiveRef.current) {
14970
+ if (fallback != null) setFallbackVisible(true);
14971
+ else setError(setupError);
14972
+ }
14973
+ return false;
14974
+ }
14975
+ );
14976
+ if (!configured || !effectActiveRef.current || !root.current) return;
15137
14977
  root.current.render(
15138
- /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsx(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: children ?? null }) }) })
14978
+ /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxs(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: [
14979
+ backgroundProps && /* @__PURE__ */ jsx(Environment, { ...backgroundProps }),
14980
+ children ?? null
14981
+ ] }) }) })
15139
14982
  );
15140
14983
  }
15141
14984
  run();
@@ -15162,20 +15005,15 @@ function CanvasImpl({
15162
15005
  const canvas = canvasRef.current;
15163
15006
  if (!canvas) return;
15164
15007
  const handleHMR = () => {
15165
- const rootEntry = _roots.get(canvas);
15166
- if (rootEntry?.store) {
15167
- rootEntry.store.setState((state) => ({
15168
- nodes: {},
15169
- uniforms: {},
15170
- _hmrVersion: state._hmrVersion + 1
15171
- }));
15172
- }
15008
+ queueMicrotask(() => {
15009
+ const rootEntry = _roots.get(canvas);
15010
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
15011
+ });
15173
15012
  };
15174
15013
  if (typeof import.meta !== "undefined" && import.meta.hot) {
15175
15014
  const hot = import.meta.hot;
15176
15015
  hot.on("vite:afterUpdate", handleHMR);
15177
- return () => hot.dispose?.(() => {
15178
- });
15016
+ return () => hot.off?.("vite:afterUpdate", handleHMR);
15179
15017
  }
15180
15018
  if (typeof module !== "undefined" && module.hot) {
15181
15019
  const hot = module.hot;
@@ -15198,7 +15036,20 @@ function CanvasImpl({
15198
15036
  ...style
15199
15037
  },
15200
15038
  ...props,
15201
- children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "r3f-canvas", style: { display: "block" }, children: fallback }) })
15039
+ children: fallbackVisible ? (
15040
+ // Renderer setup failed: render the fallback as visible DOM. Inside <canvas> (below)
15041
+ // it exists in the tree but browsers never paint it, which is the whole bug (#3757).
15042
+ fallback
15043
+ ) : /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
15044
+ "canvas",
15045
+ {
15046
+ ref: canvasRef,
15047
+ id,
15048
+ className: "r3f-canvas",
15049
+ style: { display: "block", width: "100%", height: "100%" },
15050
+ children: fallback
15051
+ }
15052
+ ) })
15202
15053
  }
15203
15054
  );
15204
15055
  }
@@ -15208,4 +15059,4 @@ function Canvas(props) {
15208
15059
 
15209
15060
  extend(THREE);
15210
15061
 
15211
- export { Block, Canvas, ErrorBoundary, IsObject, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, getInstanceProps, getRootState, getScheduler, getUuidPrefix, hasConstructor, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isObject3D, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, prepare, reconciler, removeInteractivity, resolve, unmountComponentAtNode, updateCamera, updateFrustum, useBridge, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree };
15062
+ export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, reconciler, registerPrimary, removeInteractivity, resolve, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useEnvironment, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree, waitForPrimary };