@react-three/fiber 10.0.0-canary.aecbafb → 10.0.0-canary.b6c54f2

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.
@@ -14,6 +14,7 @@ const UltraHDRLoader_js = require('three/examples/jsm/loaders/UltraHDRLoader.js'
14
14
  const gainmapJs = require('@monogrid/gainmap-js');
15
15
  const Tb = require('scheduler');
16
16
  const traditional = require('zustand/traditional');
17
+ const scheduler = require('@pmndrs/scheduler');
17
18
  const suspendReact = require('suspend-react');
18
19
  const lite = require('dequal/lite');
19
20
  require('zustand/shallow');
@@ -62,13 +63,16 @@ const WebGLRenderer = class WebGLRenderer2 {
62
63
  }
63
64
  };
64
65
  const WebGLRenderTarget = null;
66
+ const WebGLCubeRenderTarget = null;
65
67
 
66
68
  const THREE = /*#__PURE__*/_mergeNamespaces({
67
69
  __proto__: null,
70
+ CubeRenderTargetCompat: webgpu.CubeRenderTarget,
68
71
  Inspector: Inspector_js.Inspector,
69
72
  R3F_BUILD_LEGACY: R3F_BUILD_LEGACY,
70
73
  R3F_BUILD_WEBGPU: R3F_BUILD_WEBGPU,
71
74
  RenderTargetCompat: webgpu.RenderTarget,
75
+ WebGLCubeRenderTarget: WebGLCubeRenderTarget,
72
76
  WebGLRenderTarget: WebGLRenderTarget,
73
77
  WebGLRenderer: WebGLRenderer
74
78
  }, [webgpu__namespace]);
@@ -170,7 +174,7 @@ function useEnvironment({
170
174
  fiber.useLoader.clear(loader, multiFile ? [files] : files);
171
175
  }
172
176
  renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
173
- }, [files, renderer.domElement]);
177
+ }, [extension, files, loader, multiFile, renderer.domElement]);
174
178
  const loaderResult = fiber.useLoader(
175
179
  loader,
176
180
  multiFile ? [files] : files,
@@ -342,15 +346,16 @@ function EnvironmentPortal({
342
346
  preset = void 0,
343
347
  extensions
344
348
  }) {
345
- const gl = fiber.useThree((state) => state.gl);
349
+ const renderer = fiber.useThree((state) => state.renderer);
350
+ const isLegacy = fiber.useThree((state) => state.isLegacy);
346
351
  const defaultScene = fiber.useThree((state) => state.scene);
347
352
  const camera = React__namespace.useRef(null);
348
353
  const [virtualScene] = React__namespace.useState(() => new webgpu.Scene());
349
354
  const fbo = React__namespace.useMemo(() => {
350
- const fbo2 = new webgpu.WebGLCubeRenderTarget(resolution);
355
+ const fbo2 = isLegacy ? new WebGLCubeRenderTarget(resolution) : new webgpu.CubeRenderTarget(resolution);
351
356
  fbo2.texture.type = webgpu.HalfFloatType;
352
357
  return fbo2;
353
- }, [resolution]);
358
+ }, [resolution, isLegacy]);
354
359
  React__namespace.useEffect(() => {
355
360
  return () => {
356
361
  fbo.dispose();
@@ -358,10 +363,10 @@ function EnvironmentPortal({
358
363
  }, [fbo]);
359
364
  React__namespace.useLayoutEffect(() => {
360
365
  if (frames === 1) {
361
- const autoClear = gl.autoClear;
362
- gl.autoClear = true;
363
- camera.current.update(gl, virtualScene);
364
- gl.autoClear = autoClear;
366
+ const autoClear = renderer.autoClear;
367
+ renderer.autoClear = true;
368
+ camera.current.update(renderer, virtualScene);
369
+ renderer.autoClear = autoClear;
365
370
  }
366
371
  return setEnvProps(background, scene, defaultScene, fbo.texture, {
367
372
  backgroundBlurriness: blur ?? backgroundBlurriness,
@@ -370,14 +375,29 @@ function EnvironmentPortal({
370
375
  environmentIntensity,
371
376
  environmentRotation
372
377
  });
373
- }, [children, virtualScene, fbo.texture, scene, defaultScene, background, frames, gl]);
378
+ }, [
379
+ children,
380
+ virtualScene,
381
+ fbo.texture,
382
+ scene,
383
+ defaultScene,
384
+ background,
385
+ frames,
386
+ renderer,
387
+ blur,
388
+ backgroundBlurriness,
389
+ backgroundIntensity,
390
+ backgroundRotation,
391
+ environmentIntensity,
392
+ environmentRotation
393
+ ]);
374
394
  let count = 1;
375
395
  fiber.useFrame(() => {
376
396
  if (frames === Infinity || count < frames) {
377
- const autoClear = gl.autoClear;
378
- gl.autoClear = true;
379
- camera.current.update(gl, virtualScene);
380
- gl.autoClear = autoClear;
397
+ const autoClear = renderer.autoClear;
398
+ renderer.autoClear = true;
399
+ camera.current.update(renderer, virtualScene);
400
+ renderer.autoClear = autoClear;
381
401
  count++;
382
402
  }
383
403
  });
@@ -441,9 +461,9 @@ function Environment(props) {
441
461
  return props.ground ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsxRuntime.jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(EnvironmentCube, { ...props });
442
462
  }
443
463
 
444
- var __defProp$3 = Object.defineProperty;
445
- var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
446
- var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
464
+ var __defProp$1 = Object.defineProperty;
465
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
466
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
447
467
  const act = React__namespace["act"];
448
468
  const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React__namespace.useLayoutEffect : React__namespace.useEffect;
449
469
  function useMutableCallback(fn) {
@@ -475,7 +495,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
475
495
  return _a = class extends React__namespace.Component {
476
496
  constructor() {
477
497
  super(...arguments);
478
- __publicField$3(this, "state", { error: false });
498
+ __publicField$1(this, "state", { error: false });
479
499
  }
480
500
  componentDidCatch(err) {
481
501
  this.props.set(err);
@@ -483,7 +503,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
483
503
  render() {
484
504
  return this.state.error ? null : this.props.children;
485
505
  }
486
- }, __publicField$3(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
506
+ }, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
487
507
  })();
488
508
 
489
509
  const is = {
@@ -620,8 +640,7 @@ function prepare(target, root, type, props) {
620
640
  object,
621
641
  eventCount: 0,
622
642
  handlers: {},
623
- isHidden: false,
624
- deferredRefs: []
643
+ isHidden: false
625
644
  };
626
645
  if (object) object.__r3f = instance;
627
646
  }
@@ -778,6 +797,17 @@ function unregisterVisibility(store, object) {
778
797
  internal.visibilityRegistry.delete(object.uuid);
779
798
  internal.occlusionCache.delete(object);
780
799
  }
800
+ function updateVisibilityHandlers(store, object, handlers) {
801
+ const { internal } = store.getState();
802
+ const entry = internal.visibilityRegistry.get(object.uuid);
803
+ if (!entry) return false;
804
+ entry.handlers = handlers;
805
+ if ((handlers.onOccluded || handlers.onVisible) && !internal.occlusionEnabled) {
806
+ object.occlusionTest = true;
807
+ enableOcclusion(store);
808
+ }
809
+ return true;
810
+ }
781
811
  function checkVisibility(state) {
782
812
  const { internal, camera } = state;
783
813
  const registry = internal.visibilityRegistry;
@@ -965,10 +995,7 @@ function applyProps(object, props) {
965
995
  continue;
966
996
  }
967
997
  if (value === void 0) continue;
968
- if (isFromRef(value)) {
969
- instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
970
- continue;
971
- }
998
+ if (isFromRef(value)) continue;
972
999
  if (isOnce(value)) {
973
1000
  if (instance?.appliedOnce?.has(prop)) continue;
974
1001
  if (instance) {
@@ -1004,6 +1031,19 @@ function applyProps(object, props) {
1004
1031
  } else if (isVectorLike(target) && is.num(value)) {
1005
1032
  if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
1006
1033
  else target.set(value);
1034
+ } else if (root instanceof webgpu.ShaderMaterial && key === "uniforms" && is.obj(value)) {
1035
+ if (!is.obj(root.uniforms)) root.uniforms = {};
1036
+ const uniforms = root.uniforms;
1037
+ const nextUniforms = value;
1038
+ for (const name in nextUniforms) {
1039
+ const uniform = nextUniforms[name];
1040
+ const targetUniform = uniforms[name];
1041
+ if (targetUniform) Object.assign(targetUniform, uniform);
1042
+ else {
1043
+ const nextUniform = uniform instanceof webgpu.Uniform ? uniform.clone() : new webgpu.Uniform(uniform.value);
1044
+ uniforms[name] = nextUniform;
1045
+ }
1046
+ }
1007
1047
  } else {
1008
1048
  root[key] = value;
1009
1049
  if (key.endsWith("Node") && root.isMaterial) {
@@ -1022,6 +1062,9 @@ function applyProps(object, props) {
1022
1062
  if (instance.eventCount && object2.raycast !== null) {
1023
1063
  rootState.internal.interaction.push(object2);
1024
1064
  }
1065
+ }
1066
+ if (instance?.parent && rootState?.internal && instance.object?.isObject3D) {
1067
+ const object2 = instance.object;
1025
1068
  const root = findInitialRoot(instance);
1026
1069
  const visibilityHandlers = {
1027
1070
  onFramed: instance.handlers.onFramed,
@@ -1029,7 +1072,9 @@ function applyProps(object, props) {
1029
1072
  onVisible: instance.handlers.onVisible
1030
1073
  };
1031
1074
  if (hasVisibilityHandlers(visibilityHandlers)) {
1032
- registerVisibility(root, object2, visibilityHandlers);
1075
+ if (!updateVisibilityHandlers(root, object2, visibilityHandlers)) {
1076
+ registerVisibility(root, object2, visibilityHandlers);
1077
+ }
1033
1078
  } else {
1034
1079
  unregisterVisibility(root, object2);
1035
1080
  }
@@ -1297,7 +1342,7 @@ function createEvents(store) {
1297
1342
  }
1298
1343
  function processDeferredPointer(event, pointerId) {
1299
1344
  const state = store.getState();
1300
- const { onPointerMissed, onDragOverMissed, internal } = state;
1345
+ const { internal } = state;
1301
1346
  if (!state.events.enabled) return;
1302
1347
  const filter = filterPointerEvents;
1303
1348
  const hits = intersect(event, filter);
@@ -1532,6 +1577,7 @@ function createPointerEvents(store) {
1532
1577
  }
1533
1578
  },
1534
1579
  connect: (target) => {
1580
+ if (!target) return;
1535
1581
  const { set, events } = store.getState();
1536
1582
  events.disconnect?.();
1537
1583
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -1556,6 +1602,7 @@ function createPointerEvents(store) {
1556
1602
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
1557
1603
  }
1558
1604
  },
1605
+ // PREVIEW (A8): the XR pointer registration API may change before stable.
1559
1606
  registerPointer: (config) => {
1560
1607
  const pointerId = nextXRPointerId++;
1561
1608
  xrPointers.set(pointerId, config);
@@ -1642,1229 +1689,206 @@ function notifyAlpha({ message, link }) {
1642
1689
  }
1643
1690
  }
1644
1691
 
1645
- var __defProp$2 = Object.defineProperty;
1646
- var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1647
- var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
1648
- const DEFAULT_PHASES = ["start", "input", "physics", "update", "render", "finish"];
1649
- class PhaseGraph {
1650
- constructor() {
1651
- /** Ordered list of phase nodes */
1652
- __publicField$2(this, "phases", []);
1653
- /** Quick lookup by name */
1654
- __publicField$2(this, "phaseMap", /* @__PURE__ */ new Map());
1655
- /** Cached ordered names (invalidated on changes) */
1656
- __publicField$2(this, "orderedNamesCache", null);
1657
- this.initializeDefaultPhases();
1658
- }
1659
- //* Initialization --------------------------------
1660
- initializeDefaultPhases() {
1661
- for (const name of DEFAULT_PHASES) {
1662
- const node = { name, isAutoGenerated: false };
1663
- this.phases.push(node);
1664
- this.phaseMap.set(name, node);
1665
- }
1666
- this.invalidateCache();
1667
- }
1668
- //* Public API --------------------------------
1669
- /**
1670
- * Add a named phase to the graph
1671
- * @param name - Phase name (must be unique)
1672
- * @param options - Position options (before or after another phase)
1673
- */
1674
- addPhase(name, options = {}) {
1675
- if (this.phaseMap.has(name)) {
1676
- console.warn(`[useFrame] Phase "${name}" already exists`);
1677
- return;
1678
- }
1679
- const { before, after } = options;
1680
- const node = { name, isAutoGenerated: false };
1681
- let insertIndex = this.phases.length;
1682
- const targetIndex = this.getPhaseIndex(before ?? after);
1683
- if (targetIndex !== -1) {
1684
- insertIndex = before ? targetIndex : targetIndex + 1;
1685
- } else {
1686
- const constraintType = before ? "before" : "after";
1687
- console.warn(`[useFrame] Phase "${before ?? after}" not found for '${constraintType}' constraint`);
1688
- }
1689
- this.phases.splice(insertIndex, 0, node);
1690
- this.phaseMap.set(name, node);
1691
- this.invalidateCache();
1692
- }
1693
- /**
1694
- * Get ordered list of phase names
1695
- */
1696
- getOrderedPhases() {
1697
- if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
1698
- return this.orderedNamesCache;
1699
- }
1700
- /**
1701
- * Check if a phase exists
1702
- */
1703
- hasPhase(name) {
1704
- return this.phaseMap.has(name);
1705
- }
1706
- /**
1707
- * Get the index of a phase (-1 if not found)
1708
- */
1709
- getPhaseIndex(name) {
1710
- if (!name) return -1;
1711
- return this.phases.findIndex((p) => p.name === name);
1712
- }
1713
- /**
1714
- * Ensure a phase exists, creating an auto-generated one if needed.
1715
- * Used for resolving before/after constraints.
1716
- *
1717
- * @param name - The phase name to ensure exists
1718
- * @returns The phase name (may be auto-generated like 'before:render')
1719
- */
1720
- ensurePhase(name) {
1721
- if (this.phaseMap.has(name)) return name;
1722
- const node = { name, isAutoGenerated: true };
1723
- this.phases.push(node);
1724
- this.phaseMap.set(name, node);
1725
- this.invalidateCache();
1726
- return name;
1727
- }
1728
- /**
1729
- * Resolve where a job with before/after constraints should go.
1730
- * Creates auto-generated phases if needed.
1731
- *
1732
- * @param before - Phase(s) to run before
1733
- * @param after - Phase(s) to run after
1734
- * @returns The resolved phase name
1735
- */
1736
- resolveConstraintPhase(before, after) {
1737
- const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
1738
- const afterArr = after ? Array.isArray(after) ? after : [after] : [];
1739
- if (beforeArr.length > 0) {
1740
- return this.ensureAutoPhase(beforeArr[0], "before", 0);
1741
- }
1742
- if (afterArr.length > 0) {
1743
- return this.ensureAutoPhase(afterArr[0], "after", 1);
1744
- }
1745
- return "update";
1746
- }
1747
- /**
1748
- * Ensure an auto-generated phase exists relative to a target phase.
1749
- * Creates the phase if it doesn't exist, inserting it at the correct position.
1750
- *
1751
- * @param target - The target phase name to position relative to
1752
- * @param prefix - Prefix for auto-generated phase name ('before' or 'after')
1753
- * @param offset - Insertion offset (0 for before, 1 for after)
1754
- * @returns The auto-generated phase name
1755
- */
1756
- ensureAutoPhase(target, prefix, offset) {
1757
- const autoName = `${prefix}:${target}`;
1758
- if (this.phaseMap.has(autoName)) return autoName;
1759
- const node = { name: autoName, isAutoGenerated: true };
1760
- const targetIndex = this.getPhaseIndex(target);
1761
- if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
1762
- else this.phases.push(node);
1763
- this.phaseMap.set(autoName, node);
1764
- this.invalidateCache();
1765
- return autoName;
1766
- }
1767
- // Internal --------------------------------
1768
- invalidateCache() {
1769
- this.orderedNamesCache = null;
1770
- }
1692
+ const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
1693
+ function isInternalRendererAccess(stack, callerDepth = 2) {
1694
+ if (!stack) return false;
1695
+ const caller = stack.split("\n")[callerDepth] ?? "";
1696
+ return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m));
1771
1697
  }
1772
1698
 
1773
- function rebuildSortedJobs(jobs, phaseGraph) {
1774
- const orderedPhases = phaseGraph.getOrderedPhases();
1775
- const buckets = /* @__PURE__ */ new Map();
1776
- for (const phase of orderedPhases) {
1777
- buckets.set(phase, []);
1778
- }
1779
- for (const job of jobs.values()) {
1780
- if (!job.enabled) continue;
1781
- let bucket = buckets.get(job.phase);
1782
- if (!bucket) {
1783
- bucket = [];
1784
- buckets.set(job.phase, bucket);
1785
- }
1786
- bucket.push(job);
1787
- }
1788
- const sortedBuckets = [];
1789
- for (const phase of orderedPhases) {
1790
- const bucket = buckets.get(phase);
1791
- if (!bucket || bucket.length === 0) continue;
1792
- bucket.sort((a, b) => {
1793
- if (a.priority !== b.priority) return b.priority - a.priority;
1794
- return a.index - b.index;
1795
- });
1796
- sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
1797
- }
1798
- for (const [phase, bucket] of buckets) {
1799
- if (!orderedPhases.includes(phase) && bucket.length > 0) {
1800
- bucket.sort((a, b) => {
1801
- if (a.priority !== b.priority) return b.priority - a.priority;
1802
- return a.index - b.index;
1803
- });
1804
- sortedBuckets.push(bucket);
1805
- }
1806
- }
1807
- return sortedBuckets.flat();
1808
- }
1809
- function hasCrossJobConstraints(bucket) {
1810
- const jobIds = new Set(bucket.map((j) => j.id));
1811
- for (const job of bucket) {
1812
- for (const ref of job.before) {
1813
- if (jobIds.has(ref)) return true;
1814
- }
1815
- for (const ref of job.after) {
1816
- if (jobIds.has(ref)) return true;
1817
- }
1818
- }
1819
- return false;
1820
- }
1821
- function topologicalSort(jobs) {
1822
- const n = jobs.length;
1823
- if (n <= 1) return jobs;
1824
- const jobMap = /* @__PURE__ */ new Map();
1825
- const inDegree = /* @__PURE__ */ new Map();
1826
- const adjacency = /* @__PURE__ */ new Map();
1827
- for (const job of jobs) {
1828
- jobMap.set(job.id, job);
1829
- inDegree.set(job.id, 0);
1830
- adjacency.set(job.id, []);
1831
- }
1832
- for (const job of jobs) {
1833
- for (const ref of job.before) {
1834
- if (jobMap.has(ref)) {
1835
- adjacency.get(job.id).push(ref);
1836
- inDegree.set(ref, inDegree.get(ref) + 1);
1699
+ const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
1700
+ const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React__namespace.createContext(null));
1701
+ const createStore = (invalidate, advance) => {
1702
+ const rootStore = traditional.createWithEqualityFn((set, get) => {
1703
+ const position = new webgpu.Vector3();
1704
+ const defaultTarget = new webgpu.Vector3();
1705
+ const tempTarget = new webgpu.Vector3();
1706
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
1707
+ const { width, height, top, left } = size;
1708
+ const aspect = width / height;
1709
+ if (target.isVector3) tempTarget.copy(target);
1710
+ else tempTarget.set(...target);
1711
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1712
+ if (isOrthographicCamera(camera)) {
1713
+ return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
1714
+ } else {
1715
+ const fov = camera.fov * Math.PI / 180;
1716
+ const h = 2 * Math.tan(fov / 2) * distance;
1717
+ const w = h * (width / height);
1718
+ return { width: w, height: h, top, left, factor: width / w, distance, aspect };
1837
1719
  }
1838
1720
  }
1839
- for (const ref of job.after) {
1840
- if (jobMap.has(ref)) {
1841
- adjacency.get(ref).push(job.id);
1842
- inDegree.set(job.id, inDegree.get(job.id) + 1);
1721
+ let performanceTimeout = void 0;
1722
+ const setPerformanceCurrent = (current) => set((state2) => ({ performance: { ...state2.performance, current } }));
1723
+ const pointer = new webgpu.Vector2();
1724
+ const rootState = {
1725
+ set,
1726
+ get,
1727
+ // Mock objects that have to be configured
1728
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
1729
+ primaryStore: null,
1730
+ gl: null,
1731
+ renderer: null,
1732
+ camera: null,
1733
+ frustum: new webgpu.Frustum(),
1734
+ autoUpdateFrustum: true,
1735
+ raycaster: null,
1736
+ events: {
1737
+ priority: 1,
1738
+ enabled: true,
1739
+ connected: false,
1740
+ frameTimedRaycasts: true,
1741
+ alwaysFireOnScroll: true,
1742
+ updateOnFrame: false
1743
+ },
1744
+ scene: null,
1745
+ rootScene: null,
1746
+ xr: null,
1747
+ inspector: null,
1748
+ invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
1749
+ advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
1750
+ textureColorSpace: webgpu.SRGBColorSpace,
1751
+ isLegacy: false,
1752
+ webGPUSupported: false,
1753
+ isNative: false,
1754
+ controls: null,
1755
+ pointer,
1756
+ mouse: pointer,
1757
+ frameloop: "always",
1758
+ onPointerMissed: void 0,
1759
+ onDragOverMissed: void 0,
1760
+ onDropMissed: void 0,
1761
+ performance: {
1762
+ current: 1,
1763
+ min: 0.5,
1764
+ max: 1,
1765
+ debounce: 200,
1766
+ regress: () => {
1767
+ const state2 = get();
1768
+ if (performanceTimeout) clearTimeout(performanceTimeout);
1769
+ if (state2.performance.current !== state2.performance.min) setPerformanceCurrent(state2.performance.min);
1770
+ performanceTimeout = setTimeout(
1771
+ () => setPerformanceCurrent(get().performance.max),
1772
+ state2.performance.debounce
1773
+ );
1774
+ }
1775
+ },
1776
+ size: { width: 0, height: 0, top: 0, left: 0 },
1777
+ viewport: {
1778
+ initialDpr: 0,
1779
+ dpr: 0,
1780
+ width: 0,
1781
+ height: 0,
1782
+ top: 0,
1783
+ left: 0,
1784
+ aspect: 0,
1785
+ distance: 0,
1786
+ factor: 0,
1787
+ getCurrentViewport
1788
+ },
1789
+ setEvents: (events) => set((state2) => ({ ...state2, events: { ...state2.events, ...events } })),
1790
+ setSize: (width, height, top, left) => {
1791
+ const state2 = get();
1792
+ if (width === void 0) {
1793
+ set({ _sizeImperative: false });
1794
+ if (state2._sizeProps) {
1795
+ const { width: propW, height: propH } = state2._sizeProps;
1796
+ if (propW !== void 0 || propH !== void 0) {
1797
+ const currentSize = state2.size;
1798
+ const newSize = {
1799
+ width: propW ?? currentSize.width,
1800
+ height: propH ?? currentSize.height,
1801
+ top: currentSize.top,
1802
+ left: currentSize.left
1803
+ };
1804
+ set((s) => ({
1805
+ size: newSize,
1806
+ viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
1807
+ }));
1808
+ scheduler.getScheduler().invalidate();
1809
+ }
1810
+ }
1811
+ return;
1812
+ }
1813
+ const w = width;
1814
+ const h = height ?? width;
1815
+ const t = top ?? state2.size.top;
1816
+ const l = left ?? state2.size.left;
1817
+ const size = { width: w, height: h, top: t, left: l };
1818
+ set((s) => ({
1819
+ size,
1820
+ viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
1821
+ _sizeImperative: true
1822
+ }));
1823
+ scheduler.getScheduler().invalidate();
1824
+ },
1825
+ setDpr: (dpr) => set((state2) => {
1826
+ const resolved = calculateDpr(dpr);
1827
+ return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
1828
+ }),
1829
+ setFrameloop: (frameloop = "always") => {
1830
+ set(() => ({ frameloop }));
1831
+ scheduler.getScheduler().frameloop = frameloop;
1832
+ },
1833
+ setError: (error) => set(() => ({ error })),
1834
+ error: null,
1835
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
1836
+ uniforms: {},
1837
+ nodes: {},
1838
+ buffers: {},
1839
+ gpuStorage: {},
1840
+ textures: /* @__PURE__ */ new Map(),
1841
+ _textureRefs: /* @__PURE__ */ new Map(),
1842
+ renderPipeline: null,
1843
+ passes: {},
1844
+ _hmrVersion: 0,
1845
+ _sizeImperative: false,
1846
+ _sizeProps: null,
1847
+ previousRoot: void 0,
1848
+ internal: {
1849
+ // Events
1850
+ interaction: [],
1851
+ subscribers: [],
1852
+ // Per-pointer state (new unified structure)
1853
+ pointerMap: /* @__PURE__ */ new Map(),
1854
+ pointerDirty: /* @__PURE__ */ new Map(),
1855
+ lastEvent: React__namespace.createRef(),
1856
+ // Deprecated but kept for backwards compatibility
1857
+ hovered: /* @__PURE__ */ new Map(),
1858
+ initialClick: [0, 0],
1859
+ initialHits: [],
1860
+ capturedMap: /* @__PURE__ */ new Map(),
1861
+ // Visibility tracking (onFramed, onOccluded, onVisible)
1862
+ visibilityRegistry: /* @__PURE__ */ new Map(),
1863
+ // Occlusion system (WebGPU only)
1864
+ occlusionEnabled: false,
1865
+ occlusionObserver: null,
1866
+ occlusionCache: /* @__PURE__ */ new Map(),
1867
+ helperGroup: null,
1868
+ // Updates
1869
+ active: false,
1870
+ frames: 0,
1871
+ priority: 0,
1872
+ subscribe: (ref, priority, store) => {
1873
+ const internal = get().internal;
1874
+ internal.priority = internal.priority + (priority > 0 ? 1 : 0);
1875
+ internal.subscribers.push({ ref, priority, store });
1876
+ internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority);
1877
+ return () => {
1878
+ const internal2 = get().internal;
1879
+ if (internal2?.subscribers) {
1880
+ internal2.priority = internal2.priority - (priority > 0 ? 1 : 0);
1881
+ internal2.subscribers = internal2.subscribers.filter((s) => s.ref !== ref);
1882
+ }
1883
+ };
1884
+ },
1885
+ // Renderer Storage (single source of truth)
1886
+ actualRenderer: null,
1887
+ // Scheduler for useFrameNext (initialized in renderer.tsx)
1888
+ scheduler: null
1843
1889
  }
1844
- }
1845
- }
1846
- const queue = [];
1847
- for (const job of jobs) {
1848
- if (inDegree.get(job.id) === 0) {
1849
- queue.push(job);
1850
- }
1851
- }
1852
- queue.sort((a, b) => {
1853
- if (a.priority !== b.priority) return b.priority - a.priority;
1854
- return a.index - b.index;
1855
- });
1856
- const result = [];
1857
- while (queue.length > 0) {
1858
- const job = queue.shift();
1859
- result.push(job);
1860
- const neighbors = adjacency.get(job.id) || [];
1861
- for (const neighborId of neighbors) {
1862
- const newDegree = inDegree.get(neighborId) - 1;
1863
- inDegree.set(neighborId, newDegree);
1864
- if (newDegree === 0) {
1865
- const neighbor = jobMap.get(neighborId);
1866
- insertSorted(queue, neighbor);
1867
- }
1868
- }
1869
- }
1870
- if (result.length !== n) {
1871
- console.warn("[useFrame] Circular dependency detected in job constraints");
1872
- const resultIds = new Set(result.map((j) => j.id));
1873
- for (const job of jobs) {
1874
- if (!resultIds.has(job.id)) result.push(job);
1875
- }
1876
- }
1877
- return result;
1878
- }
1879
- function insertSorted(arr, job) {
1880
- let i = 0;
1881
- while (i < arr.length) {
1882
- const cmp = arr[i];
1883
- if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
1884
- break;
1885
- }
1886
- i++;
1887
- }
1888
- arr.splice(i, 0, job);
1889
- }
1890
-
1891
- function shouldRun(job, now) {
1892
- if (!job.enabled) return false;
1893
- if (!job.fps) return true;
1894
- const minInterval = 1e3 / job.fps;
1895
- const lastRun = job.lastRun ?? 0;
1896
- const elapsed = now - lastRun;
1897
- if (elapsed < minInterval) return false;
1898
- if (job.drop) {
1899
- job.lastRun = now;
1900
- } else {
1901
- const steps = Math.floor(elapsed / minInterval);
1902
- job.lastRun = lastRun + steps * minInterval;
1903
- if (job.lastRun < now - minInterval) {
1904
- job.lastRun = now;
1905
- }
1906
- }
1907
- return true;
1908
- }
1909
- function resetJobTiming(job) {
1910
- job.lastRun = void 0;
1911
- }
1912
-
1913
- var __defProp$1 = Object.defineProperty;
1914
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1915
- var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1916
- const hmrData = (() => {
1917
- if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
1918
- if (typeof import_meta_hot !== "undefined") return import_meta_hot;
1919
- try {
1920
- return (0, eval)("import.meta.hot");
1921
- } catch {
1922
- return void 0;
1923
- }
1924
- })();
1925
- const _Scheduler = class _Scheduler {
1926
- //* Constructor ================================
1927
- constructor() {
1928
- //* Critical State ================================
1929
- __publicField$1(this, "roots", /* @__PURE__ */ new Map());
1930
- __publicField$1(this, "phaseGraph");
1931
- __publicField$1(this, "loopState", {
1932
- running: false,
1933
- rafHandle: null,
1934
- lastTime: null,
1935
- // null = uninitialized, 0+ = valid timestamp
1936
- frameCount: 0,
1937
- elapsedTime: 0,
1938
- createdAt: performance.now()
1939
- });
1940
- __publicField$1(this, "stoppedTime", 0);
1941
- //* Private State ================================
1942
- __publicField$1(this, "nextRootIndex", 0);
1943
- __publicField$1(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
1944
- __publicField$1(this, "globalAfterJobs", /* @__PURE__ */ new Map());
1945
- __publicField$1(this, "nextGlobalIndex", 0);
1946
- __publicField$1(this, "idleCallbacks", /* @__PURE__ */ new Set());
1947
- __publicField$1(this, "nextJobIndex", 0);
1948
- __publicField$1(this, "jobStateListeners", /* @__PURE__ */ new Map());
1949
- __publicField$1(this, "pendingFrames", 0);
1950
- __publicField$1(this, "_frameloop", "always");
1951
- //* Independent Mode & Error Handling State ================================
1952
- __publicField$1(this, "_independent", false);
1953
- __publicField$1(this, "errorHandler", null);
1954
- __publicField$1(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1955
- //* Core Loop Execution Methods ================================
1956
- /**
1957
- * Main RAF loop callback.
1958
- * Executes frame, handles demand mode, and schedules next frame.
1959
- * @param {number} timestamp - RAF timestamp in milliseconds
1960
- * @returns {void}
1961
- * @private
1962
- */
1963
- __publicField$1(this, "loop", (timestamp) => {
1964
- if (!this.loopState.running) return;
1965
- this.executeFrame(timestamp);
1966
- if (this._frameloop === "demand") {
1967
- this.pendingFrames = Math.max(0, this.pendingFrames - 1);
1968
- if (this.pendingFrames === 0) {
1969
- this.notifyIdle(timestamp);
1970
- return this.stop();
1971
- }
1972
- }
1973
- this.loopState.rafHandle = requestAnimationFrame(this.loop);
1974
- });
1975
- this.phaseGraph = new PhaseGraph();
1976
- }
1977
- static get instance() {
1978
- return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1979
- }
1980
- static set instance(value) {
1981
- globalThis[_Scheduler.INSTANCE_KEY] = value;
1982
- }
1983
- /**
1984
- * Get the global scheduler instance (creates if doesn't exist).
1985
- * Uses HMR data to preserve instance across hot reloads.
1986
- * @returns {Scheduler} The singleton scheduler instance
1987
- */
1988
- static get() {
1989
- if (!_Scheduler.instance && hmrData?.data?.scheduler) {
1990
- _Scheduler.instance = hmrData.data.scheduler;
1991
- }
1992
- if (!_Scheduler.instance) {
1993
- _Scheduler.instance = new _Scheduler();
1994
- if (hmrData?.data) {
1995
- hmrData.data.scheduler = _Scheduler.instance;
1996
- }
1997
- }
1998
- return _Scheduler.instance;
1999
- }
2000
- /**
2001
- * Reset the singleton instance. Stops the loop and clears all state.
2002
- * Primarily used for testing to ensure clean state between tests.
2003
- * @returns {void}
2004
- */
2005
- static reset() {
2006
- if (_Scheduler.instance) {
2007
- _Scheduler.instance.stop();
2008
- _Scheduler.instance = null;
2009
- }
2010
- if (hmrData?.data) {
2011
- hmrData.data.scheduler = null;
2012
- }
2013
- }
2014
- //* Getters & Setters ================================
2015
- get phases() {
2016
- return this.phaseGraph.getOrderedPhases();
2017
- }
2018
- get frameloop() {
2019
- return this._frameloop;
2020
- }
2021
- set frameloop(mode) {
2022
- if (this._frameloop === mode) return;
2023
- const wasAlways = this._frameloop === "always";
2024
- this._frameloop = mode;
2025
- if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
2026
- else if (mode !== "always" && wasAlways) this.stop();
2027
- }
2028
- get isRunning() {
2029
- return this.loopState.running;
2030
- }
2031
- get isReady() {
2032
- return this.roots.size > 0;
2033
- }
2034
- get independent() {
2035
- return this._independent;
2036
- }
2037
- set independent(value) {
2038
- this._independent = value;
2039
- if (value) this.ensureDefaultRoot();
2040
- }
2041
- //* Root Management Methods ================================
2042
- /**
2043
- * Register a root (Canvas) with the scheduler.
2044
- * The first root to register starts the RAF loop (if frameloop='always').
2045
- * @param {string} id - Unique identifier for this root
2046
- * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
2047
- * @returns {() => void} Unsubscribe function to remove this root
2048
- */
2049
- registerRoot(id, options = {}) {
2050
- if (this.roots.has(id)) {
2051
- console.warn(`[Scheduler] Root "${id}" already registered`);
2052
- return () => this.unregisterRoot(id);
2053
- }
2054
- const entry = {
2055
- id,
2056
- getState: options.getState ?? (() => ({})),
2057
- jobs: /* @__PURE__ */ new Map(),
2058
- sortedJobs: [],
2059
- needsRebuild: false
2060
- };
2061
- if (options.onError) {
2062
- this.errorHandler = options.onError;
2063
- }
2064
- this.roots.set(id, entry);
2065
- if (this.roots.size === 1) {
2066
- this.notifyRootReady();
2067
- if (this._frameloop === "always") this.start();
2068
- }
2069
- return () => this.unregisterRoot(id);
2070
- }
2071
- /**
2072
- * Unregister a root from the scheduler.
2073
- * Cleans up all job state listeners for this root's jobs.
2074
- * The last root to unregister stops the RAF loop.
2075
- * @param {string} id - The root ID to unregister
2076
- * @returns {void}
2077
- */
2078
- unregisterRoot(id) {
2079
- const root = this.roots.get(id);
2080
- if (!root) return;
2081
- for (const jobId of root.jobs.keys()) {
2082
- this.jobStateListeners.delete(jobId);
2083
- }
2084
- this.roots.delete(id);
2085
- if (this.roots.size === 0) {
2086
- this.stop();
2087
- this.errorHandler = null;
2088
- }
2089
- }
2090
- /**
2091
- * Subscribe to be notified when a root becomes available.
2092
- * Fires immediately if a root already exists.
2093
- * @param {() => void} callback - Function called when first root registers
2094
- * @returns {() => void} Unsubscribe function
2095
- */
2096
- onRootReady(callback) {
2097
- if (this.roots.size > 0) {
2098
- callback();
2099
- return () => {
2100
- };
2101
- }
2102
- this.rootReadyCallbacks.add(callback);
2103
- return () => this.rootReadyCallbacks.delete(callback);
2104
- }
2105
- /**
2106
- * Notify all registered root-ready callbacks.
2107
- * Called when the first root registers.
2108
- * @returns {void}
2109
- * @private
2110
- */
2111
- notifyRootReady() {
2112
- for (const cb of this.rootReadyCallbacks) {
2113
- try {
2114
- cb();
2115
- } catch (error) {
2116
- console.error("[Scheduler] Error in root-ready callback:", error);
2117
- }
2118
- }
2119
- this.rootReadyCallbacks.clear();
2120
- }
2121
- /**
2122
- * Ensure a default root exists for independent mode.
2123
- * Creates a minimal root with no state provider.
2124
- * @returns {void}
2125
- * @private
2126
- */
2127
- ensureDefaultRoot() {
2128
- if (!this.roots.has("__default__")) {
2129
- this.registerRoot("__default__");
2130
- }
2131
- }
2132
- /**
2133
- * Trigger error handling for job errors.
2134
- * Uses the bound error handler if available, otherwise logs to console.
2135
- * @param {Error} error - The error to handle
2136
- * @returns {void}
2137
- */
2138
- triggerError(error) {
2139
- if (this.errorHandler) this.errorHandler(error);
2140
- else console.error("[Scheduler]", error);
2141
- }
2142
- //* Phase Management Methods ================================
2143
- /**
2144
- * Add a named phase to the scheduler's execution order.
2145
- * Marks all roots for rebuild to incorporate the new phase.
2146
- * @param {string} name - The phase name (e.g., 'physics', 'postprocess')
2147
- * @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
2148
- * @returns {void}
2149
- * @example
2150
- * scheduler.addPhase('physics', { before: 'update' });
2151
- * scheduler.addPhase('postprocess', { after: 'render' });
2152
- */
2153
- addPhase(name, options) {
2154
- this.phaseGraph.addPhase(name, options);
2155
- for (const root of this.roots.values()) {
2156
- root.needsRebuild = true;
2157
- }
2158
- }
2159
- /**
2160
- * Check if a phase exists in the scheduler.
2161
- * @param {string} name - The phase name to check
2162
- * @returns {boolean} True if the phase exists
2163
- */
2164
- hasPhase(name) {
2165
- return this.phaseGraph.hasPhase(name);
2166
- }
2167
- //* Global Job Registration Methods (Deprecated APIs) ================================
2168
- /**
2169
- * Register a global job that runs once per frame (not per-root).
2170
- * Used internally by deprecated addEffect/addAfterEffect APIs.
2171
- * @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
2172
- * @param {string} id - Unique identifier for this global job
2173
- * @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
2174
- * @returns {() => void} Unsubscribe function to remove this global job
2175
- * @deprecated Use useFrame with phases instead
2176
- */
2177
- registerGlobal(phase, id, callback) {
2178
- const job = { id, callback };
2179
- if (phase === "before") {
2180
- this.globalBeforeJobs.set(id, job);
2181
- } else {
2182
- this.globalAfterJobs.set(id, job);
2183
- }
2184
- return () => {
2185
- if (phase === "before") this.globalBeforeJobs.delete(id);
2186
- else this.globalAfterJobs.delete(id);
2187
- };
2188
- }
2189
- //* Idle Callback Methods (Deprecated API) ================================
2190
- /**
2191
- * Register an idle callback that fires when the loop stops.
2192
- * Used internally by deprecated addTail API.
2193
- * @param {(timestamp: number) => void} callback - Function called when loop becomes idle
2194
- * @returns {() => void} Unsubscribe function to remove this idle callback
2195
- * @deprecated Use demand mode with invalidate() instead
2196
- */
2197
- onIdle(callback) {
2198
- this.idleCallbacks.add(callback);
2199
- return () => this.idleCallbacks.delete(callback);
2200
- }
2201
- /**
2202
- * Notify all registered idle callbacks.
2203
- * Called when the loop stops in demand mode.
2204
- * @param {number} timestamp - The RAF timestamp when idle occurred
2205
- * @returns {void}
2206
- * @private
2207
- */
2208
- notifyIdle(timestamp) {
2209
- for (const cb of this.idleCallbacks) {
2210
- try {
2211
- cb(timestamp);
2212
- } catch (error) {
2213
- console.error("[Scheduler] Error in idle callback:", error);
2214
- }
2215
- }
2216
- }
2217
- //* Job Registration & Management Methods ================================
2218
- /**
2219
- * Register a job (frame callback) with a specific root.
2220
- * This is the core registration method used by useFrame internally.
2221
- * @param {FrameNextCallback} callback - The function to call each frame
2222
- * @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
2223
- * @param {string} [options.rootId] - Target root ID (defaults to first registered root)
2224
- * @param {string} [options.id] - Unique job ID (auto-generated if not provided)
2225
- * @param {string} [options.phase] - Execution phase (defaults to 'update')
2226
- * @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
2227
- * @param {number} [options.fps] - FPS throttle limit
2228
- * @param {boolean} [options.drop] - Drop frames when behind (default true)
2229
- * @param {boolean} [options.enabled] - Whether job is active (default true)
2230
- * @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
2231
- * @returns {() => void} Unsubscribe function to remove this job
2232
- */
2233
- register(callback, options = {}) {
2234
- const rootId = options.rootId;
2235
- const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
2236
- if (!root) {
2237
- console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
2238
- return () => {
2239
- };
2240
- }
2241
- const id = options.id ?? this.generateJobId();
2242
- let phase = options.phase ?? "update";
2243
- if (!options.phase && (options.before || options.after)) {
2244
- phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
2245
- }
2246
- const before = this.normalizeConstraints(options.before);
2247
- const after = this.normalizeConstraints(options.after);
2248
- const job = {
2249
- id,
2250
- callback,
2251
- phase,
2252
- before,
2253
- after,
2254
- priority: options.priority ?? 0,
2255
- index: this.nextJobIndex++,
2256
- fps: options.fps,
2257
- drop: options.drop ?? true,
2258
- enabled: options.enabled ?? true,
2259
- system: options.system ?? false
2260
- };
2261
- if (root.jobs.has(id)) {
2262
- console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
2263
- }
2264
- root.jobs.set(id, job);
2265
- root.needsRebuild = true;
2266
- return () => this.unregister(id, root.id);
2267
- }
2268
- /**
2269
- * Unregister a job by its ID.
2270
- * Searches all roots if rootId is not provided.
2271
- * @param {string} id - The job ID to unregister
2272
- * @param {string} [rootId] - Optional root ID to search (searches all if not provided)
2273
- * @returns {void}
2274
- */
2275
- unregister(id, rootId) {
2276
- const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
2277
- if (root?.jobs.delete(id)) {
2278
- root.needsRebuild = true;
2279
- this.jobStateListeners.delete(id);
2280
- }
2281
- }
2282
- /**
2283
- * Update a job's options dynamically.
2284
- * Searches all roots to find the job by ID.
2285
- * Phase/constraint changes trigger a rebuild of the sorted job list.
2286
- * @param {string} id - The job ID to update
2287
- * @param {Partial<JobOptions>} options - The options to update
2288
- * @returns {void}
2289
- */
2290
- updateJob(id, options) {
2291
- let job;
2292
- let root;
2293
- for (const r of this.roots.values()) {
2294
- job = r.jobs.get(id);
2295
- if (job) {
2296
- root = r;
2297
- break;
2298
- }
2299
- }
2300
- if (!job || !root) return;
2301
- if (options.priority !== void 0) job.priority = options.priority;
2302
- if (options.fps !== void 0) job.fps = options.fps;
2303
- if (options.drop !== void 0) job.drop = options.drop;
2304
- if (options.enabled !== void 0) {
2305
- const wasEnabled = job.enabled;
2306
- job.enabled = options.enabled;
2307
- if (!wasEnabled && job.enabled) resetJobTiming(job);
2308
- if (wasEnabled !== job.enabled) root.needsRebuild = true;
2309
- }
2310
- if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
2311
- if (options.phase) job.phase = options.phase;
2312
- if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
2313
- if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
2314
- root.needsRebuild = true;
2315
- }
2316
- }
2317
- //* Job State Management Methods ================================
2318
- /**
2319
- * Check if a job is currently paused (disabled).
2320
- * @param {string} id - The job ID to check
2321
- * @returns {boolean} True if the job exists and is paused
2322
- */
2323
- isJobPaused(id) {
2324
- for (const root of this.roots.values()) {
2325
- const job = root.jobs.get(id);
2326
- if (job) return !job.enabled;
2327
- }
2328
- return false;
2329
- }
2330
- /**
2331
- * Subscribe to state changes for a specific job.
2332
- * Listener is called when job is paused or resumed.
2333
- * @param {string} id - The job ID to subscribe to
2334
- * @param {() => void} listener - Callback invoked on state changes
2335
- * @returns {() => void} Unsubscribe function
2336
- */
2337
- subscribeJobState(id, listener) {
2338
- if (!this.jobStateListeners.has(id)) {
2339
- this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
2340
- }
2341
- this.jobStateListeners.get(id).add(listener);
2342
- return () => {
2343
- this.jobStateListeners.get(id)?.delete(listener);
2344
- if (this.jobStateListeners.get(id)?.size === 0) {
2345
- this.jobStateListeners.delete(id);
2346
- }
2347
- };
2348
- }
2349
- /**
2350
- * Notify all listeners that a job's state has changed.
2351
- * @param {string} id - The job ID that changed
2352
- * @returns {void}
2353
- * @private
2354
- */
2355
- notifyJobStateChange(id) {
2356
- this.jobStateListeners.get(id)?.forEach((listener) => listener());
2357
- }
2358
- /**
2359
- * Pause a job by ID (sets enabled=false).
2360
- * Notifies any subscribed state listeners.
2361
- * @param {string} id - The job ID to pause
2362
- * @returns {void}
2363
- */
2364
- pauseJob(id) {
2365
- this.updateJob(id, { enabled: false });
2366
- this.notifyJobStateChange(id);
2367
- }
2368
- /**
2369
- * Resume a paused job by ID (sets enabled=true).
2370
- * Resets job timing to prevent frame accumulation.
2371
- * Notifies any subscribed state listeners.
2372
- * @param {string} id - The job ID to resume
2373
- * @returns {void}
2374
- */
2375
- resumeJob(id) {
2376
- this.updateJob(id, { enabled: true });
2377
- this.notifyJobStateChange(id);
2378
- }
2379
- //* Frame Loop Control Methods ================================
2380
- /**
2381
- * Start the requestAnimationFrame loop.
2382
- * Resets timing state (elapsedTime, frameCount) on start.
2383
- * No-op if already running.
2384
- * @returns {void}
2385
- */
2386
- start() {
2387
- if (this.loopState.running) return;
2388
- const { elapsedTime, createdAt } = this.loopState;
2389
- let adjustedCreated = 0;
2390
- if (this.stoppedTime > 0) {
2391
- adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
2392
- this.stoppedTime = 0;
2393
- }
2394
- Object.assign(this.loopState, {
2395
- running: true,
2396
- elapsedTime: elapsedTime ?? 0,
2397
- lastTime: performance.now(),
2398
- createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
2399
- frameCount: 0,
2400
- rafHandle: requestAnimationFrame(this.loop)
2401
- });
2402
- }
2403
- /**
2404
- * Stop the requestAnimationFrame loop.
2405
- * Cancels any pending RAF callback.
2406
- * No-op if not running.
2407
- * @returns {void}
2408
- */
2409
- stop() {
2410
- if (!this.loopState.running) return;
2411
- this.loopState.running = false;
2412
- if (this.loopState.rafHandle !== null) {
2413
- cancelAnimationFrame(this.loopState.rafHandle);
2414
- this.loopState.rafHandle = null;
2415
- }
2416
- this.stoppedTime = performance.now();
2417
- }
2418
- /**
2419
- * Request frames to be rendered in demand mode.
2420
- * Accumulates pending frames (capped at 60) and starts the loop if not running.
2421
- * No-op if frameloop is not 'demand'.
2422
- * @param {number} [frames=1] - Number of frames to request
2423
- * @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
2424
- * - `false` (default): Sets pending frames to the specified value (replaces existing count)
2425
- * - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
2426
- * @returns {void}
2427
- * @example
2428
- * // Request a single frame render
2429
- * scheduler.invalidate();
2430
- *
2431
- * @example
2432
- * // Request 5 frames (e.g., for animations)
2433
- * scheduler.invalidate(5);
2434
- *
2435
- * @example
2436
- * // Set pending frames to exactly 3 (don't stack with existing)
2437
- * scheduler.invalidate(3, false);
2438
- *
2439
- * @example
2440
- * // Add 2 more frames to existing pending count
2441
- * scheduler.invalidate(2, true);
2442
- */
2443
- invalidate(frames = 1, stackFrames = false) {
2444
- if (this._frameloop !== "demand") return;
2445
- const baseFrames = stackFrames ? this.pendingFrames : 0;
2446
- this.pendingFrames = Math.min(60, baseFrames + frames);
2447
- if (!this.loopState.running && this.pendingFrames > 0) this.start();
2448
- }
2449
- /**
2450
- * Reset timing state for deterministic testing.
2451
- * Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
2452
- * @returns {void}
2453
- */
2454
- resetTiming() {
2455
- this.loopState.lastTime = null;
2456
- this.loopState.frameCount = 0;
2457
- this.loopState.elapsedTime = 0;
2458
- this.loopState.createdAt = performance.now();
2459
- }
2460
- //* Manual Stepping Methods ================================
2461
- /**
2462
- * Manually execute a single frame for all roots.
2463
- * Useful for frameloop='never' mode or testing scenarios.
2464
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2465
- * @returns {void}
2466
- * @example
2467
- * // Manual control mode
2468
- * scheduler.frameloop = 'never';
2469
- * scheduler.step(); // Execute one frame
2470
- */
2471
- step(timestamp) {
2472
- const now = timestamp ?? performance.now();
2473
- this.executeFrame(now);
2474
- }
2475
- /**
2476
- * Manually execute a single job by its ID.
2477
- * Useful for testing individual job callbacks in isolation.
2478
- * @param {string} id - The job ID to step
2479
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2480
- * @returns {void}
2481
- */
2482
- stepJob(id, timestamp) {
2483
- let job;
2484
- let root;
2485
- for (const r of this.roots.values()) {
2486
- job = r.jobs.get(id);
2487
- if (job) {
2488
- root = r;
2489
- break;
2490
- }
2491
- }
2492
- if (!job || !root) {
2493
- console.warn(`[Scheduler] Job "${id}" not found`);
2494
- return;
2495
- }
2496
- const now = timestamp ?? performance.now();
2497
- const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
2498
- const delta = deltaMs / 1e3;
2499
- const elapsed = now - this.loopState.createdAt;
2500
- const providedState = root.getState?.() ?? {};
2501
- const frameState = {
2502
- ...providedState,
2503
- time: now,
2504
- delta,
2505
- elapsed,
2506
- frame: this.loopState.frameCount
2507
- };
2508
- try {
2509
- job.callback(frameState, delta);
2510
- } catch (error) {
2511
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2512
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2513
- }
2514
- }
2515
- /**
2516
- * Execute a single frame across all roots.
2517
- * Order: globalBefore → each root's jobs → globalAfter
2518
- * @param {number} timestamp - RAF timestamp in milliseconds
2519
- * @returns {void}
2520
- * @private
2521
- */
2522
- executeFrame(timestamp) {
2523
- const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
2524
- const delta = deltaMs / 1e3;
2525
- this.loopState.lastTime = timestamp;
2526
- this.loopState.frameCount++;
2527
- this.loopState.elapsedTime += deltaMs;
2528
- this.runGlobalJobs(this.globalBeforeJobs, timestamp);
2529
- for (const root of this.roots.values()) {
2530
- this.tickRoot(root, timestamp, delta);
2531
- }
2532
- this.runGlobalJobs(this.globalAfterJobs, timestamp);
2533
- }
2534
- /**
2535
- * Run all global jobs from a job map.
2536
- * Catches and logs errors without stopping execution.
2537
- * @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
2538
- * @param {number} timestamp - RAF timestamp in milliseconds
2539
- * @returns {void}
2540
- * @private
2541
- */
2542
- runGlobalJobs(jobs, timestamp) {
2543
- for (const job of jobs.values()) {
2544
- try {
2545
- job.callback(timestamp);
2546
- } catch (error) {
2547
- console.error(`[Scheduler] Error in global job "${job.id}":`, error);
2548
- }
2549
- }
2550
- }
2551
- /**
2552
- * Execute all jobs for a single root in sorted order.
2553
- * Rebuilds sorted job list if needed, then dispatches each job.
2554
- * Errors are caught and propagated via triggerError.
2555
- * @param {RootEntry} root - The root entry to tick
2556
- * @param {number} timestamp - RAF timestamp in milliseconds
2557
- * @param {number} delta - Time since last frame in seconds
2558
- * @returns {void}
2559
- * @private
2560
- */
2561
- tickRoot(root, timestamp, delta) {
2562
- if (root.needsRebuild) {
2563
- root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
2564
- root.needsRebuild = false;
2565
- }
2566
- const providedState = root.getState?.() ?? {};
2567
- const frameState = {
2568
- ...providedState,
2569
- time: timestamp,
2570
- delta,
2571
- elapsed: this.loopState.elapsedTime / 1e3,
2572
- // Convert ms to seconds
2573
- frame: this.loopState.frameCount
2574
- };
2575
- for (const job of root.sortedJobs) {
2576
- if (!shouldRun(job, timestamp)) continue;
2577
- try {
2578
- job.callback(frameState, delta);
2579
- } catch (error) {
2580
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2581
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2582
- }
2583
- }
2584
- }
2585
- //* Debug & Inspection Methods ================================
2586
- /**
2587
- * Get the total number of registered jobs across all roots.
2588
- * Includes both per-root jobs and global before/after jobs.
2589
- * @returns {number} Total job count
2590
- */
2591
- getJobCount() {
2592
- let count = 0;
2593
- for (const root of this.roots.values()) {
2594
- count += root.jobs.size;
2595
- }
2596
- return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
2597
- }
2598
- /**
2599
- * Get all registered job IDs across all roots.
2600
- * Includes both per-root jobs and global before/after jobs.
2601
- * @returns {string[]} Array of all job IDs
2602
- */
2603
- getJobIds() {
2604
- const ids = [];
2605
- for (const root of this.roots.values()) {
2606
- ids.push(...root.jobs.keys());
2607
- }
2608
- ids.push(...this.globalBeforeJobs.keys());
2609
- ids.push(...this.globalAfterJobs.keys());
2610
- return ids;
2611
- }
2612
- /**
2613
- * Get the number of registered roots (Canvas instances).
2614
- * @returns {number} Number of registered roots
2615
- */
2616
- getRootCount() {
2617
- return this.roots.size;
2618
- }
2619
- /**
2620
- * Check if any user (non-system) jobs are registered in a specific phase.
2621
- * Used by the default render job to know if a user has taken over rendering.
2622
- *
2623
- * @param phase The phase to check
2624
- * @param rootId Optional root ID to check (checks all roots if not provided)
2625
- * @returns true if any user jobs exist in the phase
2626
- */
2627
- hasUserJobsInPhase(phase, rootId) {
2628
- const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
2629
- return rootsToCheck.some((root) => {
2630
- if (!root) return false;
2631
- for (const job of root.jobs.values()) {
2632
- if (job.phase === phase && !job.system && job.enabled) return true;
2633
- }
2634
- return false;
2635
- });
2636
- }
2637
- //* Utility Methods ================================
2638
- /**
2639
- * Generate a unique root ID for automatic root registration.
2640
- * @returns {string} A unique root ID in the format 'root_N'
2641
- */
2642
- generateRootId() {
2643
- return `root_${this.nextRootIndex++}`;
2644
- }
2645
- /**
2646
- * Generate a unique job ID.
2647
- * @returns {string} A unique job ID in the format 'job_N'
2648
- * @private
2649
- */
2650
- generateJobId() {
2651
- return `job_${this.nextJobIndex}`;
2652
- }
2653
- /**
2654
- * Normalize before/after constraints to a Set.
2655
- * Handles undefined, single string, or array inputs.
2656
- * @param {string | string[] | undefined} value - The constraint value(s)
2657
- * @returns {Set<string>} Normalized Set of constraint strings
2658
- * @private
2659
- */
2660
- normalizeConstraints(value) {
2661
- if (!value) return /* @__PURE__ */ new Set();
2662
- if (Array.isArray(value)) return new Set(value);
2663
- return /* @__PURE__ */ new Set([value]);
2664
- }
2665
- };
2666
- //* Static State & Methods (Singleton Usage) ================================
2667
- //* Cross-Bundle Singleton Key ==============================
2668
- // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2669
- // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2670
- __publicField$1(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
2671
- let Scheduler = _Scheduler;
2672
- const getScheduler = () => Scheduler.get();
2673
- if (hmrData) {
2674
- hmrData.accept?.();
2675
- }
2676
-
2677
- const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
2678
- const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React__namespace.createContext(null));
2679
- const createStore = (invalidate, advance) => {
2680
- const rootStore = traditional.createWithEqualityFn((set, get) => {
2681
- const position = new webgpu.Vector3();
2682
- const defaultTarget = new webgpu.Vector3();
2683
- const tempTarget = new webgpu.Vector3();
2684
- function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
2685
- const { width, height, top, left } = size;
2686
- const aspect = width / height;
2687
- if (target.isVector3) tempTarget.copy(target);
2688
- else tempTarget.set(...target);
2689
- const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
2690
- if (isOrthographicCamera(camera)) {
2691
- return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
2692
- } else {
2693
- const fov = camera.fov * Math.PI / 180;
2694
- const h = 2 * Math.tan(fov / 2) * distance;
2695
- const w = h * (width / height);
2696
- return { width: w, height: h, top, left, factor: width / w, distance, aspect };
2697
- }
2698
- }
2699
- let performanceTimeout = void 0;
2700
- const setPerformanceCurrent = (current) => set((state2) => ({ performance: { ...state2.performance, current } }));
2701
- const pointer = new webgpu.Vector2();
2702
- const rootState = {
2703
- set,
2704
- get,
2705
- // Mock objects that have to be configured
2706
- // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
2707
- primaryStore: null,
2708
- gl: null,
2709
- renderer: null,
2710
- camera: null,
2711
- frustum: new webgpu.Frustum(),
2712
- autoUpdateFrustum: true,
2713
- raycaster: null,
2714
- events: {
2715
- priority: 1,
2716
- enabled: true,
2717
- connected: false,
2718
- frameTimedRaycasts: true,
2719
- alwaysFireOnScroll: true,
2720
- updateOnFrame: false
2721
- },
2722
- scene: null,
2723
- rootScene: null,
2724
- xr: null,
2725
- inspector: null,
2726
- invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
2727
- advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
2728
- textureColorSpace: webgpu.SRGBColorSpace,
2729
- isLegacy: false,
2730
- webGPUSupported: false,
2731
- isNative: false,
2732
- controls: null,
2733
- pointer,
2734
- mouse: pointer,
2735
- frameloop: "always",
2736
- onPointerMissed: void 0,
2737
- onDragOverMissed: void 0,
2738
- onDropMissed: void 0,
2739
- performance: {
2740
- current: 1,
2741
- min: 0.5,
2742
- max: 1,
2743
- debounce: 200,
2744
- regress: () => {
2745
- const state2 = get();
2746
- if (performanceTimeout) clearTimeout(performanceTimeout);
2747
- if (state2.performance.current !== state2.performance.min) setPerformanceCurrent(state2.performance.min);
2748
- performanceTimeout = setTimeout(
2749
- () => setPerformanceCurrent(get().performance.max),
2750
- state2.performance.debounce
2751
- );
2752
- }
2753
- },
2754
- size: { width: 0, height: 0, top: 0, left: 0 },
2755
- viewport: {
2756
- initialDpr: 0,
2757
- dpr: 0,
2758
- width: 0,
2759
- height: 0,
2760
- top: 0,
2761
- left: 0,
2762
- aspect: 0,
2763
- distance: 0,
2764
- factor: 0,
2765
- getCurrentViewport
2766
- },
2767
- setEvents: (events) => set((state2) => ({ ...state2, events: { ...state2.events, ...events } })),
2768
- setSize: (width, height, top, left) => {
2769
- const state2 = get();
2770
- if (width === void 0) {
2771
- set({ _sizeImperative: false });
2772
- if (state2._sizeProps) {
2773
- const { width: propW, height: propH } = state2._sizeProps;
2774
- if (propW !== void 0 || propH !== void 0) {
2775
- const currentSize = state2.size;
2776
- const newSize = {
2777
- width: propW ?? currentSize.width,
2778
- height: propH ?? currentSize.height,
2779
- top: currentSize.top,
2780
- left: currentSize.left
2781
- };
2782
- set((s) => ({
2783
- size: newSize,
2784
- viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
2785
- }));
2786
- getScheduler().invalidate();
2787
- }
2788
- }
2789
- return;
2790
- }
2791
- const w = width;
2792
- const h = height ?? width;
2793
- const t = top ?? state2.size.top;
2794
- const l = left ?? state2.size.left;
2795
- const size = { width: w, height: h, top: t, left: l };
2796
- set((s) => ({
2797
- size,
2798
- viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
2799
- _sizeImperative: true
2800
- }));
2801
- getScheduler().invalidate();
2802
- },
2803
- setDpr: (dpr) => set((state2) => {
2804
- const resolved = calculateDpr(dpr);
2805
- return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
2806
- }),
2807
- setFrameloop: (frameloop = "always") => {
2808
- set(() => ({ frameloop }));
2809
- },
2810
- setError: (error) => set(() => ({ error })),
2811
- error: null,
2812
- //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
2813
- uniforms: {},
2814
- nodes: {},
2815
- buffers: {},
2816
- gpuStorage: {},
2817
- textures: /* @__PURE__ */ new Map(),
2818
- renderPipeline: null,
2819
- passes: {},
2820
- _hmrVersion: 0,
2821
- _sizeImperative: false,
2822
- _sizeProps: null,
2823
- previousRoot: void 0,
2824
- internal: {
2825
- // Events
2826
- interaction: [],
2827
- subscribers: [],
2828
- // Per-pointer state (new unified structure)
2829
- pointerMap: /* @__PURE__ */ new Map(),
2830
- pointerDirty: /* @__PURE__ */ new Map(),
2831
- lastEvent: React__namespace.createRef(),
2832
- // Deprecated but kept for backwards compatibility
2833
- hovered: /* @__PURE__ */ new Map(),
2834
- initialClick: [0, 0],
2835
- initialHits: [],
2836
- capturedMap: /* @__PURE__ */ new Map(),
2837
- // Visibility tracking (onFramed, onOccluded, onVisible)
2838
- visibilityRegistry: /* @__PURE__ */ new Map(),
2839
- // Occlusion system (WebGPU only)
2840
- occlusionEnabled: false,
2841
- occlusionObserver: null,
2842
- occlusionCache: /* @__PURE__ */ new Map(),
2843
- helperGroup: null,
2844
- // Updates
2845
- active: false,
2846
- frames: 0,
2847
- priority: 0,
2848
- subscribe: (ref, priority, store) => {
2849
- const internal = get().internal;
2850
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
2851
- internal.subscribers.push({ ref, priority, store });
2852
- internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority);
2853
- return () => {
2854
- const internal2 = get().internal;
2855
- if (internal2?.subscribers) {
2856
- internal2.priority = internal2.priority - (priority > 0 ? 1 : 0);
2857
- internal2.subscribers = internal2.subscribers.filter((s) => s.ref !== ref);
2858
- }
2859
- };
2860
- },
2861
- // Renderer Storage (single source of truth)
2862
- actualRenderer: null,
2863
- // Scheduler for useFrameNext (initialized in renderer.tsx)
2864
- scheduler: null
2865
- }
2866
- };
2867
- return rootState;
1890
+ };
1891
+ return rootState;
2868
1892
  });
2869
1893
  const state = rootStore.getState();
2870
1894
  Object.defineProperty(state, "gl", {
@@ -2872,8 +1896,7 @@ const createStore = (invalidate, advance) => {
2872
1896
  const currentState = rootStore.getState();
2873
1897
  if (!currentState.isLegacy && currentState.internal.actualRenderer) {
2874
1898
  const stack = new Error().stack || "";
2875
- const isInternalAccess = stack.includes("zustand") || stack.includes("setState") || stack.includes("Object.assign") || stack.includes("react-three-fiber/packages/fiber/src/core");
2876
- if (!isInternalAccess) {
1899
+ if (!isInternalRendererAccess(stack)) {
2877
1900
  const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
2878
1901
  notifyDepreciated({
2879
1902
  heading: "Accessing state.gl in WebGPU mode",
@@ -2921,14 +1944,16 @@ const createStore = (invalidate, advance) => {
2921
1944
  oldSize = size;
2922
1945
  oldDpr = viewport.dpr;
2923
1946
  updateCamera(camera, size);
2924
- if (canvasTarget) {
1947
+ if (internal.isSecondary && canvasTarget) {
2925
1948
  if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
2926
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && canvasTarget.domElement instanceof HTMLCanvasElement;
2927
- canvasTarget.setSize(size.width, size.height, updateStyle);
1949
+ canvasTarget.setSize(size.width, size.height, false);
2928
1950
  } else {
2929
1951
  if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
2930
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && actualRenderer.domElement instanceof HTMLCanvasElement;
2931
- actualRenderer.setSize(size.width, size.height, updateStyle);
1952
+ actualRenderer.setSize(size.width, size.height, false);
1953
+ if (canvasTarget) {
1954
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1955
+ canvasTarget.setSize(size.width, size.height, false);
1956
+ }
2932
1957
  }
2933
1958
  }
2934
1959
  if (camera !== oldCamera) {
@@ -2984,26 +2009,32 @@ function loadingFn(extensions, onProgress) {
2984
2009
  );
2985
2010
  };
2986
2011
  }
2987
- function useLoader(loader, input, extensions, onProgress) {
2988
- const keys = Array.isArray(input) ? input : [input];
2012
+ function useLoader(loader, input, extensions, onProgress, cacheKey) {
2013
+ const inputs = Array.isArray(input) ? input : [input];
2014
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2989
2015
  const fn = loadingFn(extensions, onProgress);
2990
- const results = keys.map((key) => suspendReact.suspend(fn, [loader, key], { equal: is.equ }));
2016
+ const results = cacheKeys.map(
2017
+ (key, index) => suspendReact.suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
2018
+ );
2991
2019
  return Array.isArray(input) ? results : results[0];
2992
2020
  }
2993
- useLoader.preload = function(loader, input, extensions, onProgress) {
2994
- const keys = Array.isArray(input) ? input : [input];
2995
- keys.forEach((key) => suspendReact.preload(loadingFn(extensions, onProgress), [loader, key]));
2021
+ useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
2022
+ const inputs = Array.isArray(input) ? input : [input];
2023
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2024
+ cacheKeys.forEach(
2025
+ (key, index) => suspendReact.preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
2026
+ );
2996
2027
  };
2997
- useLoader.clear = function(loader, input) {
2998
- const keys = Array.isArray(input) ? input : [input];
2999
- keys.forEach((key) => suspendReact.clear([loader, key]));
2028
+ useLoader.clear = function(loader, input, cacheKey) {
2029
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2030
+ cacheKeys.forEach((key) => suspendReact.clear([loader, key]));
3000
2031
  };
3001
2032
  useLoader.loader = getLoader;
3002
2033
 
3003
2034
  function useFrame(callback, priorityOrOptions) {
3004
2035
  const store = React__namespace.useContext(context);
3005
2036
  const isInsideCanvas = store !== null;
3006
- const scheduler = getScheduler();
2037
+ const scheduler$1 = scheduler.getScheduler();
3007
2038
  const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
3008
2039
  id: priorityOrOptions.id,
3009
2040
  phase: priorityOrOptions.phase,
@@ -3051,7 +2082,7 @@ function useFrame(callback, priorityOrOptions) {
3051
2082
  };
3052
2083
  callbackRef.current?.(mergedState, delta);
3053
2084
  };
3054
- const unregister = scheduler.register(wrappedCallback, {
2085
+ const unregister = scheduler$1.register(wrappedCallback, {
3055
2086
  id,
3056
2087
  rootId,
3057
2088
  ...options
@@ -3072,37 +2103,31 @@ function useFrame(callback, priorityOrOptions) {
3072
2103
  }
3073
2104
  };
3074
2105
  } else {
3075
- const registerOutside = () => {
3076
- return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
3077
- };
3078
- if (scheduler.independent || scheduler.isReady) {
3079
- return registerOutside();
3080
- }
3081
- let unregisterJob = null;
3082
- const unsubReady = scheduler.onRootReady(() => {
3083
- unregisterJob = registerOutside();
3084
- });
3085
- return () => {
3086
- unsubReady();
3087
- unregisterJob?.();
3088
- };
2106
+ return scheduler$1.register(
2107
+ (state, delta) => {
2108
+ const frameState = state;
2109
+ if (!frameState.renderer) return;
2110
+ callbackRef.current?.(frameState, delta);
2111
+ },
2112
+ { id, ...options }
2113
+ );
3089
2114
  }
3090
- }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2115
+ }, [store, scheduler$1, id, optionsKey, isLegacyPriority, isInsideCanvas]);
3091
2116
  const isPaused = React__namespace.useSyncExternalStore(
3092
2117
  // Subscribe function
3093
2118
  React__namespace.useCallback(
3094
2119
  (onStoreChange) => {
3095
- return getScheduler().subscribeJobState(id, onStoreChange);
2120
+ return scheduler.getScheduler().subscribeJobState(id, onStoreChange);
3096
2121
  },
3097
2122
  [id]
3098
2123
  ),
3099
2124
  // getSnapshot function
3100
- React__namespace.useCallback(() => getScheduler().isJobPaused(id), [id]),
2125
+ React__namespace.useCallback(() => scheduler.getScheduler().isJobPaused(id), [id]),
3101
2126
  // getServerSnapshot function (SSR)
3102
2127
  React__namespace.useCallback(() => false, [])
3103
2128
  );
3104
2129
  const controls = React__namespace.useMemo(() => {
3105
- const scheduler2 = getScheduler();
2130
+ const scheduler2 = scheduler.getScheduler();
3106
2131
  return {
3107
2132
  /** The job's unique ID */
3108
2133
  id,
@@ -3117,7 +2142,7 @@ function useFrame(callback, priorityOrOptions) {
3117
2142
  * @param timestamp Optional timestamp (defaults to performance.now())
3118
2143
  */
3119
2144
  step: (timestamp) => {
3120
- getScheduler().stepJob(id, timestamp);
2145
+ scheduler.getScheduler().stepJob(id, timestamp);
3121
2146
  },
3122
2147
  /**
3123
2148
  * Manually step ALL jobs in the scheduler.
@@ -3125,20 +2150,20 @@ function useFrame(callback, priorityOrOptions) {
3125
2150
  * @param timestamp Optional timestamp (defaults to performance.now())
3126
2151
  */
3127
2152
  stepAll: (timestamp) => {
3128
- getScheduler().step(timestamp);
2153
+ scheduler.getScheduler().step(timestamp);
3129
2154
  },
3130
2155
  /**
3131
2156
  * Pause this job (set enabled=false).
3132
2157
  * Job remains registered but won't run.
3133
2158
  */
3134
2159
  pause: () => {
3135
- getScheduler().pauseJob(id);
2160
+ scheduler.getScheduler().pauseJob(id);
3136
2161
  },
3137
2162
  /**
3138
2163
  * Resume this job (set enabled=true).
3139
2164
  */
3140
2165
  resume: () => {
3141
- getScheduler().resumeJob(id);
2166
+ scheduler.getScheduler().resumeJob(id);
3142
2167
  },
3143
2168
  /**
3144
2169
  * Reactive paused state - automatically updates when pause/resume is called.
@@ -3176,18 +2201,18 @@ function buildFromCache(input, textureCache) {
3176
2201
  function useTexture(input, optionsOrOnLoad) {
3177
2202
  const renderer = useThree((state) => state.internal.actualRenderer);
3178
2203
  const store = useStore();
3179
- const textureCache = useThree((state) => state.textures);
3180
2204
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
3181
- const { onLoad, cache = false } = options;
2205
+ const { onLoad, cache = true } = options;
3182
2206
  const onLoadRef = React.useRef(onLoad);
3183
2207
  onLoadRef.current = onLoad;
3184
2208
  const onLoadCalledForRef = React.useRef(null);
3185
2209
  const urls = React.useMemo(() => getUrls(input), [input]);
3186
2210
  const cachedResult = React.useMemo(() => {
3187
2211
  if (!cache) return null;
3188
- if (!allUrlsCached(urls, textureCache)) return null;
3189
- return buildFromCache(input, textureCache);
3190
- }, [cache, urls, textureCache, input]);
2212
+ const textures = store.getState().textures;
2213
+ if (!allUrlsCached(urls, textures)) return null;
2214
+ return buildFromCache(input, textures);
2215
+ }, [cache, urls, input, store]);
3191
2216
  const loadedTextures = useLoader(
3192
2217
  webgpu.TextureLoader,
3193
2218
  IsObject(input) ? Object.values(input) : input
@@ -3231,8 +2256,6 @@ function useTexture(input, optionsOrOnLoad) {
3231
2256
  }, [input, loadedTextures, cachedResult]);
3232
2257
  React.useEffect(() => {
3233
2258
  if (!cache) return;
3234
- if (cachedResult) return;
3235
- const set = store.setState;
3236
2259
  const urlTextureMap = [];
3237
2260
  if (typeof input === "string") {
3238
2261
  urlTextureMap.push([input, mappedTextures]);
@@ -3246,18 +2269,32 @@ function useTexture(input, optionsOrOnLoad) {
3246
2269
  urlTextureMap.push([url, textureRecord[key]]);
3247
2270
  }
3248
2271
  }
3249
- set((state) => {
3250
- const newMap = new Map(state.textures);
3251
- let changed = false;
2272
+ store.setState((state) => {
2273
+ const refs = new Map(state._textureRefs);
2274
+ let textures = state.textures;
2275
+ let added = false;
3252
2276
  for (const [url, texture] of urlTextureMap) {
3253
- if (!newMap.has(url)) {
3254
- newMap.set(url, texture);
3255
- changed = true;
2277
+ if (!textures.has(url)) {
2278
+ if (!added) {
2279
+ textures = new Map(textures);
2280
+ added = true;
2281
+ }
2282
+ textures.set(url, texture);
3256
2283
  }
2284
+ refs.set(url, (refs.get(url) ?? 0) + 1);
3257
2285
  }
3258
- return changed ? { textures: newMap } : state;
2286
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
2287
+ });
2288
+ return () => store.setState((state) => {
2289
+ const refs = new Map(state._textureRefs);
2290
+ for (const [url] of urlTextureMap) {
2291
+ const next = (refs.get(url) ?? 0) - 1;
2292
+ if (next <= 0) refs.delete(url);
2293
+ else refs.set(url, next);
2294
+ }
2295
+ return { _textureRefs: refs };
3259
2296
  });
3260
- }, [cache, input, mappedTextures, store, cachedResult]);
2297
+ }, [cache, input, mappedTextures, store]);
3261
2298
  return mappedTextures;
3262
2299
  }
3263
2300
  useTexture.preload = (url) => useLoader.preload(webgpu.TextureLoader, url);
@@ -3273,96 +2310,63 @@ const Texture = ({
3273
2310
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children?.(ret) });
3274
2311
  };
3275
2312
 
3276
- function getTextureValue(entry) {
3277
- if (entry instanceof webgpu.Texture) return entry;
3278
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof webgpu.Texture) {
3279
- return entry.value;
3280
- }
3281
- return null;
3282
- }
3283
- function useTextures() {
2313
+ function useTextures(selector) {
3284
2314
  const store = useStore();
3285
- return React.useMemo(() => {
3286
- const set = store.setState;
2315
+ const registry = React.useMemo(() => {
3287
2316
  const getState = store.getState;
3288
- const add = (key, value) => {
3289
- set((state) => {
3290
- const newMap = new Map(state.textures);
3291
- newMap.set(key, value);
3292
- return { textures: newMap };
3293
- });
3294
- };
3295
- const addMultiple = (items) => {
3296
- set((state) => {
3297
- const newMap = new Map(state.textures);
3298
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
3299
- for (const [key, value] of entries) {
3300
- newMap.set(key, value);
3301
- }
3302
- return { textures: newMap };
3303
- });
3304
- };
3305
- const remove = (key) => {
3306
- set((state) => {
3307
- const newMap = new Map(state.textures);
3308
- newMap.delete(key);
3309
- return { textures: newMap };
3310
- });
3311
- };
3312
- const removeMultiple = (keys) => {
3313
- set((state) => {
3314
- const newMap = new Map(state.textures);
3315
- for (const key of keys) newMap.delete(key);
3316
- return { textures: newMap };
3317
- });
3318
- };
3319
- const dispose = (key) => {
3320
- const entry = getState().textures.get(key);
3321
- if (entry) {
3322
- const tex = getTextureValue(entry);
3323
- tex?.dispose();
3324
- }
3325
- remove(key);
3326
- };
3327
- const disposeMultiple = (keys) => {
3328
- const textures = getState().textures;
3329
- for (const key of keys) {
3330
- const entry = textures.get(key);
3331
- if (entry) {
3332
- const tex = getTextureValue(entry);
3333
- tex?.dispose();
3334
- }
3335
- }
3336
- removeMultiple(keys);
3337
- };
3338
- const disposeAll = () => {
3339
- const textures = getState().textures;
3340
- for (const entry of textures.values()) {
3341
- const tex = getTextureValue(entry);
3342
- tex?.dispose();
3343
- }
3344
- set({ textures: /* @__PURE__ */ new Map() });
3345
- };
2317
+ const setState = store.setState;
2318
+ const getOne = (key) => getState().textures.get(key);
3346
2319
  return {
3347
- // Getter for the textures Map (reactive via getState)
3348
- get textures() {
2320
+ get all() {
3349
2321
  return getState().textures;
3350
2322
  },
3351
- // Read
3352
- get: (key) => getState().textures.get(key),
2323
+ get(input) {
2324
+ if (typeof input === "string") return getOne(input);
2325
+ if (Array.isArray(input)) return input.map(getOne);
2326
+ const out = {};
2327
+ for (const name in input) out[name] = getOne(input[name]);
2328
+ return out;
2329
+ },
3353
2330
  has: (key) => getState().textures.has(key),
3354
- // Write
3355
- add,
3356
- addMultiple,
3357
- // Remove (cache only)
3358
- remove,
3359
- removeMultiple,
3360
- // Dispose (GPU + cache)
3361
- dispose,
3362
- disposeMultiple,
3363
- disposeAll
2331
+ add(keyOrRecord, texture) {
2332
+ setState((state) => {
2333
+ const textures = new Map(state.textures);
2334
+ if (typeof keyOrRecord === "string") {
2335
+ textures.set(keyOrRecord, texture);
2336
+ } else {
2337
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2338
+ }
2339
+ return { textures };
2340
+ });
2341
+ },
2342
+ dispose(key, options) {
2343
+ const state = getState();
2344
+ const refs = state._textureRefs.get(key) ?? 0;
2345
+ if (refs > 0 && !options?.force) {
2346
+ console.warn(
2347
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2348
+ );
2349
+ return false;
2350
+ }
2351
+ state.textures.get(key)?.dispose();
2352
+ setState((s) => {
2353
+ const textures = new Map(s.textures);
2354
+ textures.delete(key);
2355
+ const nextRefs = new Map(s._textureRefs);
2356
+ nextRefs.delete(key);
2357
+ return { textures, _textureRefs: nextRefs };
2358
+ });
2359
+ return true;
2360
+ },
2361
+ disposeAll() {
2362
+ for (const texture of getState().textures.values()) texture.dispose();
2363
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2364
+ }
3364
2365
  };
3365
2366
  }, [store]);
2367
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2368
+ const selected = useThree(subscribe);
2369
+ return selector ? selected : registry;
3366
2370
  }
3367
2371
 
3368
2372
  function useRenderTarget(widthOrOptions, heightOrOptions, options) {
@@ -3417,7 +2421,7 @@ function addEffect(callback) {
3417
2421
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
3418
2422
  });
3419
2423
  const id = `legacy_effect_${effectId++}`;
3420
- return getScheduler().registerGlobal("before", id, callback);
2424
+ return scheduler.getScheduler().registerGlobal("before", id, callback);
3421
2425
  }
3422
2426
  function addAfterEffect(callback) {
3423
2427
  notifyDepreciated({
@@ -3426,7 +2430,7 @@ function addAfterEffect(callback) {
3426
2430
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
3427
2431
  });
3428
2432
  const id = `legacy_afterEffect_${effectId++}`;
3429
- return getScheduler().registerGlobal("after", id, callback);
2433
+ return scheduler.getScheduler().registerGlobal("after", id, callback);
3430
2434
  }
3431
2435
  function addTail(callback) {
3432
2436
  notifyDepreciated({
@@ -3434,16 +2438,16 @@ function addTail(callback) {
3434
2438
  body: "Use scheduler.onIdle(callback) instead.\naddTail will be removed in a future version.",
3435
2439
  link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
3436
2440
  });
3437
- return getScheduler().onIdle(callback);
2441
+ return scheduler.getScheduler().onIdle(callback);
3438
2442
  }
3439
2443
  function invalidate(state, frames = 1, stackFrames = false) {
3440
- getScheduler().invalidate(frames, stackFrames);
2444
+ scheduler.getScheduler().invalidate(frames, stackFrames);
3441
2445
  }
3442
2446
  function advance(timestamp) {
3443
- getScheduler().step(timestamp);
2447
+ scheduler.getScheduler().step(timestamp);
3444
2448
  }
3445
2449
 
3446
- const version = "10.0.0-alpha.2";
2450
+ const version = "10.0.0-alpha.3";
3447
2451
  const packageData = {
3448
2452
  version: version};
3449
2453
 
@@ -14882,6 +13886,7 @@ function swapInstances() {
14882
13886
  }
14883
13887
  }
14884
13888
  if (instance.isHidden) unhideInstance(instance);
13889
+ if (isObject3D(instance.object)) removeInteractivity(findInitialRoot(instance), instance.object);
14885
13890
  if (instance.object.__r3f) delete instance.object.__r3f;
14886
13891
  if (instance.type !== "primitive") disposeOnIdle(instance.object);
14887
13892
  }
@@ -14894,6 +13899,8 @@ function swapInstances() {
14894
13899
  instance.object.__r3f = instance;
14895
13900
  setFiberRef(fiber, instance.object);
14896
13901
  delete instance.appliedOnce;
13902
+ instance.eventCount = 0;
13903
+ instance.handlers = {};
14897
13904
  applyProps(instance.object, instance.props);
14898
13905
  if (instance.props.attach) {
14899
13906
  attach(parent, instance);
@@ -15109,11 +14116,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
15109
14116
  cloneMutableTextInstance(textInstance) {
15110
14117
  return textInstance;
15111
14118
  },
14119
+ // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
14120
+ // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
14121
+ // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
14122
+ // React never calls them. They stay as no-ops rather than throws: the host config is shared by
14123
+ // every root, and a throw here would turn a React-internal probe into a hard crash for users.
14124
+ // Guarded by a test asserting a full render + unmount never reaches them.
15112
14125
  cloneRootViewTransitionContainer(_rootContainer) {
15113
- throw new Error("Not implemented.");
15114
14126
  },
15115
14127
  removeRootViewTransitionClone(_rootContainer, _clone) {
15116
- throw new Error("Not implemented.");
15117
14128
  },
15118
14129
  // https://github.com/facebook/react/pull/32465
15119
14130
  createFragmentInstance: (_fiber) => null,
@@ -15198,6 +14209,7 @@ function createRoot(canvas) {
15198
14209
  const lastConfiguredProps = {};
15199
14210
  let configured = false;
15200
14211
  let pending = null;
14212
+ let rendererSetup = null;
15201
14213
  return {
15202
14214
  async configure(props = {}) {
15203
14215
  let resolve;
@@ -15213,7 +14225,6 @@ function createRoot(canvas) {
15213
14225
  events,
15214
14226
  onCreated: onCreatedCallback,
15215
14227
  shadows = false,
15216
- textureColorSpace = webgpu.SRGBColorSpace,
15217
14228
  orthographic = false,
15218
14229
  frameloop = "always",
15219
14230
  dpr = [1, 2],
@@ -15228,7 +14239,8 @@ function createRoot(canvas) {
15228
14239
  _sizeProps,
15229
14240
  forceEven
15230
14241
  } = props;
15231
- const state = store.getState();
14242
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || webgpu.SRGBColorSpace;
14243
+ let state = store.getState();
15232
14244
  const defaultGPUProps = {
15233
14245
  canvas,
15234
14246
  antialias: true
@@ -15242,52 +14254,64 @@ function createRoot(canvas) {
15242
14254
  throw new Error("Cannot use both gl and renderer props at the same time");
15243
14255
  }
15244
14256
  let renderer = state.internal.actualRenderer;
15245
- if (primaryCanvas && !state.internal.actualRenderer) {
15246
- const primary = await waitForPrimary(primaryCanvas);
15247
- renderer = primary.renderer;
15248
- state.internal.actualRenderer = renderer;
15249
- const canvasTarget = new webgpu.CanvasTarget(canvas);
15250
- primary.store.setState((prev) => ({
15251
- internal: { ...prev.internal, isMultiCanvas: true }
15252
- }));
15253
- state.set((prev) => ({
15254
- webGPUSupported: primary.store.getState().webGPUSupported,
15255
- renderer,
15256
- primaryStore: primary.store,
15257
- internal: {
15258
- ...prev.internal,
15259
- canvasTarget,
15260
- isMultiCanvas: true,
15261
- isSecondary: true,
15262
- targetId: primaryCanvas
15263
- }
15264
- }));
15265
- } else if (!state.internal.actualRenderer) {
15266
- renderer = await resolveRenderer(rendererConfig, defaultGPUProps, webgpu.WebGPURenderer);
15267
- if (!renderer.hasInitialized?.()) {
15268
- const size2 = computeInitialSize(canvas, propsSize);
15269
- if (size2.width > 0 && size2.height > 0) {
15270
- const pixelRatio = calculateDpr(dpr);
15271
- canvas.width = size2.width * pixelRatio;
15272
- canvas.height = size2.height * pixelRatio;
15273
- }
15274
- await renderer.init();
15275
- }
15276
- const backend = renderer.backend;
15277
- const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
15278
- state.internal.actualRenderer = renderer;
15279
- state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
15280
- if (canvasId && !state.internal.isSecondary) {
15281
- const canvasTarget = new webgpu.CanvasTarget(canvas);
15282
- const unregisterPrimary = registerPrimary(canvasId, renderer, store);
15283
- state.set((prev) => ({
15284
- internal: {
15285
- ...prev.internal,
15286
- canvasTarget,
15287
- unregisterPrimary
14257
+ if (!state.internal.actualRenderer) {
14258
+ if (!rendererSetup) {
14259
+ rendererSetup = (async () => {
14260
+ if (primaryCanvas) {
14261
+ const primary = await waitForPrimary(primaryCanvas);
14262
+ renderer = primary.renderer;
14263
+ state.internal.actualRenderer = renderer;
14264
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14265
+ primary.store.setState((prev) => ({
14266
+ internal: { ...prev.internal, isMultiCanvas: true }
14267
+ }));
14268
+ state.set((prev) => ({
14269
+ webGPUSupported: primary.store.getState().webGPUSupported,
14270
+ renderer,
14271
+ primaryStore: primary.store,
14272
+ internal: {
14273
+ ...prev.internal,
14274
+ canvasTarget,
14275
+ isMultiCanvas: true,
14276
+ isSecondary: true,
14277
+ targetId: primaryCanvas
14278
+ }
14279
+ }));
14280
+ } else {
14281
+ renderer = await resolveRenderer(rendererConfig, defaultGPUProps, webgpu.WebGPURenderer);
14282
+ if (!renderer.hasInitialized?.()) {
14283
+ const size2 = computeInitialSize(canvas, propsSize);
14284
+ if (size2.width > 0 && size2.height > 0) {
14285
+ const pixelRatio = calculateDpr(dpr);
14286
+ canvas.width = size2.width * pixelRatio;
14287
+ canvas.height = size2.height * pixelRatio;
14288
+ }
14289
+ await renderer.init();
14290
+ }
14291
+ const backend = renderer.backend;
14292
+ const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14293
+ state.internal.actualRenderer = renderer;
14294
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14295
+ if (canvasId && !state.internal.isSecondary) {
14296
+ const canvasTarget = new webgpu.CanvasTarget(canvas);
14297
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14298
+ state.set((prev) => ({
14299
+ internal: {
14300
+ ...prev.internal,
14301
+ canvasTarget,
14302
+ unregisterPrimary
14303
+ }
14304
+ }));
14305
+ }
15288
14306
  }
15289
- }));
14307
+ })().catch((err) => {
14308
+ rendererSetup = null;
14309
+ throw err;
14310
+ });
15290
14311
  }
14312
+ await rendererSetup;
14313
+ state = store.getState();
14314
+ renderer = state.internal.actualRenderer;
15291
14315
  }
15292
14316
  let raycaster = state.raycaster;
15293
14317
  if (!raycaster) state.set({ raycaster: raycaster = new webgpu.Raycaster() });
@@ -15380,7 +14404,7 @@ function createRoot(canvas) {
15380
14404
  lastConfiguredProps.performance = performance;
15381
14405
  }
15382
14406
  if (!state.xr) {
15383
- const handleXRFrame = (timestamp, frame) => {
14407
+ const handleXRFrame = (timestamp, _frame) => {
15384
14408
  const state2 = store.getState();
15385
14409
  if (state2.frameloop === "never") return;
15386
14410
  advance(timestamp);
@@ -15416,15 +14440,22 @@ function createRoot(canvas) {
15416
14440
  const oldType = renderer.shadowMap.type;
15417
14441
  renderer.shadowMap.enabled = !!shadows;
15418
14442
  if (is.boo(shadows)) {
15419
- renderer.shadowMap.type = webgpu.PCFSoftShadowMap;
14443
+ renderer.shadowMap.type = webgpu.PCFShadowMap;
15420
14444
  } else if (is.str(shadows)) {
14445
+ if (shadows === "soft") {
14446
+ notifyDepreciated({
14447
+ heading: 'shadows="soft" is deprecated',
14448
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14449
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14450
+ });
14451
+ }
15421
14452
  const types = {
15422
14453
  basic: webgpu.BasicShadowMap,
15423
14454
  percentage: webgpu.PCFShadowMap,
15424
- soft: webgpu.PCFSoftShadowMap,
14455
+ soft: webgpu.PCFShadowMap,
15425
14456
  variance: webgpu.VSMShadowMap
15426
14457
  };
15427
- renderer.shadowMap.type = types[shadows] ?? webgpu.PCFSoftShadowMap;
14458
+ renderer.shadowMap.type = types[shadows] ?? webgpu.PCFShadowMap;
15428
14459
  } else if (is.obj(shadows)) {
15429
14460
  Object.assign(renderer.shadowMap, shadows);
15430
14461
  }
@@ -15440,24 +14471,35 @@ function createRoot(canvas) {
15440
14471
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
15441
14472
  lastConfiguredProps.textureColorSpace = textureColorSpace;
15442
14473
  }
14474
+ const r3fProps = ["textureColorSpace"];
14475
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14476
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
15443
14477
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
15444
- applyProps(renderer, glConfig);
14478
+ const glProps = {};
14479
+ for (const key in glConfig) {
14480
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14481
+ }
14482
+ applyProps(renderer, glProps);
15445
14483
  }
15446
14484
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
15447
14485
  const currentRenderer = state.renderer;
15448
14486
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
15449
- applyProps(currentRenderer, rendererConfig);
14487
+ const rendererProps = {};
14488
+ for (const key in rendererConfig) {
14489
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14490
+ }
14491
+ applyProps(currentRenderer, rendererProps);
15450
14492
  }
15451
14493
  }
15452
- const scheduler = getScheduler();
14494
+ const scheduler$1 = scheduler.getScheduler();
15453
14495
  const rootId = state.internal.rootId;
15454
14496
  if (!rootId) {
15455
- const newRootId = canvasId || scheduler.generateRootId();
15456
- const unregisterRoot = scheduler.registerRoot(newRootId, {
14497
+ const newRootId = canvasId || scheduler$1.generateRootId();
14498
+ const unregisterRoot = scheduler$1.registerRoot(newRootId, {
15457
14499
  getState: () => store.getState(),
15458
14500
  onError: (err) => store.getState().setError(err)
15459
14501
  });
15460
- const unregisterCanvasTarget = scheduler.register(
14502
+ const unregisterCanvasTarget = scheduler$1.register(
15461
14503
  () => {
15462
14504
  const state2 = store.getState();
15463
14505
  if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
@@ -15472,7 +14514,7 @@ function createRoot(canvas) {
15472
14514
  system: true
15473
14515
  }
15474
14516
  );
15475
- const unregisterEventsFlush = scheduler.register(
14517
+ const unregisterEventsFlush = scheduler$1.register(
15476
14518
  () => {
15477
14519
  const state2 = store.getState();
15478
14520
  state2.events.flush?.();
@@ -15484,7 +14526,7 @@ function createRoot(canvas) {
15484
14526
  system: true
15485
14527
  }
15486
14528
  );
15487
- const unregisterFrustum = scheduler.register(
14529
+ const unregisterFrustum = scheduler$1.register(
15488
14530
  () => {
15489
14531
  const state2 = store.getState();
15490
14532
  if (state2.autoUpdateFrustum && state2.camera) {
@@ -15494,11 +14536,11 @@ function createRoot(canvas) {
15494
14536
  {
15495
14537
  id: `${newRootId}_frustum`,
15496
14538
  rootId: newRootId,
15497
- phase: "preRender",
14539
+ before: "render",
15498
14540
  system: true
15499
14541
  }
15500
14542
  );
15501
- const unregisterVisibility = scheduler.register(
14543
+ const unregisterVisibility = scheduler$1.register(
15502
14544
  () => {
15503
14545
  const state2 = store.getState();
15504
14546
  checkVisibility(state2);
@@ -15506,16 +14548,16 @@ function createRoot(canvas) {
15506
14548
  {
15507
14549
  id: `${newRootId}_visibility`,
15508
14550
  rootId: newRootId,
15509
- phase: "preRender",
14551
+ before: "render",
15510
14552
  system: true,
15511
14553
  after: `${newRootId}_frustum`
15512
14554
  }
15513
14555
  );
15514
- const unregisterRender = scheduler.register(
14556
+ const unregisterRender = scheduler$1.register(
15515
14557
  () => {
15516
14558
  const state2 = store.getState();
15517
14559
  const renderer2 = state2.internal.actualRenderer;
15518
- const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14560
+ const userHandlesRender = scheduler$1.hasUserJobsInPhase("render", newRootId);
15519
14561
  if (userHandlesRender || state2.internal.priority) return;
15520
14562
  try {
15521
14563
  if (state2.renderPipeline?.render) state2.renderPipeline.render();
@@ -15548,11 +14590,11 @@ function createRoot(canvas) {
15548
14590
  unregisterVisibility();
15549
14591
  unregisterRender();
15550
14592
  },
15551
- scheduler
14593
+ scheduler: scheduler$1
15552
14594
  }
15553
14595
  }));
15554
14596
  }
15555
- scheduler.frameloop = frameloop;
14597
+ scheduler$1.frameloop = frameloop;
15556
14598
  onCreated = onCreatedCallback;
15557
14599
  configured = true;
15558
14600
  resolve();
@@ -15619,7 +14661,8 @@ function unmountComponentAtNode(canvas, callback) {
15619
14661
  dispose(state.scene);
15620
14662
  _roots.delete(canvas);
15621
14663
  if (callback) callback(canvas);
15622
- } catch {
14664
+ } catch (error) {
14665
+ console.warn("[R3F] Error while unmounting root; teardown may be incomplete:", error);
15623
14666
  }
15624
14667
  }, 500);
15625
14668
  }
@@ -15682,6 +14725,7 @@ function PortalInner({ state = {}, children, container }) {
15682
14725
  viewport = rootState.viewport.getCurrentViewport(camera, new webgpu.Vector3(), resolvedSize);
15683
14726
  if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
15684
14727
  }
14728
+ const set = injectState.set;
15685
14729
  return {
15686
14730
  // The intersect consists of the previous root state
15687
14731
  ...rootState,
@@ -15700,33 +14744,205 @@ function PortalInner({ state = {}, children, container }) {
15700
14744
  size: resolvedSize,
15701
14745
  viewport: { ...rootState.viewport, ...viewport },
15702
14746
  // Layers are allowed to override events
15703
- setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14747
+ setEvents: (events2) => set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
15704
14748
  // Container for child attachment - the portalScene (injected or container itself)
15705
14749
  internal: { ...rootState.internal, ...injectState.internal, container: portalScene }
15706
14750
  };
15707
14751
  });
15708
- const usePortalStore = React.useMemo(() => {
15709
- const store = traditional.createWithEqualityFn((set, get) => ({ ...rest, set, get }));
15710
- const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
15711
- onMutate(previousRoot.getState());
15712
- return store;
15713
- }, [previousRoot, container]);
15714
- useIsomorphicLayoutEffect(() => {
15715
- const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
15716
- const unsubscribe = previousRoot.subscribe(onMutate);
15717
- return unsubscribe;
15718
- }, [previousRoot, usePortalStore]);
15719
- return (
15720
- // @ts-ignore, reconciler types are not maintained
15721
- /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reconciler.createPortal(
15722
- /* @__PURE__ */ jsxRuntime.jsx(context.Provider, { value: usePortalStore, children }),
15723
- usePortalStore,
15724
- null
15725
- ) })
15726
- );
14752
+ const usePortalStore = React.useMemo(() => {
14753
+ const store = traditional.createWithEqualityFn((set, get) => ({ ...rest, set, get }));
14754
+ const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
14755
+ onMutate(previousRoot.getState());
14756
+ return store;
14757
+ }, [previousRoot, container]);
14758
+ useIsomorphicLayoutEffect(() => {
14759
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14760
+ const unsubscribe = previousRoot.subscribe(onMutate);
14761
+ return unsubscribe;
14762
+ }, [previousRoot, usePortalStore]);
14763
+ return (
14764
+ // @ts-ignore, reconciler types are not maintained
14765
+ /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: reconciler.createPortal(
14766
+ /* @__PURE__ */ jsxRuntime.jsx(context.Provider, { value: usePortalStore, children }),
14767
+ usePortalStore,
14768
+ null
14769
+ ) })
14770
+ );
14771
+ }
14772
+ function flushSync(fn) {
14773
+ return reconciler.flushSyncFromReconciler(fn);
14774
+ }
14775
+
14776
+ function parseBackground(background) {
14777
+ if (!background) return null;
14778
+ if (typeof background === "object" && !background.isColor) {
14779
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14780
+ return {
14781
+ ...rest,
14782
+ preset,
14783
+ files: envMap || files,
14784
+ backgroundFiles: backgroundMap,
14785
+ background: true
14786
+ };
14787
+ }
14788
+ if (typeof background === "number") {
14789
+ return { color: background, background: true };
14790
+ }
14791
+ if (typeof background === "string") {
14792
+ if (background in presetsObj) {
14793
+ return { preset: background, background: true };
14794
+ }
14795
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14796
+ return { files: background, background: true };
14797
+ }
14798
+ return { color: background, background: true };
14799
+ }
14800
+ if (background.isColor) {
14801
+ return { color: background, background: true };
14802
+ }
14803
+ return null;
14804
+ }
14805
+
14806
+ function parseRendererConfig(rendererProp) {
14807
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14808
+ if (isRendererConfig) {
14809
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14810
+ return {
14811
+ primaryCanvas: pc,
14812
+ scheduler: sc,
14813
+ renderer: Object.keys(rest).length > 0 ? rest : rendererProp
14814
+ };
14815
+ }
14816
+ return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
14817
+ }
14818
+
14819
+ const RESOURCE_KINDS = ["nodes", "uniforms", "buffers", "gpuStorage"];
14820
+ const ROOT_SCOPE = "";
14821
+ const registries = /* @__PURE__ */ new WeakMap();
14822
+ function getKindRegistry(store, kind) {
14823
+ let registry = registries.get(store);
14824
+ if (!registry) {
14825
+ registry = {
14826
+ nodes: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14827
+ uniforms: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14828
+ buffers: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14829
+ gpuStorage: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() }
14830
+ };
14831
+ registries.set(store, registry);
14832
+ }
14833
+ return registry[kind];
14834
+ }
14835
+ function resolvePrimary$1(store) {
14836
+ return store.getState().primaryStore ?? store;
14837
+ }
14838
+ function isScopeValid(store, kind, scopeKey) {
14839
+ return getKindRegistry(store, kind).valid.has(scopeKey);
14840
+ }
14841
+ function peekStaged(store, kind, scopeKey) {
14842
+ return getKindRegistry(store, kind).staged.get(scopeKey);
14843
+ }
14844
+ function stageEntries(store, kind, scopeKey, entries) {
14845
+ const { staged } = getKindRegistry(store, kind);
14846
+ const current = staged.get(scopeKey);
14847
+ if (current) Object.assign(current, entries);
14848
+ else staged.set(scopeKey, { ...entries });
14849
+ }
14850
+ function readCommittedScope(map, scopeKey, isLeaf) {
14851
+ if (scopeKey === ROOT_SCOPE) {
14852
+ const leaves = {};
14853
+ for (const [key, value] of Object.entries(map)) if (isLeaf(value)) leaves[key] = value;
14854
+ return leaves;
14855
+ }
14856
+ const scopeData = map[scopeKey];
14857
+ if (scopeData && typeof scopeData === "object" && !isLeaf(scopeData)) return scopeData;
14858
+ return {};
14859
+ }
14860
+ function withStagedOverlay(store, kind, committed) {
14861
+ const { staged } = getKindRegistry(store, kind);
14862
+ if (staged.size === 0) return committed;
14863
+ const view = { ...committed };
14864
+ for (const [scopeKey, entries] of staged) {
14865
+ if (scopeKey === ROOT_SCOPE) Object.assign(view, entries);
14866
+ else view[scopeKey] = { ...view[scopeKey], ...entries };
14867
+ }
14868
+ return view;
14869
+ }
14870
+ function flushStagedScope(store, kind, scopeKey, isLeaf) {
14871
+ const registry = getKindRegistry(store, kind);
14872
+ const staged = registry.staged.get(scopeKey);
14873
+ if (!staged) return;
14874
+ registry.staged.delete(scopeKey);
14875
+ const replace = !registry.valid.has(scopeKey);
14876
+ store.setState((state) => {
14877
+ const map = state[kind];
14878
+ if (scopeKey === ROOT_SCOPE) {
14879
+ if (!replace) return { [kind]: { ...map, ...staged } };
14880
+ const next = {};
14881
+ for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
14882
+ return { [kind]: { ...next, ...staged } };
14883
+ }
14884
+ const current = replace ? {} : map[scopeKey] ?? {};
14885
+ return { [kind]: { ...map, [scopeKey]: { ...current, ...staged } } };
14886
+ });
14887
+ registry.valid.add(scopeKey);
14888
+ }
14889
+ function invalidateResource(store, kind, scopeKey) {
14890
+ const registry = getKindRegistry(store, kind);
14891
+ if (scopeKey === void 0) {
14892
+ registry.valid.clear();
14893
+ registry.staged.clear();
14894
+ } else {
14895
+ registry.valid.delete(scopeKey);
14896
+ registry.staged.delete(scopeKey);
14897
+ }
14898
+ }
14899
+ function rebuildResource(store, kind, userScope) {
14900
+ const primary = resolvePrimary$1(store);
14901
+ invalidateResource(primary, kind, userScope === "root" ? ROOT_SCOPE : userScope);
14902
+ primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
14903
+ }
14904
+ function invalidateAllResources(store) {
14905
+ for (const kind of RESOURCE_KINDS) invalidateResource(store, kind);
14906
+ }
14907
+ function removeResourceEntries(store, kind, names, scope, isLeaf) {
14908
+ const staged = peekStaged(store, kind, scope ?? ROOT_SCOPE);
14909
+ if (staged) for (const name of names) delete staged[name];
14910
+ store.setState((state) => {
14911
+ const map = state[kind];
14912
+ if (scope) {
14913
+ const currentScope = { ...map[scope] };
14914
+ for (const name of names) delete currentScope[name];
14915
+ return { [kind]: { ...map, [scope]: currentScope } };
14916
+ }
14917
+ const next = { ...map };
14918
+ for (const name of names) if (isLeaf(next[name])) delete next[name];
14919
+ return { [kind]: next };
14920
+ });
15727
14921
  }
15728
- function flushSync(fn) {
15729
- return reconciler.flushSyncFromReconciler(fn);
14922
+ function clearResourceEntries(store, kind, scope, isLeaf) {
14923
+ const registry = getKindRegistry(store, kind);
14924
+ if (scope && scope !== "root") registry.staged.delete(scope);
14925
+ else if (scope === "root") registry.staged.delete(ROOT_SCOPE);
14926
+ else registry.staged.clear();
14927
+ store.setState((state) => {
14928
+ const map = state[kind];
14929
+ if (scope && scope !== "root") {
14930
+ const { [scope]: _, ...rest } = map;
14931
+ return { [kind]: rest };
14932
+ }
14933
+ if (scope === "root") {
14934
+ const next = {};
14935
+ for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
14936
+ return { [kind]: next };
14937
+ }
14938
+ return { [kind]: {} };
14939
+ });
14940
+ }
14941
+
14942
+ function clearHmrCaches(store) {
14943
+ const primary = store.getState().primaryStore ?? store;
14944
+ invalidateAllResources(primary);
14945
+ primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
15730
14946
  }
15731
14947
 
15732
14948
  function CanvasImpl({
@@ -15749,6 +14965,8 @@ function CanvasImpl({
15749
14965
  raycaster,
15750
14966
  camera,
15751
14967
  scene,
14968
+ autoUpdateFrustum,
14969
+ occlusion,
15752
14970
  onPointerMissed,
15753
14971
  onDragOverMissed,
15754
14972
  onDropMissed,
@@ -15760,39 +14978,10 @@ function CanvasImpl({
15760
14978
  forceEven,
15761
14979
  ...props
15762
14980
  }) {
15763
- const { primaryCanvas, scheduler, ...rendererConfig } = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp) ? rendererProp : { primaryCanvas: void 0, scheduler: void 0 };
15764
- const renderer = Object.keys(rendererConfig).length > 0 ? rendererConfig : rendererProp;
14981
+ const { primaryCanvas, scheduler, renderer } = parseRendererConfig(rendererProp);
15765
14982
  React__namespace.useMemo(() => extend(THREE), []);
15766
14983
  const Bridge = useBridge();
15767
- const backgroundProps = React__namespace.useMemo(() => {
15768
- if (!background) return null;
15769
- if (typeof background === "object" && !background.isColor) {
15770
- const { backgroundMap, envMap, files, preset, ...rest } = background;
15771
- return {
15772
- ...rest,
15773
- preset,
15774
- files: envMap || files,
15775
- backgroundFiles: backgroundMap,
15776
- background: true
15777
- };
15778
- }
15779
- if (typeof background === "number") {
15780
- return { color: background, background: true };
15781
- }
15782
- if (typeof background === "string") {
15783
- if (background in presetsObj) {
15784
- return { preset: background, background: true };
15785
- }
15786
- if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
15787
- return { files: background, background: true };
15788
- }
15789
- return { color: background, background: true };
15790
- }
15791
- if (background.isColor) {
15792
- return { color: background, background: true };
15793
- }
15794
- return null;
15795
- }, [background]);
14984
+ const backgroundProps = React__namespace.useMemo(() => parseBackground(background), [background]);
15796
14985
  const hasInitialSizeRef = React__namespace.useRef(false);
15797
14986
  const measureConfig = React__namespace.useMemo(() => {
15798
14987
  if (!hasInitialSizeRef.current) {
@@ -15834,6 +15023,7 @@ function CanvasImpl({
15834
15023
  const handleDropMissed = useMutableCallback(onDropMissed);
15835
15024
  const [block, setBlock] = React__namespace.useState(false);
15836
15025
  const [error, setError] = React__namespace.useState(false);
15026
+ const [fallbackVisible, setFallbackVisible] = React__namespace.useState(false);
15837
15027
  if (block) throw block;
15838
15028
  if (error) throw error;
15839
15029
  const root = React__namespace.useRef(null);
@@ -15841,6 +15031,7 @@ function CanvasImpl({
15841
15031
  const unsubscribeErrorRef = React__namespace.useRef(null);
15842
15032
  useIsomorphicLayoutEffect(() => {
15843
15033
  effectActiveRef.current = true;
15034
+ if (fallbackVisible) return;
15844
15035
  const canvas = canvasRef.current;
15845
15036
  if (effectiveSize.width > 0 && effectiveSize.height > 0 && canvas) {
15846
15037
  if (!root.current) {
@@ -15861,7 +15052,7 @@ function CanvasImpl({
15861
15052
  }
15862
15053
  async function run() {
15863
15054
  if (!effectActiveRef.current || !root.current) return;
15864
- await root.current.configure({
15055
+ const configured = await root.current.configure({
15865
15056
  id,
15866
15057
  primaryCanvas,
15867
15058
  scheduler,
@@ -15876,6 +15067,8 @@ function CanvasImpl({
15876
15067
  performance,
15877
15068
  raycaster,
15878
15069
  camera,
15070
+ autoUpdateFrustum,
15071
+ occlusion,
15879
15072
  size: effectiveSize,
15880
15073
  // Store size props for reset functionality
15881
15074
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
@@ -15900,8 +15093,20 @@ function CanvasImpl({
15900
15093
  }
15901
15094
  onCreated?.(state);
15902
15095
  }
15903
- });
15904
- if (!effectActiveRef.current || !root.current) return;
15096
+ }).then(
15097
+ () => true,
15098
+ // Renderer setup failed (e.g. no WebGL/WebGPU support). The `fallback` prop lives
15099
+ // inside <canvas>, which browsers don't display, so surface it as visible DOM
15100
+ // instead; with no fallback, rethrow to an external error boundary. (#3757)
15101
+ (setupError) => {
15102
+ if (effectActiveRef.current) {
15103
+ if (fallback != null) setFallbackVisible(true);
15104
+ else setError(setupError);
15105
+ }
15106
+ return false;
15107
+ }
15108
+ );
15109
+ if (!configured || !effectActiveRef.current || !root.current) return;
15905
15110
  root.current.render(
15906
15111
  /* @__PURE__ */ jsxRuntime.jsx(Bridge, { children: /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(Block, { set: setBlock }), children: [
15907
15112
  backgroundProps && /* @__PURE__ */ jsxRuntime.jsx(Environment, { ...backgroundProps }),
@@ -15935,14 +15140,7 @@ function CanvasImpl({
15935
15140
  const handleHMR = () => {
15936
15141
  queueMicrotask(() => {
15937
15142
  const rootEntry = _roots.get(canvas);
15938
- if (rootEntry?.store) {
15939
- console.log("[R3F] HMR detected \u2014 rebuilding nodes/uniforms");
15940
- rootEntry.store.setState((state) => ({
15941
- nodes: {},
15942
- uniforms: {},
15943
- _hmrVersion: state._hmrVersion + 1
15944
- }));
15945
- }
15143
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
15946
15144
  });
15947
15145
  };
15948
15146
  if (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) }) !== "undefined" && undefined) {
@@ -15971,7 +15169,20 @@ function CanvasImpl({
15971
15169
  ...style
15972
15170
  },
15973
15171
  ...props,
15974
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, id, className: "r3f-canvas", style: { display: "block" }, children: fallback }) })
15172
+ children: fallbackVisible ? (
15173
+ // Renderer setup failed: render the fallback as visible DOM. Inside <canvas> (below)
15174
+ // it exists in the tree but browsers never paint it, which is the whole bug (#3757).
15175
+ fallback
15176
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(
15177
+ "canvas",
15178
+ {
15179
+ ref: canvasRef,
15180
+ id,
15181
+ className: "r3f-canvas",
15182
+ style: { display: "block", width: "100%", height: "100%" },
15183
+ children: fallback
15184
+ }
15185
+ ) })
15975
15186
  }
15976
15187
  );
15977
15188
  }
@@ -16044,35 +15255,50 @@ let ScopedStore = _ScopedStore;
16044
15255
  function createScopedStore(data) {
16045
15256
  return new ScopedStore(data);
16046
15257
  }
16047
- function createLazyCreatorState(state) {
15258
+ function createLazyCreatorState(state, store) {
16048
15259
  let _uniforms = null;
16049
15260
  let _nodes = null;
16050
15261
  let _buffers = null;
16051
15262
  let _gpuStorage = null;
15263
+ const view = (kind) => store ? withStagedOverlay(store, kind, state[kind]) : state[kind];
16052
15264
  return Object.create(state, {
16053
15265
  uniforms: {
16054
15266
  get() {
16055
- return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
15267
+ return _uniforms ?? (_uniforms = createScopedStore(view("uniforms")));
16056
15268
  }
16057
15269
  },
16058
15270
  nodes: {
16059
15271
  get() {
16060
- return _nodes ?? (_nodes = createScopedStore(state.nodes));
15272
+ return _nodes ?? (_nodes = createScopedStore(view("nodes")));
16061
15273
  }
16062
15274
  },
16063
15275
  buffers: {
16064
15276
  get() {
16065
- return _buffers ?? (_buffers = createScopedStore(state.buffers));
15277
+ return _buffers ?? (_buffers = createScopedStore(view("buffers")));
16066
15278
  }
16067
15279
  },
16068
15280
  gpuStorage: {
16069
15281
  get() {
16070
- return _gpuStorage ?? (_gpuStorage = createScopedStore(state.gpuStorage));
15282
+ return _gpuStorage ?? (_gpuStorage = createScopedStore(view("gpuStorage")));
16071
15283
  }
16072
15284
  }
16073
15285
  });
16074
15286
  }
16075
15287
 
15288
+ function resolvePrimary(local) {
15289
+ return local.getState().primaryStore ?? local;
15290
+ }
15291
+ function usePrimaryStore() {
15292
+ const local = useStore();
15293
+ const primary = local.getState().primaryStore;
15294
+ return React.useMemo(() => primary ?? local, [primary, local]);
15295
+ }
15296
+ function usePrimaryThree(selector = (state) => state, equalityFn) {
15297
+ const local = useStore();
15298
+ const primary = resolvePrimary(local);
15299
+ return primary(selector, equalityFn);
15300
+ }
15301
+
16076
15302
  function addTexture(set, key, value) {
16077
15303
  set((state) => {
16078
15304
  const newMap = new Map(state.textures);
@@ -16176,65 +15402,56 @@ function useCompareMemoize(value, deep) {
16176
15402
  return ref.current;
16177
15403
  }
16178
15404
 
15405
+ function useScopedResource(options) {
15406
+ const { store, kind, scope, isLeaf, create, prepare, reconcile, input } = options;
15407
+ const scopeKey = scope ?? ROOT_SCOPE;
15408
+ const version = store((s) => s._hmrVersion);
15409
+ const entries = React.useMemo(() => {
15410
+ const staged = peekStaged(store, kind, scopeKey);
15411
+ const reusable = isScopeValid(store, kind, scopeKey);
15412
+ const map = store.getState()[kind];
15413
+ const committed = reusable ? readCommittedScope(map, scopeKey, isLeaf) : void 0;
15414
+ const result = {};
15415
+ let fresh = null;
15416
+ for (const [name, candidate] of Object.entries(create())) {
15417
+ const existing = staged?.[name] ?? committed?.[name];
15418
+ if (existing !== void 0) {
15419
+ result[name] = existing;
15420
+ reconcile?.(existing, candidate);
15421
+ } else {
15422
+ const value = prepare ? prepare(name, candidate) : candidate;
15423
+ result[name] = value;
15424
+ (fresh ?? (fresh = {}))[name] = value;
15425
+ }
15426
+ }
15427
+ if (fresh) stageEntries(store, kind, scopeKey, fresh);
15428
+ return result;
15429
+ }, [store, kind, scopeKey, version, input]);
15430
+ useIsomorphicLayoutEffect(() => {
15431
+ flushStagedScope(store, kind, scopeKey, isLeaf);
15432
+ });
15433
+ return entries;
15434
+ }
15435
+
16179
15436
  const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
16180
15437
  function useUniforms(creatorOrScope, scope) {
16181
- const store = useStore();
15438
+ const store = usePrimaryStore();
16182
15439
  const removeUniforms2 = React.useCallback(
16183
- (names, targetScope) => {
16184
- const nameArray = Array.isArray(names) ? names : [names];
16185
- store.setState((state) => {
16186
- if (targetScope) {
16187
- const currentScope = { ...state.uniforms[targetScope] };
16188
- for (const name of nameArray) delete currentScope[name];
16189
- return { uniforms: { ...state.uniforms, [targetScope]: currentScope } };
16190
- }
16191
- const uniforms2 = { ...state.uniforms };
16192
- for (const name of nameArray) if (isUniformNode$1(uniforms2[name])) delete uniforms2[name];
16193
- return { uniforms: uniforms2 };
16194
- });
16195
- },
15440
+ (names, targetScope) => removeResourceEntries(store, "uniforms", Array.isArray(names) ? names : [names], targetScope, isUniformNode$1),
16196
15441
  [store]
16197
15442
  );
16198
15443
  const clearUniforms = React.useCallback(
16199
- (targetScope) => {
16200
- store.setState((state) => {
16201
- if (targetScope && targetScope !== "root") {
16202
- const { [targetScope]: _, ...rest } = state.uniforms;
16203
- return { uniforms: rest };
16204
- }
16205
- if (targetScope === "root") {
16206
- const uniforms2 = {};
16207
- for (const [key, value] of Object.entries(state.uniforms)) {
16208
- if (!isUniformNode$1(value)) uniforms2[key] = value;
16209
- }
16210
- return { uniforms: uniforms2 };
16211
- }
16212
- return { uniforms: {} };
16213
- });
16214
- },
15444
+ (targetScope) => clearResourceEntries(store, "uniforms", targetScope, isUniformNode$1),
16215
15445
  [store]
16216
15446
  );
16217
15447
  const rebuildUniforms = React.useCallback(
16218
- (targetScope) => {
16219
- store.setState((state) => {
16220
- let newUniforms = {};
16221
- if (targetScope && targetScope !== "root") {
16222
- const { [targetScope]: _, ...rest } = state.uniforms;
16223
- newUniforms = rest;
16224
- } else if (targetScope === "root") {
16225
- for (const [key, value] of Object.entries(state.uniforms)) {
16226
- if (!isUniformNode$1(value)) newUniforms[key] = value;
16227
- }
16228
- }
16229
- return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
16230
- });
16231
- },
15448
+ (targetScope) => rebuildResource(store, "uniforms", targetScope),
16232
15449
  [store]
16233
15450
  );
16234
15451
  const inputForMemoization = React.useMemo(() => {
16235
15452
  let raw = creatorOrScope;
16236
15453
  if (is.fun(creatorOrScope)) {
16237
- const wrappedState = createLazyCreatorState(store.getState());
15454
+ const wrappedState = createLazyCreatorState(store.getState(), store);
16238
15455
  raw = creatorOrScope(wrappedState);
16239
15456
  }
16240
15457
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
@@ -16248,80 +15465,44 @@ function useUniforms(creatorOrScope, scope) {
16248
15465
  }, [creatorOrScope, store]);
16249
15466
  const memoizedInput = useCompareMemoize(inputForMemoization);
16250
15467
  const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
16251
- const storeUniforms = useThree((s) => s.uniforms);
16252
- const hmrVersion = useThree((s) => s._hmrVersion);
16253
- const readerDep = isReader ? storeUniforms : null;
16254
- const creatorDep = isReader ? null : hmrVersion;
16255
- const uniforms = React.useMemo(() => {
16256
- if (memoizedInput === void 0) {
16257
- return storeUniforms;
16258
- }
16259
- if (typeof memoizedInput === "string") {
16260
- const scopeData = storeUniforms[memoizedInput];
16261
- if (scopeData && !isUniformNode$1(scopeData)) return scopeData;
16262
- return {};
16263
- }
16264
- const state = store.getState();
16265
- const set = store.setState;
16266
- if (typeof memoizedInput !== "object" || memoizedInput === null) {
16267
- throw new Error("Invalid uniform input");
16268
- }
16269
- const created = memoizedInput;
16270
- const result = {};
16271
- let hasNewUniforms = false;
16272
- let targetRecord = state.uniforms;
16273
- if (scope) {
16274
- if (!state.uniforms[scope]) state.uniforms[scope] = {};
16275
- targetRecord = state.uniforms[scope];
16276
- }
16277
- for (const [name, node] of Object.entries(created)) {
16278
- if (targetRecord[name]) {
16279
- result[name] = targetRecord[name];
16280
- const existingVal = result[name].value;
16281
- const newVal = vectorize(node);
16282
- let equals = newVal === existingVal;
16283
- if (!equals && hasEqualsMethod(existingVal) && hasEqualsMethod(newVal)) {
16284
- if (isThreeVector(existingVal) && isThreeVector(newVal)) {
16285
- equals = vectorEquals(existingVal, newVal);
16286
- } else if (isSameThreeType(existingVal, newVal)) {
16287
- equals = existingVal.equals(newVal);
16288
- }
15468
+ const storeUniforms = usePrimaryThree((s) => s.uniforms);
15469
+ const created = useScopedResource({
15470
+ store,
15471
+ kind: "uniforms",
15472
+ scope: isReader ? void 0 : scope,
15473
+ isLeaf: isUniformNode$1,
15474
+ input: memoizedInput,
15475
+ create: () => {
15476
+ if (isReader) return {};
15477
+ if (typeof memoizedInput !== "object" || memoizedInput === null) throw new Error("Invalid uniform input");
15478
+ return memoizedInput;
15479
+ },
15480
+ prepare: (name, candidate) => createUniform(name, candidate, scope),
15481
+ reconcile: (existing, candidate) => {
15482
+ const existingVal = existing.value;
15483
+ const newVal = vectorize(candidate);
15484
+ let equals = newVal === existingVal;
15485
+ if (!equals && hasEqualsMethod(existingVal) && hasEqualsMethod(newVal)) {
15486
+ if (isThreeVector(existingVal) && isThreeVector(newVal)) {
15487
+ equals = vectorEquals(existingVal, newVal);
15488
+ } else if (isSameThreeType(existingVal, newVal)) {
15489
+ equals = existingVal.equals(newVal);
16289
15490
  }
16290
- if (!equals) result[name].value = newVal;
16291
- } else {
16292
- result[name] = createUniform(name, node, scope);
16293
- hasNewUniforms = true;
16294
- }
16295
- }
16296
- if (hasNewUniforms) {
16297
- if (scope) {
16298
- set((s) => ({
16299
- uniforms: {
16300
- ...s.uniforms,
16301
- [scope]: { ...s.uniforms[scope], ...result }
16302
- }
16303
- }));
16304
- } else {
16305
- set((s) => ({ uniforms: { ...s.uniforms, ...result } }));
16306
15491
  }
15492
+ if (!equals) existing.value = newVal;
16307
15493
  }
16308
- return result;
16309
- }, [store, memoizedInput, scope, readerDep, creatorDep]);
15494
+ });
15495
+ let uniforms = created;
15496
+ if (memoizedInput === void 0) {
15497
+ uniforms = storeUniforms;
15498
+ } else if (typeof memoizedInput === "string") {
15499
+ const scopeData = storeUniforms[memoizedInput];
15500
+ uniforms = scopeData && !isUniformNode$1(scopeData) ? scopeData : {};
15501
+ }
16310
15502
  return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
16311
15503
  }
16312
15504
  function rebuildAllUniforms(store, scope) {
16313
- store.setState((state) => {
16314
- let newUniforms = {};
16315
- if (scope && scope !== "root") {
16316
- const { [scope]: _, ...rest } = state.uniforms;
16317
- newUniforms = rest;
16318
- } else if (scope === "root") {
16319
- for (const [key, value] of Object.entries(state.uniforms)) {
16320
- if (!isUniformNode$1(value)) newUniforms[key] = value;
16321
- }
16322
- }
16323
- return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
16324
- });
15505
+ rebuildResource(store, "uniforms", scope);
16325
15506
  }
16326
15507
  function removeUniforms(set, names, scope) {
16327
15508
  set((state) => {
@@ -16385,191 +15566,91 @@ function isSameThreeType(a, b) {
16385
15566
  return obj_a.isColor && obj_b.isColor || obj_a.isMatrix3 && obj_b.isMatrix3 || obj_a.isMatrix4 && obj_b.isMatrix4 || obj_a.isEuler && obj_b.isEuler || obj_a.isQuaternion && obj_b.isQuaternion;
16386
15567
  }
16387
15568
 
15569
+ const uniformOf = tsl.uniform;
16388
15570
  const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
16389
15571
  const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
16390
15572
  function useUniform(name, value) {
16391
15573
  const store = useStore();
16392
- const hmrVersion = useThree((s) => s._hmrVersion);
16393
- return React.useMemo(() => {
16394
- const state = store.getState();
16395
- const set = store.setState;
16396
- const existing = state.uniforms[name];
16397
- if (existing && isUniformNode(existing)) {
15574
+ const registered = useScopedResource({
15575
+ store,
15576
+ kind: "uniforms",
15577
+ scope: void 0,
15578
+ isLeaf: isUniformNode,
15579
+ input: name,
15580
+ create: () => value === void 0 ? {} : { [name]: value },
15581
+ prepare: createNamedUniform,
15582
+ reconcile: (existing2) => {
16398
15583
  if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
16399
- existing.value = typeof value === "string" ? new webgpu.Color(value) : value;
15584
+ existing2.value = typeof value === "string" ? new webgpu.Color(value) : value;
16400
15585
  }
16401
- return existing;
16402
- }
16403
- if (value === void 0) {
16404
- throw new Error(
16405
- `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
16406
- );
16407
- }
16408
- if (isUniformNode(value)) {
16409
- const node2 = value;
16410
- if (typeof node2.setName === "function") {
16411
- node2.setName(name);
16412
- }
16413
- set((s) => ({
16414
- uniforms: { ...s.uniforms, [name]: node2 }
16415
- }));
16416
- return node2;
16417
- }
16418
- let node;
16419
- if (isTSLNode$1(value)) {
16420
- node = tsl.uniform(value);
16421
- } else if (typeof value === "string") {
16422
- node = tsl.uniform(new webgpu.Color(value));
16423
- } else {
16424
- node = tsl.uniform(value);
16425
- }
16426
- if (typeof node.setName === "function") {
16427
- node.setName(name);
16428
15586
  }
16429
- set((s) => ({
16430
- uniforms: {
16431
- ...s.uniforms,
16432
- [name]: node
16433
- }
16434
- }));
16435
- return node;
16436
- }, [store, name, hmrVersion]);
15587
+ });
15588
+ if (registered[name]) return registered[name];
15589
+ const existing = peekStaged(store, "uniforms", ROOT_SCOPE)?.[name] ?? store.getState().uniforms[name];
15590
+ if (existing && isUniformNode(existing)) return existing;
15591
+ throw new Error(
15592
+ `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
15593
+ );
15594
+ }
15595
+ function createNamedUniform(name, value) {
15596
+ let node;
15597
+ if (isUniformNode(value)) {
15598
+ node = value;
15599
+ } else if (isTSLNode$1(value)) {
15600
+ node = uniformOf(value);
15601
+ } else if (typeof value === "string") {
15602
+ node = tsl.uniform(new webgpu.Color(value));
15603
+ } else {
15604
+ node = uniformOf(value);
15605
+ }
15606
+ if (typeof node.setName === "function") {
15607
+ node.setName(name);
15608
+ }
15609
+ return node;
16437
15610
  }
16438
15611
 
16439
15612
  const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
16440
15613
  function useNodes(creatorOrScope, scope) {
16441
- const store = useStore();
15614
+ const store = usePrimaryStore();
16442
15615
  const removeNodes2 = React.useCallback(
16443
- (names, targetScope) => {
16444
- const nameArray = Array.isArray(names) ? names : [names];
16445
- store.setState((state) => {
16446
- if (targetScope) {
16447
- const currentScope = { ...state.nodes[targetScope] };
16448
- for (const name of nameArray) delete currentScope[name];
16449
- return { nodes: { ...state.nodes, [targetScope]: currentScope } };
16450
- }
16451
- const nodes2 = { ...state.nodes };
16452
- for (const name of nameArray) if (isTSLNode(nodes2[name])) delete nodes2[name];
16453
- return { nodes: nodes2 };
16454
- });
16455
- },
15616
+ (names, targetScope) => removeResourceEntries(store, "nodes", Array.isArray(names) ? names : [names], targetScope, isTSLNode),
16456
15617
  [store]
16457
15618
  );
16458
15619
  const clearNodes = React.useCallback(
16459
- (targetScope) => {
16460
- store.setState((state) => {
16461
- if (targetScope && targetScope !== "root") {
16462
- const { [targetScope]: _, ...rest } = state.nodes;
16463
- return { nodes: rest };
16464
- }
16465
- if (targetScope === "root") {
16466
- const nodes2 = {};
16467
- for (const [key, value] of Object.entries(state.nodes)) {
16468
- if (!isTSLNode(value)) nodes2[key] = value;
16469
- }
16470
- return { nodes: nodes2 };
16471
- }
16472
- return { nodes: {} };
16473
- });
16474
- },
15620
+ (targetScope) => clearResourceEntries(store, "nodes", targetScope, isTSLNode),
16475
15621
  [store]
16476
15622
  );
16477
15623
  const rebuildNodes = React.useCallback(
16478
- (targetScope) => {
16479
- store.setState((state) => {
16480
- let newNodes = state.nodes;
16481
- if (targetScope && targetScope !== "root") {
16482
- const { [targetScope]: _, ...rest } = state.nodes;
16483
- newNodes = rest;
16484
- } else if (targetScope === "root") {
16485
- newNodes = {};
16486
- for (const [key, value] of Object.entries(state.nodes)) {
16487
- if (!isTSLNode(value)) newNodes[key] = value;
16488
- }
16489
- } else {
16490
- newNodes = {};
16491
- }
16492
- return { nodes: newNodes, _hmrVersion: state._hmrVersion + 1 };
16493
- });
16494
- },
15624
+ (targetScope) => rebuildResource(store, "nodes", targetScope),
16495
15625
  [store]
16496
15626
  );
16497
15627
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
16498
- const storeNodes = useThree((s) => s.nodes);
16499
- const hmrVersion = useThree((s) => s._hmrVersion);
16500
- const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
16501
- const readerDep = isReader ? storeNodes : null;
16502
- const creatorDep = isReader ? null : hmrVersion;
16503
- const nodes = React.useMemo(() => {
16504
- if (creatorOrScope === void 0) {
16505
- return storeNodes;
16506
- }
16507
- if (typeof creatorOrScope === "string") {
16508
- const scopeData = storeNodes[creatorOrScope];
16509
- if (scopeData && !isTSLNode(scopeData)) return scopeData;
16510
- return {};
16511
- }
16512
- const state = store.getState();
16513
- const set = store.setState;
16514
- const creator = creatorOrScope;
16515
- const wrappedState = createLazyCreatorState(state);
16516
- const created = creator(wrappedState);
16517
- const result = {};
16518
- let hasNewNodes = false;
16519
- if (scope) {
16520
- const currentScope = state.nodes[scope] ?? {};
16521
- for (const [name, node] of Object.entries(created)) {
16522
- if (currentScope[name]) {
16523
- result[name] = currentScope[name];
16524
- } else {
16525
- node.setName?.(`${scope}.${name}`);
16526
- result[name] = node;
16527
- hasNewNodes = true;
16528
- }
16529
- }
16530
- if (hasNewNodes) {
16531
- set((s) => ({
16532
- nodes: {
16533
- ...s.nodes,
16534
- [scope]: { ...s.nodes[scope], ...result }
16535
- }
16536
- }));
16537
- }
16538
- return result;
16539
- }
16540
- for (const [name, node] of Object.entries(created)) {
16541
- const existing = state.nodes[name];
16542
- if (existing && isTSLNode(existing)) {
16543
- result[name] = existing;
16544
- } else {
16545
- node.setName?.(name);
16546
- result[name] = node;
16547
- hasNewNodes = true;
16548
- }
16549
- }
16550
- if (hasNewNodes) {
16551
- set((s) => ({ nodes: { ...s.nodes, ...result } }));
15628
+ const storeNodes = usePrimaryThree((s) => s.nodes);
15629
+ const created = useScopedResource({
15630
+ store,
15631
+ kind: "nodes",
15632
+ scope: isReader ? void 0 : scope,
15633
+ isLeaf: isTSLNode,
15634
+ create: () => {
15635
+ if (isReader) return {};
15636
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15637
+ },
15638
+ prepare: (name, node) => {
15639
+ node.setName?.(scope ? `${scope}.${name}` : name);
15640
+ return node;
16552
15641
  }
16553
- return result;
16554
- }, [store, scopeDep, readerDep, creatorDep]);
15642
+ });
15643
+ let nodes = created;
15644
+ if (creatorOrScope === void 0) {
15645
+ nodes = storeNodes;
15646
+ } else if (typeof creatorOrScope === "string") {
15647
+ const scopeData = storeNodes[creatorOrScope];
15648
+ nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
15649
+ }
16555
15650
  return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
16556
15651
  }
16557
15652
  function rebuildAllNodes(store, scope) {
16558
- store.setState((state) => {
16559
- let newNodes = state.nodes;
16560
- if (scope && scope !== "root") {
16561
- const { [scope]: _, ...rest } = state.nodes;
16562
- newNodes = rest;
16563
- } else if (scope === "root") {
16564
- newNodes = {};
16565
- for (const [key, value] of Object.entries(state.nodes)) {
16566
- if (!isTSLNode(value)) newNodes[key] = value;
16567
- }
16568
- } else {
16569
- newNodes = {};
16570
- }
16571
- return { nodes: newNodes, _hmrVersion: state._hmrVersion + 1 };
16572
- });
15653
+ rebuildResource(store, "nodes", scope);
16573
15654
  }
16574
15655
  function removeNodes(set, names, scope) {
16575
15656
  set((state) => {
@@ -16599,13 +15680,13 @@ function clearRootNodes(set) {
16599
15680
  });
16600
15681
  }
16601
15682
  function useLocalNodes(creator) {
16602
- const store = useStore();
16603
- const uniforms = useThree((s) => s.uniforms);
16604
- const nodes = useThree((s) => s.nodes);
16605
- const textures = useThree((s) => s.textures);
16606
- const hmrVersion = useThree((s) => s._hmrVersion);
15683
+ const store = usePrimaryStore();
15684
+ const uniforms = usePrimaryThree((s) => s.uniforms);
15685
+ const nodes = usePrimaryThree((s) => s.nodes);
15686
+ const textures = usePrimaryThree((s) => s.textures);
15687
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
16607
15688
  return React.useMemo(() => {
16608
- const wrappedState = createLazyCreatorState(store.getState());
15689
+ const wrappedState = createLazyCreatorState(store.getState(), store);
16609
15690
  return creator(wrappedState);
16610
15691
  }, [store, creator, uniforms, nodes, textures, hmrVersion]);
16611
15692
  }
@@ -16624,60 +15705,17 @@ const disposeBuffer = (buffer) => {
16624
15705
  }
16625
15706
  };
16626
15707
  function useBuffers(creatorOrScope, scope) {
16627
- const store = useStore();
15708
+ const store = usePrimaryStore();
16628
15709
  const removeBuffers = React.useCallback(
16629
- (names, targetScope) => {
16630
- const nameArray = Array.isArray(names) ? names : [names];
16631
- store.setState((state) => {
16632
- if (targetScope) {
16633
- const currentScope = { ...state.buffers[targetScope] };
16634
- for (const name of nameArray) delete currentScope[name];
16635
- return { buffers: { ...state.buffers, [targetScope]: currentScope } };
16636
- }
16637
- const buffers2 = { ...state.buffers };
16638
- for (const name of nameArray) if (isBufferLike(buffers2[name])) delete buffers2[name];
16639
- return { buffers: buffers2 };
16640
- });
16641
- },
15710
+ (names, targetScope) => removeResourceEntries(store, "buffers", Array.isArray(names) ? names : [names], targetScope, isBufferLike),
16642
15711
  [store]
16643
15712
  );
16644
15713
  const clearBuffers = React.useCallback(
16645
- (targetScope) => {
16646
- store.setState((state) => {
16647
- if (targetScope && targetScope !== "root") {
16648
- const { [targetScope]: _, ...rest } = state.buffers;
16649
- return { buffers: rest };
16650
- }
16651
- if (targetScope === "root") {
16652
- const buffers2 = {};
16653
- for (const [key, value] of Object.entries(state.buffers)) {
16654
- if (!isBufferLike(value)) buffers2[key] = value;
16655
- }
16656
- return { buffers: buffers2 };
16657
- }
16658
- return { buffers: {} };
16659
- });
16660
- },
15714
+ (targetScope) => clearResourceEntries(store, "buffers", targetScope, isBufferLike),
16661
15715
  [store]
16662
15716
  );
16663
15717
  const rebuildBuffers = React.useCallback(
16664
- (targetScope) => {
16665
- store.setState((state) => {
16666
- let newBuffers = state.buffers;
16667
- if (targetScope && targetScope !== "root") {
16668
- const { [targetScope]: _, ...rest } = state.buffers;
16669
- newBuffers = rest;
16670
- } else if (targetScope === "root") {
16671
- newBuffers = {};
16672
- for (const [key, value] of Object.entries(state.buffers)) {
16673
- if (!isBufferLike(value)) newBuffers[key] = value;
16674
- }
16675
- } else {
16676
- newBuffers = {};
16677
- }
16678
- return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
16679
- });
16680
- },
15718
+ (targetScope) => rebuildResource(store, "buffers", targetScope),
16681
15719
  [store]
16682
15720
  );
16683
15721
  const disposeBuffers = React.useCallback(
@@ -16695,85 +15733,34 @@ function useBuffers(creatorOrScope, scope) {
16695
15733
  [store, removeBuffers]
16696
15734
  );
16697
15735
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
16698
- const storeBuffers = useThree((s) => s.buffers);
16699
- const hmrVersion = useThree((s) => s._hmrVersion);
16700
- const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
16701
- const readerDep = isReader ? storeBuffers : null;
16702
- const creatorDep = isReader ? null : hmrVersion;
16703
- const buffers = React.useMemo(() => {
16704
- if (creatorOrScope === void 0) {
16705
- return storeBuffers;
16706
- }
16707
- if (typeof creatorOrScope === "string") {
16708
- const scopeData = storeBuffers[creatorOrScope];
16709
- if (scopeData && !isBufferLike(scopeData)) return scopeData;
16710
- return {};
16711
- }
16712
- const state = store.getState();
16713
- const set = store.setState;
16714
- const creator = creatorOrScope;
16715
- const wrappedState = createLazyCreatorState(state);
16716
- const created = creator(wrappedState);
16717
- const result = {};
16718
- let hasNewBuffers = false;
16719
- if (scope) {
16720
- const currentScope = state.buffers[scope] ?? {};
16721
- for (const [name, buffer] of Object.entries(created)) {
16722
- if (currentScope[name]) {
16723
- result[name] = currentScope[name];
16724
- } else {
16725
- if ("setName" in buffer && typeof buffer.setName === "function") {
16726
- buffer.setName(`${scope}.${name}`);
16727
- }
16728
- result[name] = buffer;
16729
- hasNewBuffers = true;
16730
- }
16731
- }
16732
- if (hasNewBuffers) {
16733
- set((s) => ({
16734
- buffers: {
16735
- ...s.buffers,
16736
- [scope]: { ...s.buffers[scope], ...result }
16737
- }
16738
- }));
16739
- }
16740
- return result;
16741
- }
16742
- for (const [name, buffer] of Object.entries(created)) {
16743
- const existing = state.buffers[name];
16744
- if (existing && isBufferLike(existing)) {
16745
- result[name] = existing;
16746
- } else {
16747
- if ("setName" in buffer && typeof buffer.setName === "function") {
16748
- buffer.setName(name);
16749
- }
16750
- result[name] = buffer;
16751
- hasNewBuffers = true;
15736
+ const storeBuffers = usePrimaryThree((s) => s.buffers);
15737
+ const created = useScopedResource({
15738
+ store,
15739
+ kind: "buffers",
15740
+ scope: isReader ? void 0 : scope,
15741
+ isLeaf: isBufferLike,
15742
+ create: () => {
15743
+ if (isReader) return {};
15744
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15745
+ },
15746
+ prepare: (name, buffer) => {
15747
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15748
+ buffer.setName(scope ? `${scope}.${name}` : name);
16752
15749
  }
15750
+ return buffer;
16753
15751
  }
16754
- if (hasNewBuffers) {
16755
- set((s) => ({ buffers: { ...s.buffers, ...result } }));
16756
- }
16757
- return result;
16758
- }, [store, scopeDep, readerDep, creatorDep]);
15752
+ });
15753
+ let buffers = created;
15754
+ if (creatorOrScope === void 0) {
15755
+ buffers = storeBuffers;
15756
+ } else if (typeof creatorOrScope === "string") {
15757
+ const scopeData = storeBuffers[creatorOrScope];
15758
+ buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
15759
+ }
16759
15760
  return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
16760
15761
  }
16761
15762
  function rebuildAllBuffers(store, scope) {
16762
- store.setState((state) => {
16763
- let newBuffers = state.buffers;
16764
- if (scope && scope !== "root") {
16765
- const { [scope]: _, ...rest } = state.buffers;
16766
- newBuffers = rest;
16767
- } else if (scope === "root") {
16768
- newBuffers = {};
16769
- for (const [key, value] of Object.entries(state.buffers)) {
16770
- if (!isBufferLike(value)) newBuffers[key] = value;
16771
- }
16772
- } else {
16773
- newBuffers = {};
16774
- }
16775
- return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
16776
- });
15763
+ rebuildResource(store, "buffers", scope);
16777
15764
  }
16778
15765
 
16779
15766
  const isStorageLike = (value) => {
@@ -16790,60 +15777,17 @@ const disposeStorage = (storage) => {
16790
15777
  }
16791
15778
  };
16792
15779
  function useGPUStorage(creatorOrScope, scope) {
16793
- const store = useStore();
15780
+ const store = usePrimaryStore();
16794
15781
  const removeStorage = React.useCallback(
16795
- (names, targetScope) => {
16796
- const nameArray = Array.isArray(names) ? names : [names];
16797
- store.setState((state) => {
16798
- if (targetScope) {
16799
- const currentScope = { ...state.gpuStorage[targetScope] };
16800
- for (const name of nameArray) delete currentScope[name];
16801
- return { gpuStorage: { ...state.gpuStorage, [targetScope]: currentScope } };
16802
- }
16803
- const gpuStorage2 = { ...state.gpuStorage };
16804
- for (const name of nameArray) if (isStorageLike(gpuStorage2[name])) delete gpuStorage2[name];
16805
- return { gpuStorage: gpuStorage2 };
16806
- });
16807
- },
15782
+ (names, targetScope) => removeResourceEntries(store, "gpuStorage", Array.isArray(names) ? names : [names], targetScope, isStorageLike),
16808
15783
  [store]
16809
15784
  );
16810
15785
  const clearStorage = React.useCallback(
16811
- (targetScope) => {
16812
- store.setState((state) => {
16813
- if (targetScope && targetScope !== "root") {
16814
- const { [targetScope]: _, ...rest } = state.gpuStorage;
16815
- return { gpuStorage: rest };
16816
- }
16817
- if (targetScope === "root") {
16818
- const gpuStorage2 = {};
16819
- for (const [key, value] of Object.entries(state.gpuStorage)) {
16820
- if (!isStorageLike(value)) gpuStorage2[key] = value;
16821
- }
16822
- return { gpuStorage: gpuStorage2 };
16823
- }
16824
- return { gpuStorage: {} };
16825
- });
16826
- },
15786
+ (targetScope) => clearResourceEntries(store, "gpuStorage", targetScope, isStorageLike),
16827
15787
  [store]
16828
15788
  );
16829
15789
  const rebuildStorage = React.useCallback(
16830
- (targetScope) => {
16831
- store.setState((state) => {
16832
- let newStorage = state.gpuStorage;
16833
- if (targetScope && targetScope !== "root") {
16834
- const { [targetScope]: _, ...rest } = state.gpuStorage;
16835
- newStorage = rest;
16836
- } else if (targetScope === "root") {
16837
- newStorage = {};
16838
- for (const [key, value] of Object.entries(state.gpuStorage)) {
16839
- if (!isStorageLike(value)) newStorage[key] = value;
16840
- }
16841
- } else {
16842
- newStorage = {};
16843
- }
16844
- return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
16845
- });
16846
- },
15790
+ (targetScope) => rebuildResource(store, "gpuStorage", targetScope),
16847
15791
  [store]
16848
15792
  );
16849
15793
  const disposeStorageFn = React.useCallback(
@@ -16861,73 +15805,34 @@ function useGPUStorage(creatorOrScope, scope) {
16861
15805
  [store, removeStorage]
16862
15806
  );
16863
15807
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
16864
- const storeStorage = useThree((s) => s.gpuStorage);
16865
- const hmrVersion = useThree((s) => s._hmrVersion);
16866
- const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
16867
- const readerDep = isReader ? storeStorage : null;
16868
- const creatorDep = isReader ? null : hmrVersion;
16869
- const gpuStorage = React.useMemo(() => {
16870
- if (creatorOrScope === void 0) {
16871
- return storeStorage;
16872
- }
16873
- if (typeof creatorOrScope === "string") {
16874
- const scopeData = storeStorage[creatorOrScope];
16875
- if (scopeData && !isStorageLike(scopeData)) return scopeData;
16876
- return {};
16877
- }
16878
- const state = store.getState();
16879
- const set = store.setState;
16880
- const creator = creatorOrScope;
16881
- const wrappedState = createLazyCreatorState(state);
16882
- const created = creator(wrappedState);
16883
- const result = {};
16884
- let hasNewStorage = false;
16885
- if (scope) {
16886
- const currentScope = state.gpuStorage[scope] ?? {};
16887
- for (const [name, storage] of Object.entries(created)) {
16888
- if (currentScope[name]) {
16889
- result[name] = currentScope[name];
16890
- } else {
16891
- if ("setName" in storage && typeof storage.setName === "function") {
16892
- storage.setName(`${scope}.${name}`);
16893
- }
16894
- if ("name" in storage && typeof storage.name === "string") {
16895
- storage.name = `${scope}.${name}`;
16896
- }
16897
- result[name] = storage;
16898
- hasNewStorage = true;
16899
- }
16900
- }
16901
- if (hasNewStorage) {
16902
- set((s) => ({
16903
- gpuStorage: {
16904
- ...s.gpuStorage,
16905
- [scope]: { ...s.gpuStorage[scope], ...result }
16906
- }
16907
- }));
15808
+ const storeStorage = usePrimaryThree((s) => s.gpuStorage);
15809
+ const created = useScopedResource({
15810
+ store,
15811
+ kind: "gpuStorage",
15812
+ scope: isReader ? void 0 : scope,
15813
+ isLeaf: isStorageLike,
15814
+ create: () => {
15815
+ if (isReader) return {};
15816
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15817
+ },
15818
+ prepare: (name, storage) => {
15819
+ const label = scope ? `${scope}.${name}` : name;
15820
+ if ("setName" in storage && typeof storage.setName === "function") {
15821
+ storage.setName(label);
16908
15822
  }
16909
- return result;
16910
- }
16911
- for (const [name, storage] of Object.entries(created)) {
16912
- const existing = state.gpuStorage[name];
16913
- if (existing && isStorageLike(existing)) {
16914
- result[name] = existing;
16915
- } else {
16916
- if ("setName" in storage && typeof storage.setName === "function") {
16917
- storage.setName(name);
16918
- }
16919
- if ("name" in storage && typeof storage.name === "string") {
16920
- storage.name = name;
16921
- }
16922
- result[name] = storage;
16923
- hasNewStorage = true;
15823
+ if ("name" in storage && typeof storage.name === "string") {
15824
+ storage.name = label;
16924
15825
  }
15826
+ return storage;
16925
15827
  }
16926
- if (hasNewStorage) {
16927
- set((s) => ({ gpuStorage: { ...s.gpuStorage, ...result } }));
16928
- }
16929
- return result;
16930
- }, [store, scopeDep, readerDep, creatorDep]);
15828
+ });
15829
+ let gpuStorage = created;
15830
+ if (creatorOrScope === void 0) {
15831
+ gpuStorage = storeStorage;
15832
+ } else if (typeof creatorOrScope === "string") {
15833
+ const scopeData = storeStorage[creatorOrScope];
15834
+ gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
15835
+ }
16931
15836
  return {
16932
15837
  ...gpuStorage,
16933
15838
  removeStorage,
@@ -16937,21 +15842,7 @@ function useGPUStorage(creatorOrScope, scope) {
16937
15842
  };
16938
15843
  }
16939
15844
  function rebuildAllStorage(store, scope) {
16940
- store.setState((state) => {
16941
- let newStorage = state.gpuStorage;
16942
- if (scope && scope !== "root") {
16943
- const { [scope]: _, ...rest } = state.gpuStorage;
16944
- newStorage = rest;
16945
- } else if (scope === "root") {
16946
- newStorage = {};
16947
- for (const [key, value] of Object.entries(state.gpuStorage)) {
16948
- if (!isStorageLike(value)) newStorage[key] = value;
16949
- }
16950
- } else {
16951
- newStorage = {};
16952
- }
16953
- return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
16954
- });
15845
+ rebuildResource(store, "gpuStorage", scope);
16955
15846
  }
16956
15847
 
16957
15848
  function useRenderPipeline(mainCB, setupCB) {
@@ -16992,12 +15883,12 @@ function useRenderPipeline(mainCB, setupCB) {
16992
15883
  const state = store.getState();
16993
15884
  const set = store.setState;
16994
15885
  try {
16995
- let pp = state.renderPipeline;
15886
+ let pipeline = state.renderPipeline;
16996
15887
  let currentPasses = { ...state.passes };
16997
- let justCreatedPP = false;
16998
- if (!pp) {
16999
- pp = new webgpu.PostProcessing(renderer);
17000
- justCreatedPP = true;
15888
+ let justCreatedPipeline = false;
15889
+ if (!pipeline) {
15890
+ pipeline = new webgpu.RenderPipeline(renderer);
15891
+ justCreatedPipeline = true;
17001
15892
  }
17002
15893
  const cacheValid = scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
17003
15894
  let scenePass;
@@ -17008,9 +15899,9 @@ function useRenderPipeline(mainCB, setupCB) {
17008
15899
  scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
17009
15900
  }
17010
15901
  currentPasses.scenePass = scenePass;
17011
- if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
17012
- set({ renderPipeline: pp, passes: currentPasses });
17013
- const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
15902
+ if (!pipeline.outputNode || justCreatedPipeline) pipeline.outputNode = scenePass;
15903
+ set({ renderPipeline: pipeline, passes: currentPasses });
15904
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
17014
15905
  if (shouldRunCallbacks) {
17015
15906
  if (setupCBRef.current) {
17016
15907
  const freshState = store.getState();
@@ -17031,7 +15922,7 @@ function useRenderPipeline(mainCB, setupCB) {
17031
15922
  callbacksRanRef.current = true;
17032
15923
  }
17033
15924
  } catch (error) {
17034
- console.error("[useRenderPipeline] Setup error:", error);
15925
+ console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
17035
15926
  }
17036
15927
  }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
17037
15928
  const passes = useThree((s) => s.passes);
@@ -17049,6 +15940,8 @@ function useRenderPipeline(mainCB, setupCB) {
17049
15940
 
17050
15941
  extend(THREE);
17051
15942
 
15943
+ exports.Scheduler = scheduler.Scheduler;
15944
+ exports.getScheduler = scheduler.getScheduler;
17052
15945
  exports.Block = Block;
17053
15946
  exports.Canvas = Canvas;
17054
15947
  exports.Environment = Environment;
@@ -17064,7 +15957,6 @@ exports.R3F_BUILD_LEGACY = R3F_BUILD_LEGACY;
17064
15957
  exports.R3F_BUILD_WEBGPU = R3F_BUILD_WEBGPU;
17065
15958
  exports.REACT_INTERNAL_PROPS = REACT_INTERNAL_PROPS;
17066
15959
  exports.RESERVED_PROPS = RESERVED_PROPS;
17067
- exports.Scheduler = Scheduler;
17068
15960
  exports.Texture = Texture;
17069
15961
  exports._roots = _roots;
17070
15962
  exports.act = act;
@@ -17100,7 +15992,6 @@ exports.getInstanceProps = getInstanceProps;
17100
15992
  exports.getPrimary = getPrimary;
17101
15993
  exports.getPrimaryIds = getPrimaryIds;
17102
15994
  exports.getRootState = getRootState;
17103
- exports.getScheduler = getScheduler;
17104
15995
  exports.getUuidPrefix = getUuidPrefix;
17105
15996
  exports.hasConstructor = hasConstructor;
17106
15997
  exports.hasPrimary = hasPrimary;