@react-three/fiber 10.0.0-canary.604355a → 10.0.0-canary.6d6e5ab

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as webgpu from 'three/webgpu';
2
- import { RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene, WebGLCubeRenderTarget, HalfFloatType, Color, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, PCFSoftShadowMap, VSMShadowMap, PCFShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, Vector4, PostProcessing } from 'three/webgpu';
2
+ import { CubeRenderTarget, RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene, HalfFloatType, Color, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, ShaderMaterial, Uniform, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, Vector4, RenderPipeline } from 'three/webgpu';
3
3
  import { Inspector } from 'three/addons/inspector/Inspector.js';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
  import * as React from 'react';
@@ -12,8 +12,10 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
12
12
  import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
13
13
  import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
14
14
  import { GainMapLoader } from '@monogrid/gainmap-js';
15
- import Tb, { unstable_scheduleCallback, unstable_IdlePriority } from 'scheduler';
15
+ import Tb, { unstable_getCurrentPriorityLevel, unstable_IdlePriority, unstable_LowPriority, unstable_NormalPriority, unstable_UserBlockingPriority, unstable_ImmediatePriority, unstable_scheduleCallback } from 'scheduler';
16
16
  import { createWithEqualityFn } from 'zustand/traditional';
17
+ import { getScheduler } from '@pmndrs/scheduler';
18
+ export { Scheduler, getScheduler } from '@pmndrs/scheduler';
17
19
  import { suspend, preload, clear } from 'suspend-react';
18
20
  import { dequal } from 'dequal/lite';
19
21
  import 'zustand/shallow';
@@ -41,13 +43,16 @@ const WebGLRenderer = class WebGLRenderer2 {
41
43
  }
42
44
  };
43
45
  const WebGLRenderTarget = null;
46
+ const WebGLCubeRenderTarget = null;
44
47
 
45
48
  const THREE = /*#__PURE__*/_mergeNamespaces({
46
49
  __proto__: null,
50
+ CubeRenderTargetCompat: CubeRenderTarget,
47
51
  Inspector: Inspector,
48
52
  R3F_BUILD_LEGACY: R3F_BUILD_LEGACY,
49
53
  R3F_BUILD_WEBGPU: R3F_BUILD_WEBGPU,
50
54
  RenderTargetCompat: RenderTarget,
55
+ WebGLCubeRenderTarget: WebGLCubeRenderTarget,
51
56
  WebGLRenderTarget: WebGLRenderTarget,
52
57
  WebGLRenderer: WebGLRenderer
53
58
  }, [webgpu]);
@@ -149,7 +154,7 @@ function useEnvironment({
149
154
  useLoader$1.clear(loader, multiFile ? [files] : files);
150
155
  }
151
156
  renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
152
- }, [files, renderer.domElement]);
157
+ }, [extension, files, loader, multiFile, renderer.domElement]);
153
158
  const loaderResult = useLoader$1(
154
159
  loader,
155
160
  multiFile ? [files] : files,
@@ -321,15 +326,16 @@ function EnvironmentPortal({
321
326
  preset = void 0,
322
327
  extensions
323
328
  }) {
324
- const gl = useThree$1((state) => state.gl);
329
+ const renderer = useThree$1((state) => state.renderer);
330
+ const isLegacy = useThree$1((state) => state.isLegacy);
325
331
  const defaultScene = useThree$1((state) => state.scene);
326
332
  const camera = React.useRef(null);
327
333
  const [virtualScene] = React.useState(() => new Scene());
328
334
  const fbo = React.useMemo(() => {
329
- const fbo2 = new WebGLCubeRenderTarget(resolution);
335
+ const fbo2 = isLegacy ? new WebGLCubeRenderTarget(resolution) : new CubeRenderTarget(resolution);
330
336
  fbo2.texture.type = HalfFloatType;
331
337
  return fbo2;
332
- }, [resolution]);
338
+ }, [resolution, isLegacy]);
333
339
  React.useEffect(() => {
334
340
  return () => {
335
341
  fbo.dispose();
@@ -337,10 +343,10 @@ function EnvironmentPortal({
337
343
  }, [fbo]);
338
344
  React.useLayoutEffect(() => {
339
345
  if (frames === 1) {
340
- const autoClear = gl.autoClear;
341
- gl.autoClear = true;
342
- camera.current.update(gl, virtualScene);
343
- gl.autoClear = autoClear;
346
+ const autoClear = renderer.autoClear;
347
+ renderer.autoClear = true;
348
+ camera.current.update(renderer, virtualScene);
349
+ renderer.autoClear = autoClear;
344
350
  }
345
351
  return setEnvProps(background, scene, defaultScene, fbo.texture, {
346
352
  backgroundBlurriness: blur ?? backgroundBlurriness,
@@ -349,14 +355,29 @@ function EnvironmentPortal({
349
355
  environmentIntensity,
350
356
  environmentRotation
351
357
  });
352
- }, [children, virtualScene, fbo.texture, scene, defaultScene, background, frames, gl]);
358
+ }, [
359
+ children,
360
+ virtualScene,
361
+ fbo.texture,
362
+ scene,
363
+ defaultScene,
364
+ background,
365
+ frames,
366
+ renderer,
367
+ blur,
368
+ backgroundBlurriness,
369
+ backgroundIntensity,
370
+ backgroundRotation,
371
+ environmentIntensity,
372
+ environmentRotation
373
+ ]);
353
374
  let count = 1;
354
375
  useFrame$1(() => {
355
376
  if (frames === Infinity || count < frames) {
356
- const autoClear = gl.autoClear;
357
- gl.autoClear = true;
358
- camera.current.update(gl, virtualScene);
359
- gl.autoClear = autoClear;
377
+ const autoClear = renderer.autoClear;
378
+ renderer.autoClear = true;
379
+ camera.current.update(renderer, virtualScene);
380
+ renderer.autoClear = autoClear;
360
381
  count++;
361
382
  }
362
383
  });
@@ -420,10 +441,9 @@ function Environment(props) {
420
441
  return props.ground ? /* @__PURE__ */ jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsx(EnvironmentCube, { ...props });
421
442
  }
422
443
 
423
- var __defProp$3 = Object.defineProperty;
424
- var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
425
- var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
426
- const act = React["act"];
444
+ var __defProp$1 = Object.defineProperty;
445
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
446
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
427
447
  const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React.useLayoutEffect : React.useEffect;
428
448
  function useMutableCallback(fn) {
429
449
  const ref = React.useRef(fn);
@@ -454,7 +474,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
454
474
  return _a = class extends React.Component {
455
475
  constructor() {
456
476
  super(...arguments);
457
- __publicField$3(this, "state", { error: false });
477
+ __publicField$1(this, "state", { error: false });
458
478
  }
459
479
  componentDidCatch(err) {
460
480
  this.props.set(err);
@@ -462,7 +482,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
462
482
  render() {
463
483
  return this.state.error ? null : this.props.children;
464
484
  }
465
- }, __publicField$3(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
485
+ }, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
466
486
  })();
467
487
 
468
488
  const is = {
@@ -599,8 +619,7 @@ function prepare(target, root, type, props) {
599
619
  object,
600
620
  eventCount: 0,
601
621
  handlers: {},
602
- isHidden: false,
603
- deferredRefs: []
622
+ isHidden: false
604
623
  };
605
624
  if (object) object.__r3f = instance;
606
625
  }
@@ -757,6 +776,17 @@ function unregisterVisibility(store, object) {
757
776
  internal.visibilityRegistry.delete(object.uuid);
758
777
  internal.occlusionCache.delete(object);
759
778
  }
779
+ function updateVisibilityHandlers(store, object, handlers) {
780
+ const { internal } = store.getState();
781
+ const entry = internal.visibilityRegistry.get(object.uuid);
782
+ if (!entry) return false;
783
+ entry.handlers = handlers;
784
+ if ((handlers.onOccluded || handlers.onVisible) && !internal.occlusionEnabled) {
785
+ object.occlusionTest = true;
786
+ enableOcclusion(store);
787
+ }
788
+ return true;
789
+ }
760
790
  function checkVisibility(state) {
761
791
  const { internal, camera } = state;
762
792
  const registry = internal.visibilityRegistry;
@@ -917,9 +947,9 @@ function diffProps(instance, newProps) {
917
947
  const { root, key } = resolve(instance.object, prop);
918
948
  if (root.constructor && root.constructor.length === 0) {
919
949
  const ctor = getMemoizedPrototype(root);
920
- if (!is.und(ctor)) changedProps[key] = ctor[key];
950
+ if (!is.und(ctor)) changedProps[prop] = ctor[key];
921
951
  } else {
922
- changedProps[key] = 0;
952
+ changedProps[prop] = 0;
923
953
  }
924
954
  }
925
955
  return changedProps;
@@ -944,10 +974,7 @@ function applyProps(object, props) {
944
974
  continue;
945
975
  }
946
976
  if (value === void 0) continue;
947
- if (isFromRef(value)) {
948
- instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
949
- continue;
950
- }
977
+ if (isFromRef(value)) continue;
951
978
  if (isOnce(value)) {
952
979
  if (instance?.appliedOnce?.has(prop)) continue;
953
980
  if (instance) {
@@ -983,8 +1010,24 @@ function applyProps(object, props) {
983
1010
  } else if (isVectorLike(target) && is.num(value)) {
984
1011
  if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
985
1012
  else target.set(value);
1013
+ } else if (root instanceof ShaderMaterial && key === "uniforms" && is.obj(value)) {
1014
+ if (!is.obj(root.uniforms)) root.uniforms = {};
1015
+ const uniforms = root.uniforms;
1016
+ const nextUniforms = value;
1017
+ for (const name in nextUniforms) {
1018
+ const uniform = nextUniforms[name];
1019
+ const targetUniform = uniforms[name];
1020
+ if (targetUniform) Object.assign(targetUniform, uniform);
1021
+ else {
1022
+ const nextUniform = uniform instanceof Uniform ? uniform.clone() : new Uniform(uniform.value);
1023
+ uniforms[name] = nextUniform;
1024
+ }
1025
+ }
986
1026
  } else {
987
1027
  root[key] = value;
1028
+ if (key.endsWith("Node") && root.isMaterial) {
1029
+ root.needsUpdate = true;
1030
+ }
988
1031
  if (rootState && rootState.renderer?.outputColorSpace === SRGBColorSpace && colorMaps.includes(key) && isTexture(value) && root[key]?.isTexture && // sRGB textures must be RGBA8 since r137 https://github.com/mrdoob/three.js/pull/23129
989
1032
  root[key].format === RGBAFormat && root[key].type === UnsignedByteType) {
990
1033
  root[key].colorSpace = rootState.textureColorSpace;
@@ -998,6 +1041,9 @@ function applyProps(object, props) {
998
1041
  if (instance.eventCount && object2.raycast !== null) {
999
1042
  rootState.internal.interaction.push(object2);
1000
1043
  }
1044
+ }
1045
+ if (instance?.parent && rootState?.internal && instance.object?.isObject3D) {
1046
+ const object2 = instance.object;
1001
1047
  const root = findInitialRoot(instance);
1002
1048
  const visibilityHandlers = {
1003
1049
  onFramed: instance.handlers.onFramed,
@@ -1005,7 +1051,9 @@ function applyProps(object, props) {
1005
1051
  onVisible: instance.handlers.onVisible
1006
1052
  };
1007
1053
  if (hasVisibilityHandlers(visibilityHandlers)) {
1008
- registerVisibility(root, object2, visibilityHandlers);
1054
+ if (!updateVisibilityHandlers(root, object2, visibilityHandlers)) {
1055
+ registerVisibility(root, object2, visibilityHandlers);
1056
+ }
1009
1057
  } else {
1010
1058
  unregisterVisibility(root, object2);
1011
1059
  }
@@ -1018,38 +1066,60 @@ function applyProps(object, props) {
1018
1066
  return object;
1019
1067
  }
1020
1068
 
1069
+ const DEFAULT_POINTER_ID = 0;
1070
+ const XR_POINTER_ID_START = 1e3;
1071
+ function getPointerState(internal, pointerId) {
1072
+ let state = internal.pointerMap.get(pointerId);
1073
+ if (!state) {
1074
+ state = {
1075
+ hovered: /* @__PURE__ */ new Map(),
1076
+ captured: /* @__PURE__ */ new Map(),
1077
+ initialClick: [0, 0],
1078
+ initialHits: []
1079
+ };
1080
+ internal.pointerMap.set(pointerId, state);
1081
+ }
1082
+ return state;
1083
+ }
1084
+ function getPointerId(event) {
1085
+ return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
1086
+ }
1021
1087
  function makeId(event) {
1022
1088
  return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
1023
1089
  }
1024
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
1025
- const captureData = captures.get(obj);
1090
+ function releaseInternalPointerCapture(internal, obj, pointerId) {
1091
+ const pointerState = internal.pointerMap.get(pointerId);
1092
+ if (!pointerState) return;
1093
+ const captureData = pointerState.captured.get(obj);
1026
1094
  if (captureData) {
1027
- captures.delete(obj);
1028
- if (captures.size === 0) {
1029
- capturedMap.delete(pointerId);
1030
- captureData.target.releasePointerCapture(pointerId);
1031
- }
1095
+ pointerState.captured.delete(obj);
1096
+ captureData.target.releasePointerCapture(pointerId);
1032
1097
  }
1033
1098
  }
1034
1099
  function removeInteractivity(store, object) {
1035
1100
  const { internal } = store.getState();
1036
1101
  internal.interaction = internal.interaction.filter((o) => o !== object);
1037
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
1038
- internal.hovered.forEach((value, key) => {
1039
- if (value.eventObject === object || value.object === object) {
1040
- internal.hovered.delete(key);
1102
+ for (const [pointerId, pointerState] of internal.pointerMap) {
1103
+ pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
1104
+ pointerState.hovered.forEach((value, key) => {
1105
+ if (value.eventObject === object || value.object === object) {
1106
+ pointerState.hovered.delete(key);
1107
+ }
1108
+ });
1109
+ if (pointerState.captured.has(object)) {
1110
+ releaseInternalPointerCapture(internal, object, pointerId);
1041
1111
  }
1042
- });
1043
- internal.capturedMap.forEach((captures, pointerId) => {
1044
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
1045
- });
1112
+ }
1046
1113
  unregisterVisibility(store, object);
1047
1114
  }
1048
1115
  function createEvents(store) {
1049
- function calculateDistance(event) {
1116
+ function calculateDistance(event, pointerId) {
1050
1117
  const { internal } = store.getState();
1051
- const dx = event.offsetX - internal.initialClick[0];
1052
- const dy = event.offsetY - internal.initialClick[1];
1118
+ const pointerState = internal.pointerMap.get(pointerId);
1119
+ if (!pointerState) return 0;
1120
+ const [initialX, initialY] = pointerState.initialClick;
1121
+ const dx = event.offsetX - initialX;
1122
+ const dy = event.offsetY - initialY;
1053
1123
  return Math.round(Math.sqrt(dx * dx + dy * dy));
1054
1124
  }
1055
1125
  function filterPointerEvents(objects) {
@@ -1085,6 +1155,15 @@ function createEvents(store) {
1085
1155
  return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
1086
1156
  }
1087
1157
  let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
1158
+ const aInteractivePriority = a.object.userData?.interactivePriority;
1159
+ const bInteractivePriority = b.object.userData?.interactivePriority;
1160
+ if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
1161
+ if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
1162
+ if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
1163
+ if (aInteractivePriority !== bInteractivePriority) {
1164
+ return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
1165
+ }
1166
+ }
1088
1167
  const aState = getRootState(a.object);
1089
1168
  const bState = getRootState(b.object);
1090
1169
  const aPriority = aState?.events?.priority ?? 1;
@@ -1106,9 +1185,13 @@ function createEvents(store) {
1106
1185
  eventObject = eventObject.parent;
1107
1186
  }
1108
1187
  }
1109
- if ("pointerId" in event && state.internal.capturedMap.has(event.pointerId)) {
1110
- for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
1111
- if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1188
+ if ("pointerId" in event) {
1189
+ const pointerId = event.pointerId;
1190
+ const pointerState = state.internal.pointerMap.get(pointerId);
1191
+ if (pointerState?.captured.size) {
1192
+ for (const captureData of pointerState.captured.values()) {
1193
+ if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
1194
+ }
1112
1195
  }
1113
1196
  }
1114
1197
  return intersections;
@@ -1121,27 +1204,25 @@ function createEvents(store) {
1121
1204
  if (state) {
1122
1205
  const { raycaster, pointer, camera, internal } = state;
1123
1206
  const unprojectedPoint = new Vector3(pointer.x, pointer.y, 0).unproject(camera);
1124
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1207
+ const hasPointerCapture = (id) => {
1208
+ const pointerState = internal.pointerMap.get(id);
1209
+ return pointerState?.captured.has(hit.eventObject) ?? false;
1210
+ };
1125
1211
  const setPointerCapture = (id) => {
1126
1212
  const captureData = { intersection: hit, target: event.target };
1127
- if (internal.capturedMap.has(id)) {
1128
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
1129
- } else {
1130
- internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
1131
- }
1213
+ const pointerState = getPointerState(internal, id);
1214
+ pointerState.captured.set(hit.eventObject, captureData);
1132
1215
  event.target.setPointerCapture(id);
1133
1216
  };
1134
1217
  const releasePointerCapture = (id) => {
1135
- const captures = internal.capturedMap.get(id);
1136
- if (captures) {
1137
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
1138
- }
1218
+ releaseInternalPointerCapture(internal, hit.eventObject, id);
1139
1219
  };
1140
1220
  const extractEventProps = {};
1141
1221
  for (const prop in event) {
1142
1222
  const property = event[prop];
1143
1223
  if (typeof property !== "function") extractEventProps[prop] = property;
1144
1224
  }
1225
+ const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
1145
1226
  const raycastEvent = {
1146
1227
  ...hit,
1147
1228
  ...extractEventProps,
@@ -1152,18 +1233,19 @@ function createEvents(store) {
1152
1233
  unprojectedPoint,
1153
1234
  ray: raycaster.ray,
1154
1235
  camera,
1236
+ pointerId: eventPointerId,
1155
1237
  // Hijack stopPropagation, which just sets a flag
1156
1238
  stopPropagation() {
1157
- const capturesForPointer = "pointerId" in event && internal.capturedMap.get(event.pointerId);
1239
+ const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
1158
1240
  if (
1159
1241
  // ...if this pointer hasn't been captured
1160
- !capturesForPointer || // ... or if the hit object is capturing the pointer
1161
- capturesForPointer.has(hit.eventObject)
1242
+ !pointerState?.captured.size || // ... or if the hit object is capturing the pointer
1243
+ pointerState.captured.has(hit.eventObject)
1162
1244
  ) {
1163
1245
  raycastEvent.stopped = localState.stopped = true;
1164
- if (internal.hovered.size && Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1246
+ if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1165
1247
  const higher = intersections.slice(0, intersections.indexOf(hit));
1166
- cancelPointer([...higher, hit]);
1248
+ cancelPointer([...higher, hit], eventPointerId);
1167
1249
  }
1168
1250
  }
1169
1251
  },
@@ -1179,15 +1261,18 @@ function createEvents(store) {
1179
1261
  }
1180
1262
  return intersections;
1181
1263
  }
1182
- function cancelPointer(intersections) {
1264
+ function cancelPointer(intersections, pointerId) {
1183
1265
  const { internal } = store.getState();
1184
- for (const hoveredObj of internal.hovered.values()) {
1266
+ const pid = pointerId ?? DEFAULT_POINTER_ID;
1267
+ const pointerState = internal.pointerMap.get(pid);
1268
+ if (!pointerState) return;
1269
+ for (const [hoveredId, hoveredObj] of pointerState.hovered) {
1185
1270
  if (!intersections.length || !intersections.find(
1186
1271
  (hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
1187
1272
  )) {
1188
1273
  const eventObject = hoveredObj.eventObject;
1189
1274
  const instance = eventObject.__r3f;
1190
- internal.hovered.delete(makeId(hoveredObj));
1275
+ pointerState.hovered.delete(hoveredId);
1191
1276
  if (instance?.eventCount) {
1192
1277
  const handlers = instance.handlers;
1193
1278
  const data = { ...hoveredObj, intersections };
@@ -1216,41 +1301,118 @@ function createEvents(store) {
1216
1301
  instance?.handlers.onDropMissed?.(event);
1217
1302
  }
1218
1303
  }
1304
+ function cleanupPointer(pointerId) {
1305
+ const { internal } = store.getState();
1306
+ const pointerState = internal.pointerMap.get(pointerId);
1307
+ if (pointerState) {
1308
+ for (const [, hoveredObj] of pointerState.hovered) {
1309
+ const eventObject = hoveredObj.eventObject;
1310
+ const instance = eventObject.__r3f;
1311
+ if (instance?.eventCount) {
1312
+ const handlers = instance.handlers;
1313
+ const data = { ...hoveredObj, intersections: [] };
1314
+ handlers.onPointerOut?.(data);
1315
+ handlers.onPointerLeave?.(data);
1316
+ }
1317
+ }
1318
+ internal.pointerMap.delete(pointerId);
1319
+ }
1320
+ internal.pointerDirty.delete(pointerId);
1321
+ }
1322
+ function processDeferredPointer(event, pointerId) {
1323
+ const state = store.getState();
1324
+ const { internal } = state;
1325
+ if (!state.events.enabled) return;
1326
+ const filter = filterPointerEvents;
1327
+ const hits = intersect(event, filter);
1328
+ cancelPointer(hits, pointerId);
1329
+ function onIntersect(data) {
1330
+ const eventObject = data.eventObject;
1331
+ const instance = eventObject.__r3f;
1332
+ if (!instance?.eventCount) return;
1333
+ const handlers = instance.handlers;
1334
+ if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1335
+ const id = makeId(data);
1336
+ const pointerState = getPointerState(internal, pointerId);
1337
+ const hoveredItem = pointerState.hovered.get(id);
1338
+ if (!hoveredItem) {
1339
+ pointerState.hovered.set(id, data);
1340
+ handlers.onPointerOver?.(data);
1341
+ handlers.onPointerEnter?.(data);
1342
+ } else if (hoveredItem.stopped) {
1343
+ data.stopPropagation();
1344
+ }
1345
+ }
1346
+ handlers.onPointerMove?.(data);
1347
+ }
1348
+ handleIntersects(hits, event, 0, onIntersect);
1349
+ }
1219
1350
  function handlePointer(name) {
1220
1351
  switch (name) {
1221
1352
  case "onPointerLeave":
1222
- case "onPointerCancel":
1223
1353
  case "onDragLeave":
1224
1354
  return () => cancelPointer([]);
1355
+ // Global cancel of these events
1356
+ case "onPointerCancel":
1357
+ return (event) => {
1358
+ const pointerId = getPointerId(event);
1359
+ cleanupPointer(pointerId);
1360
+ };
1225
1361
  case "onLostPointerCapture":
1226
1362
  return (event) => {
1227
1363
  const { internal } = store.getState();
1228
- if ("pointerId" in event && internal.capturedMap.has(event.pointerId)) {
1364
+ const pointerId = getPointerId(event);
1365
+ const pointerState = internal.pointerMap.get(pointerId);
1366
+ if (pointerState?.captured.size) {
1229
1367
  requestAnimationFrame(() => {
1230
- if (internal.capturedMap.has(event.pointerId)) {
1231
- internal.capturedMap.delete(event.pointerId);
1232
- cancelPointer([]);
1368
+ const pointerState2 = internal.pointerMap.get(pointerId);
1369
+ if (pointerState2?.captured.size) {
1370
+ pointerState2.captured.clear();
1233
1371
  }
1372
+ cancelPointer([], pointerId);
1234
1373
  });
1235
1374
  }
1236
1375
  };
1237
1376
  }
1238
1377
  return function handleEvent(event) {
1239
1378
  const state = store.getState();
1240
- const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
1379
+ const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
1380
+ const pointerId = getPointerId(event);
1241
1381
  internal.lastEvent.current = event;
1242
- if (!state.events.enabled) return;
1382
+ if (!events.enabled) return;
1243
1383
  const isPointerMove = name === "onPointerMove";
1244
1384
  const isDragOver = name === "onDragOver";
1245
1385
  const isDrop = name === "onDrop";
1246
1386
  const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
1387
+ const isPointerDown = name === "onPointerDown";
1388
+ const isPointerUp = name === "onPointerUp";
1389
+ const isWheel = name === "onWheel";
1390
+ const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
1391
+ if (isPointerMove && canDeferRaycasts) {
1392
+ events.compute?.(event, state);
1393
+ internal.pointerDirty.set(pointerId, event);
1394
+ return;
1395
+ }
1396
+ if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
1397
+ events.compute?.(event, state);
1398
+ internal.pointerDirty.set(pointerId, event);
1399
+ return;
1400
+ }
1401
+ if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
1402
+ const deferredEvent = internal.pointerDirty.get(pointerId);
1403
+ internal.pointerDirty.delete(pointerId);
1404
+ processDeferredPointer(deferredEvent, pointerId);
1405
+ }
1247
1406
  const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
1248
1407
  const hits = intersect(event, filter);
1249
- const delta = isClickEvent ? calculateDistance(event) : 0;
1250
- if (name === "onPointerDown") {
1251
- internal.initialClick = [event.offsetX, event.offsetY];
1252
- internal.initialHits = hits.map((hit) => hit.eventObject);
1253
- }
1408
+ const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
1409
+ if (isPointerDown) {
1410
+ const pointerState2 = getPointerState(internal, pointerId);
1411
+ pointerState2.initialClick = [event.offsetX, event.offsetY];
1412
+ pointerState2.initialHits = hits.map((hit) => hit.eventObject);
1413
+ }
1414
+ const pointerState = internal.pointerMap.get(pointerId);
1415
+ const initialHits = pointerState?.initialHits ?? [];
1254
1416
  if (isClickEvent && !hits.length) {
1255
1417
  if (delta <= 2) {
1256
1418
  pointerMissed(event, internal.interaction);
@@ -1265,7 +1427,9 @@ function createEvents(store) {
1265
1427
  dropMissed(event, internal.interaction);
1266
1428
  if (onDropMissed) onDropMissed(event);
1267
1429
  }
1268
- if (isPointerMove || isDragOver) cancelPointer(hits);
1430
+ if (isPointerMove || isDragOver) {
1431
+ cancelPointer(hits, pointerId);
1432
+ }
1269
1433
  function onIntersect(data) {
1270
1434
  const eventObject = data.eventObject;
1271
1435
  const instance = eventObject.__r3f;
@@ -1274,9 +1438,10 @@ function createEvents(store) {
1274
1438
  if (isPointerMove) {
1275
1439
  if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
1276
1440
  const id = makeId(data);
1277
- const hoveredItem = internal.hovered.get(id);
1441
+ const pointerState2 = getPointerState(internal, pointerId);
1442
+ const hoveredItem = pointerState2.hovered.get(id);
1278
1443
  if (!hoveredItem) {
1279
- internal.hovered.set(id, data);
1444
+ pointerState2.hovered.set(id, data);
1280
1445
  handlers.onPointerOver?.(data);
1281
1446
  handlers.onPointerEnter?.(data);
1282
1447
  } else if (hoveredItem.stopped) {
@@ -1286,9 +1451,10 @@ function createEvents(store) {
1286
1451
  handlers.onPointerMove?.(data);
1287
1452
  } else if (isDragOver) {
1288
1453
  const id = makeId(data);
1289
- const hoveredItem = internal.hovered.get(id);
1454
+ const pointerState2 = getPointerState(internal, pointerId);
1455
+ const hoveredItem = pointerState2.hovered.get(id);
1290
1456
  if (!hoveredItem) {
1291
- internal.hovered.set(id, data);
1457
+ pointerState2.hovered.set(id, data);
1292
1458
  handlers.onDragOverEnter?.(data);
1293
1459
  } else if (hoveredItem.stopped) {
1294
1460
  data.stopPropagation();
@@ -1299,18 +1465,18 @@ function createEvents(store) {
1299
1465
  } else {
1300
1466
  const handler = handlers[name];
1301
1467
  if (handler) {
1302
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1468
+ if (!isClickEvent || initialHits.includes(eventObject)) {
1303
1469
  pointerMissed(
1304
1470
  event,
1305
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1471
+ internal.interaction.filter((object) => !initialHits.includes(object))
1306
1472
  );
1307
1473
  handler(data);
1308
1474
  }
1309
1475
  } else {
1310
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1476
+ if (isClickEvent && initialHits.includes(eventObject)) {
1311
1477
  pointerMissed(
1312
1478
  event,
1313
- internal.interaction.filter((object) => !internal.initialHits.includes(object))
1479
+ internal.interaction.filter((object) => !initialHits.includes(object))
1314
1480
  );
1315
1481
  }
1316
1482
  }
@@ -1319,7 +1485,15 @@ function createEvents(store) {
1319
1485
  handleIntersects(hits, event, delta, onIntersect);
1320
1486
  };
1321
1487
  }
1322
- return { handlePointer };
1488
+ function flushDeferredPointers() {
1489
+ const { internal, events } = store.getState();
1490
+ if (!events.frameTimedRaycasts) return;
1491
+ for (const [pointerId, event] of internal.pointerDirty) {
1492
+ processDeferredPointer(event, pointerId);
1493
+ }
1494
+ internal.pointerDirty.clear();
1495
+ }
1496
+ return { handlePointer, flushDeferredPointers, processDeferredPointer };
1323
1497
  }
1324
1498
  const DOM_EVENTS = {
1325
1499
  onClick: ["click", false],
@@ -1338,10 +1512,15 @@ const DOM_EVENTS = {
1338
1512
  onLostPointerCapture: ["lostpointercapture", true]
1339
1513
  };
1340
1514
  function createPointerEvents(store) {
1341
- const { handlePointer } = createEvents(store);
1515
+ const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
1516
+ let nextXRPointerId = XR_POINTER_ID_START;
1517
+ const xrPointers = /* @__PURE__ */ new Map();
1342
1518
  return {
1343
1519
  priority: 1,
1344
1520
  enabled: true,
1521
+ frameTimedRaycasts: true,
1522
+ alwaysFireOnScroll: true,
1523
+ updateOnFrame: false,
1345
1524
  compute(event, state) {
1346
1525
  state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
1347
1526
  state.raycaster.setFromCamera(state.pointer, state.camera);
@@ -1351,11 +1530,33 @@ function createPointerEvents(store) {
1351
1530
  (acc, key) => ({ ...acc, [key]: handlePointer(key) }),
1352
1531
  {}
1353
1532
  ),
1354
- update: () => {
1533
+ update: (pointerId) => {
1534
+ const { events, internal } = store.getState();
1535
+ if (!events.handlers) return;
1536
+ if (pointerId !== void 0) {
1537
+ const event = internal.pointerDirty.get(pointerId);
1538
+ if (event) {
1539
+ internal.pointerDirty.delete(pointerId);
1540
+ processDeferredPointer(event, pointerId);
1541
+ } else if (internal.lastEvent?.current) {
1542
+ processDeferredPointer(internal.lastEvent.current, pointerId);
1543
+ }
1544
+ } else {
1545
+ flushDeferredPointers();
1546
+ if (internal.lastEvent?.current) {
1547
+ events.handlers.onPointerMove(internal.lastEvent.current);
1548
+ }
1549
+ }
1550
+ },
1551
+ flush: () => {
1355
1552
  const { events, internal } = store.getState();
1356
- if (internal.lastEvent?.current && events.handlers) events.handlers.onPointerMove(internal.lastEvent.current);
1553
+ flushDeferredPointers();
1554
+ if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
1555
+ events.handlers.onPointerMove(internal.lastEvent.current);
1556
+ }
1357
1557
  },
1358
1558
  connect: (target) => {
1559
+ if (!target) return;
1359
1560
  const { set, events } = store.getState();
1360
1561
  events.disconnect?.();
1361
1562
  set((state) => ({ events: { ...state.events, connected: target } }));
@@ -1379,6 +1580,33 @@ function createPointerEvents(store) {
1379
1580
  }
1380
1581
  set((state) => ({ events: { ...state.events, connected: void 0 } }));
1381
1582
  }
1583
+ },
1584
+ // PREVIEW (A8): the XR pointer registration API may change before stable.
1585
+ registerPointer: (config) => {
1586
+ const pointerId = nextXRPointerId++;
1587
+ xrPointers.set(pointerId, config);
1588
+ const { internal } = store.getState();
1589
+ getPointerState(internal, pointerId);
1590
+ return pointerId;
1591
+ },
1592
+ unregisterPointer: (pointerId) => {
1593
+ xrPointers.delete(pointerId);
1594
+ const { internal } = store.getState();
1595
+ const pointerState = internal.pointerMap.get(pointerId);
1596
+ if (pointerState) {
1597
+ for (const [, hoveredObj] of pointerState.hovered) {
1598
+ const eventObject = hoveredObj.eventObject;
1599
+ const instance = eventObject.__r3f;
1600
+ if (instance?.eventCount) {
1601
+ const handlers = instance.handlers;
1602
+ const data = { ...hoveredObj, intersections: [] };
1603
+ handlers.onPointerOut?.(data);
1604
+ handlers.onPointerLeave?.(data);
1605
+ }
1606
+ }
1607
+ internal.pointerMap.delete(pointerId);
1608
+ }
1609
+ internal.pointerDirty.delete(pointerId);
1382
1610
  }
1383
1611
  };
1384
1612
  }
@@ -1440,1058 +1668,33 @@ function notifyAlpha({ message, link }) {
1440
1668
  }
1441
1669
  }
1442
1670
 
1443
- var __defProp$2 = Object.defineProperty;
1444
- var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1445
- var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
1446
- const DEFAULT_PHASES = ["start", "input", "physics", "update", "render", "finish"];
1447
- class PhaseGraph {
1448
- constructor() {
1449
- /** Ordered list of phase nodes */
1450
- __publicField$2(this, "phases", []);
1451
- /** Quick lookup by name */
1452
- __publicField$2(this, "phaseMap", /* @__PURE__ */ new Map());
1453
- /** Cached ordered names (invalidated on changes) */
1454
- __publicField$2(this, "orderedNamesCache", null);
1455
- this.initializeDefaultPhases();
1456
- }
1457
- //* Initialization --------------------------------
1458
- initializeDefaultPhases() {
1459
- for (const name of DEFAULT_PHASES) {
1460
- const node = { name, isAutoGenerated: false };
1461
- this.phases.push(node);
1462
- this.phaseMap.set(name, node);
1463
- }
1464
- this.invalidateCache();
1465
- }
1466
- //* Public API --------------------------------
1467
- /**
1468
- * Add a named phase to the graph
1469
- * @param name - Phase name (must be unique)
1470
- * @param options - Position options (before or after another phase)
1471
- */
1472
- addPhase(name, options = {}) {
1473
- if (this.phaseMap.has(name)) {
1474
- console.warn(`[useFrame] Phase "${name}" already exists`);
1475
- return;
1476
- }
1477
- const { before, after } = options;
1478
- const node = { name, isAutoGenerated: false };
1479
- let insertIndex = this.phases.length;
1480
- const targetIndex = this.getPhaseIndex(before ?? after);
1481
- if (targetIndex !== -1) {
1482
- insertIndex = before ? targetIndex : targetIndex + 1;
1483
- } else {
1484
- const constraintType = before ? "before" : "after";
1485
- console.warn(`[useFrame] Phase "${before ?? after}" not found for '${constraintType}' constraint`);
1486
- }
1487
- this.phases.splice(insertIndex, 0, node);
1488
- this.phaseMap.set(name, node);
1489
- this.invalidateCache();
1490
- }
1491
- /**
1492
- * Get ordered list of phase names
1493
- */
1494
- getOrderedPhases() {
1495
- if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
1496
- return this.orderedNamesCache;
1497
- }
1498
- /**
1499
- * Check if a phase exists
1500
- */
1501
- hasPhase(name) {
1502
- return this.phaseMap.has(name);
1503
- }
1504
- /**
1505
- * Get the index of a phase (-1 if not found)
1506
- */
1507
- getPhaseIndex(name) {
1508
- if (!name) return -1;
1509
- return this.phases.findIndex((p) => p.name === name);
1510
- }
1511
- /**
1512
- * Ensure a phase exists, creating an auto-generated one if needed.
1513
- * Used for resolving before/after constraints.
1514
- *
1515
- * @param name - The phase name to ensure exists
1516
- * @returns The phase name (may be auto-generated like 'before:render')
1517
- */
1518
- ensurePhase(name) {
1519
- if (this.phaseMap.has(name)) return name;
1520
- const node = { name, isAutoGenerated: true };
1521
- this.phases.push(node);
1522
- this.phaseMap.set(name, node);
1523
- this.invalidateCache();
1524
- return name;
1525
- }
1526
- /**
1527
- * Resolve where a job with before/after constraints should go.
1528
- * Creates auto-generated phases if needed.
1529
- *
1530
- * @param before - Phase(s) to run before
1531
- * @param after - Phase(s) to run after
1532
- * @returns The resolved phase name
1533
- */
1534
- resolveConstraintPhase(before, after) {
1535
- const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
1536
- const afterArr = after ? Array.isArray(after) ? after : [after] : [];
1537
- if (beforeArr.length > 0) {
1538
- return this.ensureAutoPhase(beforeArr[0], "before", 0);
1539
- }
1540
- if (afterArr.length > 0) {
1541
- return this.ensureAutoPhase(afterArr[0], "after", 1);
1542
- }
1543
- return "update";
1544
- }
1545
- /**
1546
- * Ensure an auto-generated phase exists relative to a target phase.
1547
- * Creates the phase if it doesn't exist, inserting it at the correct position.
1548
- *
1549
- * @param target - The target phase name to position relative to
1550
- * @param prefix - Prefix for auto-generated phase name ('before' or 'after')
1551
- * @param offset - Insertion offset (0 for before, 1 for after)
1552
- * @returns The auto-generated phase name
1553
- */
1554
- ensureAutoPhase(target, prefix, offset) {
1555
- const autoName = `${prefix}:${target}`;
1556
- if (this.phaseMap.has(autoName)) return autoName;
1557
- const node = { name: autoName, isAutoGenerated: true };
1558
- const targetIndex = this.getPhaseIndex(target);
1559
- if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
1560
- else this.phases.push(node);
1561
- this.phaseMap.set(autoName, node);
1562
- this.invalidateCache();
1563
- return autoName;
1564
- }
1565
- // Internal --------------------------------
1566
- invalidateCache() {
1567
- this.orderedNamesCache = null;
1568
- }
1569
- }
1570
-
1571
- function rebuildSortedJobs(jobs, phaseGraph) {
1572
- const orderedPhases = phaseGraph.getOrderedPhases();
1573
- const buckets = /* @__PURE__ */ new Map();
1574
- for (const phase of orderedPhases) {
1575
- buckets.set(phase, []);
1576
- }
1577
- for (const job of jobs.values()) {
1578
- if (!job.enabled) continue;
1579
- let bucket = buckets.get(job.phase);
1580
- if (!bucket) {
1581
- bucket = [];
1582
- buckets.set(job.phase, bucket);
1583
- }
1584
- bucket.push(job);
1585
- }
1586
- const sortedBuckets = [];
1587
- for (const phase of orderedPhases) {
1588
- const bucket = buckets.get(phase);
1589
- if (!bucket || bucket.length === 0) continue;
1590
- bucket.sort((a, b) => {
1591
- if (a.priority !== b.priority) return b.priority - a.priority;
1592
- return a.index - b.index;
1593
- });
1594
- sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
1595
- }
1596
- for (const [phase, bucket] of buckets) {
1597
- if (!orderedPhases.includes(phase) && bucket.length > 0) {
1598
- bucket.sort((a, b) => {
1599
- if (a.priority !== b.priority) return b.priority - a.priority;
1600
- return a.index - b.index;
1601
- });
1602
- sortedBuckets.push(bucket);
1603
- }
1604
- }
1605
- return sortedBuckets.flat();
1606
- }
1607
- function hasCrossJobConstraints(bucket) {
1608
- const jobIds = new Set(bucket.map((j) => j.id));
1609
- for (const job of bucket) {
1610
- for (const ref of job.before) {
1611
- if (jobIds.has(ref)) return true;
1612
- }
1613
- for (const ref of job.after) {
1614
- if (jobIds.has(ref)) return true;
1615
- }
1616
- }
1617
- return false;
1618
- }
1619
- function topologicalSort(jobs) {
1620
- const n = jobs.length;
1621
- if (n <= 1) return jobs;
1622
- const jobMap = /* @__PURE__ */ new Map();
1623
- const inDegree = /* @__PURE__ */ new Map();
1624
- const adjacency = /* @__PURE__ */ new Map();
1625
- for (const job of jobs) {
1626
- jobMap.set(job.id, job);
1627
- inDegree.set(job.id, 0);
1628
- adjacency.set(job.id, []);
1629
- }
1630
- for (const job of jobs) {
1631
- for (const ref of job.before) {
1632
- if (jobMap.has(ref)) {
1633
- adjacency.get(job.id).push(ref);
1634
- inDegree.set(ref, inDegree.get(ref) + 1);
1635
- }
1636
- }
1637
- for (const ref of job.after) {
1638
- if (jobMap.has(ref)) {
1639
- adjacency.get(ref).push(job.id);
1640
- inDegree.set(job.id, inDegree.get(job.id) + 1);
1641
- }
1642
- }
1643
- }
1644
- const queue = [];
1645
- for (const job of jobs) {
1646
- if (inDegree.get(job.id) === 0) {
1647
- queue.push(job);
1648
- }
1649
- }
1650
- queue.sort((a, b) => {
1651
- if (a.priority !== b.priority) return b.priority - a.priority;
1652
- return a.index - b.index;
1653
- });
1654
- const result = [];
1655
- while (queue.length > 0) {
1656
- const job = queue.shift();
1657
- result.push(job);
1658
- const neighbors = adjacency.get(job.id) || [];
1659
- for (const neighborId of neighbors) {
1660
- const newDegree = inDegree.get(neighborId) - 1;
1661
- inDegree.set(neighborId, newDegree);
1662
- if (newDegree === 0) {
1663
- const neighbor = jobMap.get(neighborId);
1664
- insertSorted(queue, neighbor);
1665
- }
1666
- }
1667
- }
1668
- if (result.length !== n) {
1669
- console.warn("[useFrame] Circular dependency detected in job constraints");
1670
- const resultIds = new Set(result.map((j) => j.id));
1671
- for (const job of jobs) {
1672
- if (!resultIds.has(job.id)) result.push(job);
1673
- }
1674
- }
1675
- return result;
1676
- }
1677
- function insertSorted(arr, job) {
1678
- let i = 0;
1679
- while (i < arr.length) {
1680
- const cmp = arr[i];
1681
- if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
1682
- break;
1683
- }
1684
- i++;
1685
- }
1686
- arr.splice(i, 0, job);
1687
- }
1688
-
1689
- function shouldRun(job, now) {
1690
- if (!job.enabled) return false;
1691
- if (!job.fps) return true;
1692
- const minInterval = 1e3 / job.fps;
1693
- const lastRun = job.lastRun ?? 0;
1694
- const elapsed = now - lastRun;
1695
- if (elapsed < minInterval) return false;
1696
- if (job.drop) {
1697
- job.lastRun = now;
1698
- } else {
1699
- const steps = Math.floor(elapsed / minInterval);
1700
- job.lastRun = lastRun + steps * minInterval;
1701
- if (job.lastRun < now - minInterval) {
1702
- job.lastRun = now;
1703
- }
1704
- }
1705
- return true;
1706
- }
1707
- function resetJobTiming(job) {
1708
- job.lastRun = void 0;
1671
+ const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
1672
+ function isInternalRendererAccess(stack, callerDepth = 2) {
1673
+ if (!stack) return false;
1674
+ const caller = stack.split("\n")[callerDepth] ?? "";
1675
+ return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m));
1709
1676
  }
1710
1677
 
1711
- var __defProp$1 = Object.defineProperty;
1712
- var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1713
- var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1714
- const hmrData = (() => {
1715
- if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
1716
- if (typeof import_meta_hot !== "undefined") return import_meta_hot;
1717
- try {
1718
- return (0, eval)("import.meta.hot");
1719
- } catch {
1720
- return void 0;
1721
- }
1722
- })();
1723
- const _Scheduler = class _Scheduler {
1724
- //* Constructor ================================
1725
- constructor() {
1726
- //* Critical State ================================
1727
- __publicField$1(this, "roots", /* @__PURE__ */ new Map());
1728
- __publicField$1(this, "phaseGraph");
1729
- __publicField$1(this, "loopState", {
1730
- running: false,
1731
- rafHandle: null,
1732
- lastTime: null,
1733
- // null = uninitialized, 0+ = valid timestamp
1734
- frameCount: 0,
1735
- elapsedTime: 0,
1736
- createdAt: performance.now()
1737
- });
1738
- __publicField$1(this, "stoppedTime", 0);
1739
- //* Private State ================================
1740
- __publicField$1(this, "nextRootIndex", 0);
1741
- __publicField$1(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
1742
- __publicField$1(this, "globalAfterJobs", /* @__PURE__ */ new Map());
1743
- __publicField$1(this, "nextGlobalIndex", 0);
1744
- __publicField$1(this, "idleCallbacks", /* @__PURE__ */ new Set());
1745
- __publicField$1(this, "nextJobIndex", 0);
1746
- __publicField$1(this, "jobStateListeners", /* @__PURE__ */ new Map());
1747
- __publicField$1(this, "pendingFrames", 0);
1748
- __publicField$1(this, "_frameloop", "always");
1749
- //* Independent Mode & Error Handling State ================================
1750
- __publicField$1(this, "_independent", false);
1751
- __publicField$1(this, "errorHandler", null);
1752
- __publicField$1(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1753
- //* Core Loop Execution Methods ================================
1754
- /**
1755
- * Main RAF loop callback.
1756
- * Executes frame, handles demand mode, and schedules next frame.
1757
- * @param {number} timestamp - RAF timestamp in milliseconds
1758
- * @returns {void}
1759
- * @private
1760
- */
1761
- __publicField$1(this, "loop", (timestamp) => {
1762
- if (!this.loopState.running) return;
1763
- this.executeFrame(timestamp);
1764
- if (this._frameloop === "demand") {
1765
- this.pendingFrames = Math.max(0, this.pendingFrames - 1);
1766
- if (this.pendingFrames === 0) {
1767
- this.notifyIdle(timestamp);
1768
- return this.stop();
1769
- }
1770
- }
1771
- this.loopState.rafHandle = requestAnimationFrame(this.loop);
1772
- });
1773
- this.phaseGraph = new PhaseGraph();
1774
- }
1775
- static get instance() {
1776
- return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1777
- }
1778
- static set instance(value) {
1779
- globalThis[_Scheduler.INSTANCE_KEY] = value;
1780
- }
1781
- /**
1782
- * Get the global scheduler instance (creates if doesn't exist).
1783
- * Uses HMR data to preserve instance across hot reloads.
1784
- * @returns {Scheduler} The singleton scheduler instance
1785
- */
1786
- static get() {
1787
- if (!_Scheduler.instance && hmrData?.data?.scheduler) {
1788
- _Scheduler.instance = hmrData.data.scheduler;
1789
- }
1790
- if (!_Scheduler.instance) {
1791
- _Scheduler.instance = new _Scheduler();
1792
- if (hmrData?.data) {
1793
- hmrData.data.scheduler = _Scheduler.instance;
1794
- }
1795
- }
1796
- return _Scheduler.instance;
1797
- }
1798
- /**
1799
- * Reset the singleton instance. Stops the loop and clears all state.
1800
- * Primarily used for testing to ensure clean state between tests.
1801
- * @returns {void}
1802
- */
1803
- static reset() {
1804
- if (_Scheduler.instance) {
1805
- _Scheduler.instance.stop();
1806
- _Scheduler.instance = null;
1807
- }
1808
- if (hmrData?.data) {
1809
- hmrData.data.scheduler = null;
1810
- }
1811
- }
1812
- //* Getters & Setters ================================
1813
- get phases() {
1814
- return this.phaseGraph.getOrderedPhases();
1815
- }
1816
- get frameloop() {
1817
- return this._frameloop;
1818
- }
1819
- set frameloop(mode) {
1820
- if (this._frameloop === mode) return;
1821
- const wasAlways = this._frameloop === "always";
1822
- this._frameloop = mode;
1823
- if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
1824
- else if (mode !== "always" && wasAlways) this.stop();
1825
- }
1826
- get isRunning() {
1827
- return this.loopState.running;
1828
- }
1829
- get isReady() {
1830
- return this.roots.size > 0;
1831
- }
1832
- get independent() {
1833
- return this._independent;
1834
- }
1835
- set independent(value) {
1836
- this._independent = value;
1837
- if (value) this.ensureDefaultRoot();
1838
- }
1839
- //* Root Management Methods ================================
1840
- /**
1841
- * Register a root (Canvas) with the scheduler.
1842
- * The first root to register starts the RAF loop (if frameloop='always').
1843
- * @param {string} id - Unique identifier for this root
1844
- * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
1845
- * @returns {() => void} Unsubscribe function to remove this root
1846
- */
1847
- registerRoot(id, options = {}) {
1848
- if (this.roots.has(id)) {
1849
- console.warn(`[Scheduler] Root "${id}" already registered`);
1850
- return () => this.unregisterRoot(id);
1851
- }
1852
- const entry = {
1853
- id,
1854
- getState: options.getState ?? (() => ({})),
1855
- jobs: /* @__PURE__ */ new Map(),
1856
- sortedJobs: [],
1857
- needsRebuild: false
1858
- };
1859
- if (options.onError) {
1860
- this.errorHandler = options.onError;
1861
- }
1862
- this.roots.set(id, entry);
1863
- if (this.roots.size === 1) {
1864
- this.notifyRootReady();
1865
- if (this._frameloop === "always") this.start();
1866
- }
1867
- return () => this.unregisterRoot(id);
1868
- }
1869
- /**
1870
- * Unregister a root from the scheduler.
1871
- * Cleans up all job state listeners for this root's jobs.
1872
- * The last root to unregister stops the RAF loop.
1873
- * @param {string} id - The root ID to unregister
1874
- * @returns {void}
1875
- */
1876
- unregisterRoot(id) {
1877
- const root = this.roots.get(id);
1878
- if (!root) return;
1879
- for (const jobId of root.jobs.keys()) {
1880
- this.jobStateListeners.delete(jobId);
1881
- }
1882
- this.roots.delete(id);
1883
- if (this.roots.size === 0) {
1884
- this.stop();
1885
- this.errorHandler = null;
1886
- }
1887
- }
1888
- /**
1889
- * Subscribe to be notified when a root becomes available.
1890
- * Fires immediately if a root already exists.
1891
- * @param {() => void} callback - Function called when first root registers
1892
- * @returns {() => void} Unsubscribe function
1893
- */
1894
- onRootReady(callback) {
1895
- if (this.roots.size > 0) {
1896
- callback();
1897
- return () => {
1898
- };
1899
- }
1900
- this.rootReadyCallbacks.add(callback);
1901
- return () => this.rootReadyCallbacks.delete(callback);
1902
- }
1903
- /**
1904
- * Notify all registered root-ready callbacks.
1905
- * Called when the first root registers.
1906
- * @returns {void}
1907
- * @private
1908
- */
1909
- notifyRootReady() {
1910
- for (const cb of this.rootReadyCallbacks) {
1911
- try {
1912
- cb();
1913
- } catch (error) {
1914
- console.error("[Scheduler] Error in root-ready callback:", error);
1915
- }
1916
- }
1917
- this.rootReadyCallbacks.clear();
1918
- }
1919
- /**
1920
- * Ensure a default root exists for independent mode.
1921
- * Creates a minimal root with no state provider.
1922
- * @returns {void}
1923
- * @private
1924
- */
1925
- ensureDefaultRoot() {
1926
- if (!this.roots.has("__default__")) {
1927
- this.registerRoot("__default__");
1928
- }
1929
- }
1930
- /**
1931
- * Trigger error handling for job errors.
1932
- * Uses the bound error handler if available, otherwise logs to console.
1933
- * @param {Error} error - The error to handle
1934
- * @returns {void}
1935
- */
1936
- triggerError(error) {
1937
- if (this.errorHandler) this.errorHandler(error);
1938
- else console.error("[Scheduler]", error);
1939
- }
1940
- //* Phase Management Methods ================================
1941
- /**
1942
- * Add a named phase to the scheduler's execution order.
1943
- * Marks all roots for rebuild to incorporate the new phase.
1944
- * @param {string} name - The phase name (e.g., 'physics', 'postprocess')
1945
- * @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
1946
- * @returns {void}
1947
- * @example
1948
- * scheduler.addPhase('physics', { before: 'update' });
1949
- * scheduler.addPhase('postprocess', { after: 'render' });
1950
- */
1951
- addPhase(name, options) {
1952
- this.phaseGraph.addPhase(name, options);
1953
- for (const root of this.roots.values()) {
1954
- root.needsRebuild = true;
1955
- }
1956
- }
1957
- /**
1958
- * Check if a phase exists in the scheduler.
1959
- * @param {string} name - The phase name to check
1960
- * @returns {boolean} True if the phase exists
1961
- */
1962
- hasPhase(name) {
1963
- return this.phaseGraph.hasPhase(name);
1964
- }
1965
- //* Global Job Registration Methods (Deprecated APIs) ================================
1966
- /**
1967
- * Register a global job that runs once per frame (not per-root).
1968
- * Used internally by deprecated addEffect/addAfterEffect APIs.
1969
- * @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
1970
- * @param {string} id - Unique identifier for this global job
1971
- * @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
1972
- * @returns {() => void} Unsubscribe function to remove this global job
1973
- * @deprecated Use useFrame with phases instead
1974
- */
1975
- registerGlobal(phase, id, callback) {
1976
- const job = { id, callback };
1977
- if (phase === "before") {
1978
- this.globalBeforeJobs.set(id, job);
1979
- } else {
1980
- this.globalAfterJobs.set(id, job);
1981
- }
1982
- return () => {
1983
- if (phase === "before") this.globalBeforeJobs.delete(id);
1984
- else this.globalAfterJobs.delete(id);
1985
- };
1986
- }
1987
- //* Idle Callback Methods (Deprecated API) ================================
1988
- /**
1989
- * Register an idle callback that fires when the loop stops.
1990
- * Used internally by deprecated addTail API.
1991
- * @param {(timestamp: number) => void} callback - Function called when loop becomes idle
1992
- * @returns {() => void} Unsubscribe function to remove this idle callback
1993
- * @deprecated Use demand mode with invalidate() instead
1994
- */
1995
- onIdle(callback) {
1996
- this.idleCallbacks.add(callback);
1997
- return () => this.idleCallbacks.delete(callback);
1998
- }
1999
- /**
2000
- * Notify all registered idle callbacks.
2001
- * Called when the loop stops in demand mode.
2002
- * @param {number} timestamp - The RAF timestamp when idle occurred
2003
- * @returns {void}
2004
- * @private
2005
- */
2006
- notifyIdle(timestamp) {
2007
- for (const cb of this.idleCallbacks) {
2008
- try {
2009
- cb(timestamp);
2010
- } catch (error) {
2011
- console.error("[Scheduler] Error in idle callback:", error);
2012
- }
2013
- }
2014
- }
2015
- //* Job Registration & Management Methods ================================
2016
- /**
2017
- * Register a job (frame callback) with a specific root.
2018
- * This is the core registration method used by useFrame internally.
2019
- * @param {FrameNextCallback} callback - The function to call each frame
2020
- * @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
2021
- * @param {string} [options.rootId] - Target root ID (defaults to first registered root)
2022
- * @param {string} [options.id] - Unique job ID (auto-generated if not provided)
2023
- * @param {string} [options.phase] - Execution phase (defaults to 'update')
2024
- * @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
2025
- * @param {number} [options.fps] - FPS throttle limit
2026
- * @param {boolean} [options.drop] - Drop frames when behind (default true)
2027
- * @param {boolean} [options.enabled] - Whether job is active (default true)
2028
- * @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
2029
- * @returns {() => void} Unsubscribe function to remove this job
2030
- */
2031
- register(callback, options = {}) {
2032
- const rootId = options.rootId;
2033
- const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
2034
- if (!root) {
2035
- console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
2036
- return () => {
2037
- };
2038
- }
2039
- const id = options.id ?? this.generateJobId();
2040
- let phase = options.phase ?? "update";
2041
- if (!options.phase && (options.before || options.after)) {
2042
- phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
2043
- }
2044
- const before = this.normalizeConstraints(options.before);
2045
- const after = this.normalizeConstraints(options.after);
2046
- const job = {
2047
- id,
2048
- callback,
2049
- phase,
2050
- before,
2051
- after,
2052
- priority: options.priority ?? 0,
2053
- index: this.nextJobIndex++,
2054
- fps: options.fps,
2055
- drop: options.drop ?? true,
2056
- enabled: options.enabled ?? true,
2057
- system: options.system ?? false
2058
- };
2059
- if (root.jobs.has(id)) {
2060
- console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
2061
- }
2062
- root.jobs.set(id, job);
2063
- root.needsRebuild = true;
2064
- return () => this.unregister(id, root.id);
2065
- }
2066
- /**
2067
- * Unregister a job by its ID.
2068
- * Searches all roots if rootId is not provided.
2069
- * @param {string} id - The job ID to unregister
2070
- * @param {string} [rootId] - Optional root ID to search (searches all if not provided)
2071
- * @returns {void}
2072
- */
2073
- unregister(id, rootId) {
2074
- const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
2075
- if (root?.jobs.delete(id)) {
2076
- root.needsRebuild = true;
2077
- this.jobStateListeners.delete(id);
2078
- }
2079
- }
2080
- /**
2081
- * Update a job's options dynamically.
2082
- * Searches all roots to find the job by ID.
2083
- * Phase/constraint changes trigger a rebuild of the sorted job list.
2084
- * @param {string} id - The job ID to update
2085
- * @param {Partial<JobOptions>} options - The options to update
2086
- * @returns {void}
2087
- */
2088
- updateJob(id, options) {
2089
- let job;
2090
- let root;
2091
- for (const r of this.roots.values()) {
2092
- job = r.jobs.get(id);
2093
- if (job) {
2094
- root = r;
2095
- break;
2096
- }
2097
- }
2098
- if (!job || !root) return;
2099
- if (options.priority !== void 0) job.priority = options.priority;
2100
- if (options.fps !== void 0) job.fps = options.fps;
2101
- if (options.drop !== void 0) job.drop = options.drop;
2102
- if (options.enabled !== void 0) {
2103
- const wasEnabled = job.enabled;
2104
- job.enabled = options.enabled;
2105
- if (!wasEnabled && job.enabled) resetJobTiming(job);
2106
- if (wasEnabled !== job.enabled) root.needsRebuild = true;
2107
- }
2108
- if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
2109
- if (options.phase) job.phase = options.phase;
2110
- if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
2111
- if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
2112
- root.needsRebuild = true;
2113
- }
2114
- }
2115
- //* Job State Management Methods ================================
2116
- /**
2117
- * Check if a job is currently paused (disabled).
2118
- * @param {string} id - The job ID to check
2119
- * @returns {boolean} True if the job exists and is paused
2120
- */
2121
- isJobPaused(id) {
2122
- for (const root of this.roots.values()) {
2123
- const job = root.jobs.get(id);
2124
- if (job) return !job.enabled;
2125
- }
2126
- return false;
2127
- }
2128
- /**
2129
- * Subscribe to state changes for a specific job.
2130
- * Listener is called when job is paused or resumed.
2131
- * @param {string} id - The job ID to subscribe to
2132
- * @param {() => void} listener - Callback invoked on state changes
2133
- * @returns {() => void} Unsubscribe function
2134
- */
2135
- subscribeJobState(id, listener) {
2136
- if (!this.jobStateListeners.has(id)) {
2137
- this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
2138
- }
2139
- this.jobStateListeners.get(id).add(listener);
2140
- return () => {
2141
- this.jobStateListeners.get(id)?.delete(listener);
2142
- if (this.jobStateListeners.get(id)?.size === 0) {
2143
- this.jobStateListeners.delete(id);
2144
- }
2145
- };
2146
- }
2147
- /**
2148
- * Notify all listeners that a job's state has changed.
2149
- * @param {string} id - The job ID that changed
2150
- * @returns {void}
2151
- * @private
2152
- */
2153
- notifyJobStateChange(id) {
2154
- this.jobStateListeners.get(id)?.forEach((listener) => listener());
2155
- }
2156
- /**
2157
- * Pause a job by ID (sets enabled=false).
2158
- * Notifies any subscribed state listeners.
2159
- * @param {string} id - The job ID to pause
2160
- * @returns {void}
2161
- */
2162
- pauseJob(id) {
2163
- this.updateJob(id, { enabled: false });
2164
- this.notifyJobStateChange(id);
2165
- }
2166
- /**
2167
- * Resume a paused job by ID (sets enabled=true).
2168
- * Resets job timing to prevent frame accumulation.
2169
- * Notifies any subscribed state listeners.
2170
- * @param {string} id - The job ID to resume
2171
- * @returns {void}
2172
- */
2173
- resumeJob(id) {
2174
- this.updateJob(id, { enabled: true });
2175
- this.notifyJobStateChange(id);
2176
- }
2177
- //* Frame Loop Control Methods ================================
2178
- /**
2179
- * Start the requestAnimationFrame loop.
2180
- * Resets timing state (elapsedTime, frameCount) on start.
2181
- * No-op if already running.
2182
- * @returns {void}
2183
- */
2184
- start() {
2185
- if (this.loopState.running) return;
2186
- const { elapsedTime, createdAt } = this.loopState;
2187
- let adjustedCreated = 0;
2188
- if (this.stoppedTime > 0) {
2189
- adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
2190
- this.stoppedTime = 0;
2191
- }
2192
- Object.assign(this.loopState, {
2193
- running: true,
2194
- elapsedTime: elapsedTime ?? 0,
2195
- lastTime: performance.now(),
2196
- createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
2197
- frameCount: 0,
2198
- rafHandle: requestAnimationFrame(this.loop)
2199
- });
2200
- }
2201
- /**
2202
- * Stop the requestAnimationFrame loop.
2203
- * Cancels any pending RAF callback.
2204
- * No-op if not running.
2205
- * @returns {void}
2206
- */
2207
- stop() {
2208
- if (!this.loopState.running) return;
2209
- this.loopState.running = false;
2210
- if (this.loopState.rafHandle !== null) {
2211
- cancelAnimationFrame(this.loopState.rafHandle);
2212
- this.loopState.rafHandle = null;
2213
- }
2214
- this.stoppedTime = performance.now();
2215
- }
2216
- /**
2217
- * Request frames to be rendered in demand mode.
2218
- * Accumulates pending frames (capped at 60) and starts the loop if not running.
2219
- * No-op if frameloop is not 'demand'.
2220
- * @param {number} [frames=1] - Number of frames to request
2221
- * @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
2222
- * - `false` (default): Sets pending frames to the specified value (replaces existing count)
2223
- * - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
2224
- * @returns {void}
2225
- * @example
2226
- * // Request a single frame render
2227
- * scheduler.invalidate();
2228
- *
2229
- * @example
2230
- * // Request 5 frames (e.g., for animations)
2231
- * scheduler.invalidate(5);
2232
- *
2233
- * @example
2234
- * // Set pending frames to exactly 3 (don't stack with existing)
2235
- * scheduler.invalidate(3, false);
2236
- *
2237
- * @example
2238
- * // Add 2 more frames to existing pending count
2239
- * scheduler.invalidate(2, true);
2240
- */
2241
- invalidate(frames = 1, stackFrames = false) {
2242
- if (this._frameloop !== "demand") return;
2243
- const baseFrames = stackFrames ? this.pendingFrames : 0;
2244
- this.pendingFrames = Math.min(60, baseFrames + frames);
2245
- if (!this.loopState.running && this.pendingFrames > 0) this.start();
2246
- }
2247
- /**
2248
- * Reset timing state for deterministic testing.
2249
- * Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
2250
- * @returns {void}
2251
- */
2252
- resetTiming() {
2253
- this.loopState.lastTime = null;
2254
- this.loopState.frameCount = 0;
2255
- this.loopState.elapsedTime = 0;
2256
- this.loopState.createdAt = performance.now();
2257
- }
2258
- //* Manual Stepping Methods ================================
2259
- /**
2260
- * Manually execute a single frame for all roots.
2261
- * Useful for frameloop='never' mode or testing scenarios.
2262
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2263
- * @returns {void}
2264
- * @example
2265
- * // Manual control mode
2266
- * scheduler.frameloop = 'never';
2267
- * scheduler.step(); // Execute one frame
2268
- */
2269
- step(timestamp) {
2270
- const now = timestamp ?? performance.now();
2271
- this.executeFrame(now);
2272
- }
2273
- /**
2274
- * Manually execute a single job by its ID.
2275
- * Useful for testing individual job callbacks in isolation.
2276
- * @param {string} id - The job ID to step
2277
- * @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
2278
- * @returns {void}
2279
- */
2280
- stepJob(id, timestamp) {
2281
- let job;
2282
- let root;
2283
- for (const r of this.roots.values()) {
2284
- job = r.jobs.get(id);
2285
- if (job) {
2286
- root = r;
2287
- break;
2288
- }
2289
- }
2290
- if (!job || !root) {
2291
- console.warn(`[Scheduler] Job "${id}" not found`);
2292
- return;
2293
- }
2294
- const now = timestamp ?? performance.now();
2295
- const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
2296
- const delta = deltaMs / 1e3;
2297
- const elapsed = now - this.loopState.createdAt;
2298
- const providedState = root.getState?.() ?? {};
2299
- const frameState = {
2300
- ...providedState,
2301
- time: now,
2302
- delta,
2303
- elapsed,
2304
- frame: this.loopState.frameCount
2305
- };
2306
- try {
2307
- job.callback(frameState, delta);
2308
- } catch (error) {
2309
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2310
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2311
- }
2312
- }
2313
- /**
2314
- * Execute a single frame across all roots.
2315
- * Order: globalBefore → each root's jobs → globalAfter
2316
- * @param {number} timestamp - RAF timestamp in milliseconds
2317
- * @returns {void}
2318
- * @private
2319
- */
2320
- executeFrame(timestamp) {
2321
- const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
2322
- const delta = deltaMs / 1e3;
2323
- this.loopState.lastTime = timestamp;
2324
- this.loopState.frameCount++;
2325
- this.loopState.elapsedTime += deltaMs;
2326
- this.runGlobalJobs(this.globalBeforeJobs, timestamp);
2327
- for (const root of this.roots.values()) {
2328
- this.tickRoot(root, timestamp, delta);
2329
- }
2330
- this.runGlobalJobs(this.globalAfterJobs, timestamp);
2331
- }
2332
- /**
2333
- * Run all global jobs from a job map.
2334
- * Catches and logs errors without stopping execution.
2335
- * @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
2336
- * @param {number} timestamp - RAF timestamp in milliseconds
2337
- * @returns {void}
2338
- * @private
2339
- */
2340
- runGlobalJobs(jobs, timestamp) {
2341
- for (const job of jobs.values()) {
2342
- try {
2343
- job.callback(timestamp);
2344
- } catch (error) {
2345
- console.error(`[Scheduler] Error in global job "${job.id}":`, error);
2346
- }
2347
- }
2348
- }
2349
- /**
2350
- * Execute all jobs for a single root in sorted order.
2351
- * Rebuilds sorted job list if needed, then dispatches each job.
2352
- * Errors are caught and propagated via triggerError.
2353
- * @param {RootEntry} root - The root entry to tick
2354
- * @param {number} timestamp - RAF timestamp in milliseconds
2355
- * @param {number} delta - Time since last frame in seconds
2356
- * @returns {void}
2357
- * @private
2358
- */
2359
- tickRoot(root, timestamp, delta) {
2360
- if (root.needsRebuild) {
2361
- root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
2362
- root.needsRebuild = false;
2363
- }
2364
- const providedState = root.getState?.() ?? {};
2365
- const frameState = {
2366
- ...providedState,
2367
- time: timestamp,
2368
- delta,
2369
- elapsed: this.loopState.elapsedTime / 1e3,
2370
- // Convert ms to seconds
2371
- frame: this.loopState.frameCount
2372
- };
2373
- for (const job of root.sortedJobs) {
2374
- if (!shouldRun(job, timestamp)) continue;
2375
- try {
2376
- job.callback(frameState, delta);
2377
- } catch (error) {
2378
- console.error(`[Scheduler] Error in job "${job.id}":`, error);
2379
- this.triggerError(error instanceof Error ? error : new Error(String(error)));
2380
- }
2381
- }
2382
- }
2383
- //* Debug & Inspection Methods ================================
2384
- /**
2385
- * Get the total number of registered jobs across all roots.
2386
- * Includes both per-root jobs and global before/after jobs.
2387
- * @returns {number} Total job count
2388
- */
2389
- getJobCount() {
2390
- let count = 0;
2391
- for (const root of this.roots.values()) {
2392
- count += root.jobs.size;
2393
- }
2394
- return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
2395
- }
2396
- /**
2397
- * Get all registered job IDs across all roots.
2398
- * Includes both per-root jobs and global before/after jobs.
2399
- * @returns {string[]} Array of all job IDs
2400
- */
2401
- getJobIds() {
2402
- const ids = [];
2403
- for (const root of this.roots.values()) {
2404
- ids.push(...root.jobs.keys());
2405
- }
2406
- ids.push(...this.globalBeforeJobs.keys());
2407
- ids.push(...this.globalAfterJobs.keys());
2408
- return ids;
2409
- }
2410
- /**
2411
- * Get the number of registered roots (Canvas instances).
2412
- * @returns {number} Number of registered roots
2413
- */
2414
- getRootCount() {
2415
- return this.roots.size;
2416
- }
2417
- /**
2418
- * Check if any user (non-system) jobs are registered in a specific phase.
2419
- * Used by the default render job to know if a user has taken over rendering.
2420
- *
2421
- * @param phase The phase to check
2422
- * @param rootId Optional root ID to check (checks all roots if not provided)
2423
- * @returns true if any user jobs exist in the phase
2424
- */
2425
- hasUserJobsInPhase(phase, rootId) {
2426
- const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
2427
- return rootsToCheck.some((root) => {
2428
- if (!root) return false;
2429
- for (const job of root.jobs.values()) {
2430
- if (job.phase === phase && !job.system && job.enabled) return true;
2431
- }
2432
- return false;
2433
- });
2434
- }
2435
- //* Utility Methods ================================
2436
- /**
2437
- * Generate a unique root ID for automatic root registration.
2438
- * @returns {string} A unique root ID in the format 'root_N'
2439
- */
2440
- generateRootId() {
2441
- return `root_${this.nextRootIndex++}`;
2442
- }
2443
- /**
2444
- * Generate a unique job ID.
2445
- * @returns {string} A unique job ID in the format 'job_N'
2446
- * @private
2447
- */
2448
- generateJobId() {
2449
- return `job_${this.nextJobIndex}`;
2450
- }
2451
- /**
2452
- * Normalize before/after constraints to a Set.
2453
- * Handles undefined, single string, or array inputs.
2454
- * @param {string | string[] | undefined} value - The constraint value(s)
2455
- * @returns {Set<string>} Normalized Set of constraint strings
2456
- * @private
2457
- */
2458
- normalizeConstraints(value) {
2459
- if (!value) return /* @__PURE__ */ new Set();
2460
- if (Array.isArray(value)) return new Set(value);
2461
- return /* @__PURE__ */ new Set([value]);
2462
- }
2463
- };
2464
- //* Static State & Methods (Singleton Usage) ================================
2465
- //* Cross-Bundle Singleton Key ==============================
2466
- // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2467
- // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2468
- __publicField$1(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
2469
- let Scheduler = _Scheduler;
2470
- const getScheduler = () => Scheduler.get();
2471
- if (hmrData) {
2472
- hmrData.accept?.();
2473
- }
2474
-
2475
- const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
2476
- const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
2477
- const createStore = (invalidate, advance) => {
2478
- const rootStore = createWithEqualityFn((set, get) => {
2479
- const position = new Vector3();
2480
- const defaultTarget = new Vector3();
2481
- const tempTarget = new Vector3();
2482
- function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
2483
- const { width, height, top, left } = size;
2484
- const aspect = width / height;
2485
- if (target.isVector3) tempTarget.copy(target);
2486
- else tempTarget.set(...target);
2487
- const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
2488
- if (isOrthographicCamera(camera)) {
2489
- return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
2490
- } else {
2491
- const fov = camera.fov * Math.PI / 180;
2492
- const h = 2 * Math.tan(fov / 2) * distance;
2493
- const w = h * (width / height);
2494
- return { width: w, height: h, top, left, factor: width / w, distance, aspect };
1678
+ const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
1679
+ const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
1680
+ const createStore = (invalidate, advance) => {
1681
+ const rootStore = createWithEqualityFn((set, get) => {
1682
+ const position = new Vector3();
1683
+ const defaultTarget = new Vector3();
1684
+ const tempTarget = new Vector3();
1685
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
1686
+ const { width, height, top, left } = size;
1687
+ const aspect = width / height;
1688
+ if (target.isVector3) tempTarget.copy(target);
1689
+ else tempTarget.set(...target);
1690
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1691
+ if (isOrthographicCamera(camera)) {
1692
+ return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
1693
+ } else {
1694
+ const fov = camera.fov * Math.PI / 180;
1695
+ const h = 2 * Math.tan(fov / 2) * distance;
1696
+ const w = h * (width / height);
1697
+ return { width: w, height: h, top, left, factor: width / w, distance, aspect };
2495
1698
  }
2496
1699
  }
2497
1700
  let performanceTimeout = void 0;
@@ -2509,7 +1712,14 @@ const createStore = (invalidate, advance) => {
2509
1712
  frustum: new Frustum(),
2510
1713
  autoUpdateFrustum: true,
2511
1714
  raycaster: null,
2512
- events: { priority: 1, enabled: true, connected: false },
1715
+ events: {
1716
+ priority: 1,
1717
+ enabled: true,
1718
+ connected: false,
1719
+ frameTimedRaycasts: true,
1720
+ alwaysFireOnScroll: true,
1721
+ updateOnFrame: false
1722
+ },
2513
1723
  scene: null,
2514
1724
  rootScene: null,
2515
1725
  xr: null,
@@ -2597,14 +1807,18 @@ const createStore = (invalidate, advance) => {
2597
1807
  }),
2598
1808
  setFrameloop: (frameloop = "always") => {
2599
1809
  set(() => ({ frameloop }));
1810
+ getScheduler().frameloop = frameloop;
2600
1811
  },
2601
1812
  setError: (error) => set(() => ({ error })),
2602
1813
  error: null,
2603
- //* TSL State (managed via hooks: useUniforms, useNodes, useTextures, usePostProcessing) ==============================
1814
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
2604
1815
  uniforms: {},
2605
1816
  nodes: {},
1817
+ buffers: {},
1818
+ gpuStorage: {},
2606
1819
  textures: /* @__PURE__ */ new Map(),
2607
- postProcessing: null,
1820
+ _textureRefs: /* @__PURE__ */ new Map(),
1821
+ renderPipeline: null,
2608
1822
  passes: {},
2609
1823
  _hmrVersion: 0,
2610
1824
  _sizeImperative: false,
@@ -2613,12 +1827,16 @@ const createStore = (invalidate, advance) => {
2613
1827
  internal: {
2614
1828
  // Events
2615
1829
  interaction: [],
2616
- hovered: /* @__PURE__ */ new Map(),
2617
1830
  subscribers: [],
1831
+ // Per-pointer state (new unified structure)
1832
+ pointerMap: /* @__PURE__ */ new Map(),
1833
+ pointerDirty: /* @__PURE__ */ new Map(),
1834
+ lastEvent: React.createRef(),
1835
+ // Deprecated but kept for backwards compatibility
1836
+ hovered: /* @__PURE__ */ new Map(),
2618
1837
  initialClick: [0, 0],
2619
1838
  initialHits: [],
2620
1839
  capturedMap: /* @__PURE__ */ new Map(),
2621
- lastEvent: React.createRef(),
2622
1840
  // Visibility tracking (onFramed, onOccluded, onVisible)
2623
1841
  visibilityRegistry: /* @__PURE__ */ new Map(),
2624
1842
  // Occlusion system (WebGPU only)
@@ -2657,8 +1875,7 @@ const createStore = (invalidate, advance) => {
2657
1875
  const currentState = rootStore.getState();
2658
1876
  if (!currentState.isLegacy && currentState.internal.actualRenderer) {
2659
1877
  const stack = new Error().stack || "";
2660
- const isInternalAccess = stack.includes("zustand") || stack.includes("setState") || stack.includes("Object.assign") || stack.includes("react-three-fiber/packages/fiber/src/core");
2661
- if (!isInternalAccess) {
1878
+ if (!isInternalRendererAccess(stack)) {
2662
1879
  const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
2663
1880
  notifyDepreciated({
2664
1881
  heading: "Accessing state.gl in WebGPU mode",
@@ -2706,14 +1923,16 @@ const createStore = (invalidate, advance) => {
2706
1923
  oldSize = size;
2707
1924
  oldDpr = viewport.dpr;
2708
1925
  updateCamera(camera, size);
2709
- if (canvasTarget) {
1926
+ if (internal.isSecondary && canvasTarget) {
2710
1927
  if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
2711
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && canvasTarget.domElement instanceof HTMLCanvasElement;
2712
- canvasTarget.setSize(size.width, size.height, updateStyle);
1928
+ canvasTarget.setSize(size.width, size.height, false);
2713
1929
  } else {
2714
1930
  if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
2715
- const updateStyle = typeof HTMLCanvasElement !== "undefined" && actualRenderer.domElement instanceof HTMLCanvasElement;
2716
- actualRenderer.setSize(size.width, size.height, updateStyle);
1931
+ actualRenderer.setSize(size.width, size.height, false);
1932
+ if (canvasTarget) {
1933
+ if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1934
+ canvasTarget.setSize(size.width, size.height, false);
1935
+ }
2717
1936
  }
2718
1937
  }
2719
1938
  if (camera !== oldCamera) {
@@ -2769,19 +1988,25 @@ function loadingFn(extensions, onProgress) {
2769
1988
  );
2770
1989
  };
2771
1990
  }
2772
- function useLoader(loader, input, extensions, onProgress) {
2773
- const keys = Array.isArray(input) ? input : [input];
1991
+ function useLoader(loader, input, extensions, onProgress, cacheKey) {
1992
+ const inputs = Array.isArray(input) ? input : [input];
1993
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2774
1994
  const fn = loadingFn(extensions, onProgress);
2775
- const results = keys.map((key) => suspend(fn, [loader, key], { equal: is.equ }));
1995
+ const results = cacheKeys.map(
1996
+ (key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
1997
+ );
2776
1998
  return Array.isArray(input) ? results : results[0];
2777
1999
  }
2778
- useLoader.preload = function(loader, input, extensions, onProgress) {
2779
- const keys = Array.isArray(input) ? input : [input];
2780
- keys.forEach((key) => preload(loadingFn(extensions, onProgress), [loader, key]));
2000
+ useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
2001
+ const inputs = Array.isArray(input) ? input : [input];
2002
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2003
+ cacheKeys.forEach(
2004
+ (key, index) => preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
2005
+ );
2781
2006
  };
2782
- useLoader.clear = function(loader, input) {
2783
- const keys = Array.isArray(input) ? input : [input];
2784
- keys.forEach((key) => clear([loader, key]));
2007
+ useLoader.clear = function(loader, input, cacheKey) {
2008
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
2009
+ cacheKeys.forEach((key) => clear([loader, key]));
2785
2010
  };
2786
2011
  useLoader.loader = getLoader;
2787
2012
 
@@ -2857,20 +2082,14 @@ function useFrame(callback, priorityOrOptions) {
2857
2082
  }
2858
2083
  };
2859
2084
  } else {
2860
- const registerOutside = () => {
2861
- return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
2862
- };
2863
- if (scheduler.independent || scheduler.isReady) {
2864
- return registerOutside();
2865
- }
2866
- let unregisterJob = null;
2867
- const unsubReady = scheduler.onRootReady(() => {
2868
- unregisterJob = registerOutside();
2869
- });
2870
- return () => {
2871
- unsubReady();
2872
- unregisterJob?.();
2873
- };
2085
+ return scheduler.register(
2086
+ (state, delta) => {
2087
+ const frameState = state;
2088
+ if (!frameState.renderer) return;
2089
+ callbackRef.current?.(frameState, delta);
2090
+ },
2091
+ { id, ...options }
2092
+ );
2874
2093
  }
2875
2094
  }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2876
2095
  const isPaused = React.useSyncExternalStore(
@@ -2961,18 +2180,18 @@ function buildFromCache(input, textureCache) {
2961
2180
  function useTexture(input, optionsOrOnLoad) {
2962
2181
  const renderer = useThree((state) => state.internal.actualRenderer);
2963
2182
  const store = useStore();
2964
- const textureCache = useThree((state) => state.textures);
2965
2183
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
2966
- const { onLoad, cache = false } = options;
2184
+ const { onLoad, cache = true } = options;
2967
2185
  const onLoadRef = useRef(onLoad);
2968
2186
  onLoadRef.current = onLoad;
2969
2187
  const onLoadCalledForRef = useRef(null);
2970
2188
  const urls = useMemo(() => getUrls(input), [input]);
2971
2189
  const cachedResult = useMemo(() => {
2972
2190
  if (!cache) return null;
2973
- if (!allUrlsCached(urls, textureCache)) return null;
2974
- return buildFromCache(input, textureCache);
2975
- }, [cache, urls, textureCache, input]);
2191
+ const textures = store.getState().textures;
2192
+ if (!allUrlsCached(urls, textures)) return null;
2193
+ return buildFromCache(input, textures);
2194
+ }, [cache, urls, input, store]);
2976
2195
  const loadedTextures = useLoader(
2977
2196
  TextureLoader,
2978
2197
  IsObject(input) ? Object.values(input) : input
@@ -3016,8 +2235,6 @@ function useTexture(input, optionsOrOnLoad) {
3016
2235
  }, [input, loadedTextures, cachedResult]);
3017
2236
  useEffect(() => {
3018
2237
  if (!cache) return;
3019
- if (cachedResult) return;
3020
- const set = store.setState;
3021
2238
  const urlTextureMap = [];
3022
2239
  if (typeof input === "string") {
3023
2240
  urlTextureMap.push([input, mappedTextures]);
@@ -3031,18 +2248,32 @@ function useTexture(input, optionsOrOnLoad) {
3031
2248
  urlTextureMap.push([url, textureRecord[key]]);
3032
2249
  }
3033
2250
  }
3034
- set((state) => {
3035
- const newMap = new Map(state.textures);
3036
- let changed = false;
2251
+ store.setState((state) => {
2252
+ const refs = new Map(state._textureRefs);
2253
+ let textures = state.textures;
2254
+ let added = false;
3037
2255
  for (const [url, texture] of urlTextureMap) {
3038
- if (!newMap.has(url)) {
3039
- newMap.set(url, texture);
3040
- changed = true;
2256
+ if (!textures.has(url)) {
2257
+ if (!added) {
2258
+ textures = new Map(textures);
2259
+ added = true;
2260
+ }
2261
+ textures.set(url, texture);
3041
2262
  }
2263
+ refs.set(url, (refs.get(url) ?? 0) + 1);
3042
2264
  }
3043
- return changed ? { textures: newMap } : state;
2265
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
3044
2266
  });
3045
- }, [cache, input, mappedTextures, store, cachedResult]);
2267
+ return () => store.setState((state) => {
2268
+ const refs = new Map(state._textureRefs);
2269
+ for (const [url] of urlTextureMap) {
2270
+ const next = (refs.get(url) ?? 0) - 1;
2271
+ if (next <= 0) refs.delete(url);
2272
+ else refs.set(url, next);
2273
+ }
2274
+ return { _textureRefs: refs };
2275
+ });
2276
+ }, [cache, input, mappedTextures, store]);
3046
2277
  return mappedTextures;
3047
2278
  }
3048
2279
  useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
@@ -3058,96 +2289,63 @@ const Texture = ({
3058
2289
  return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
3059
2290
  };
3060
2291
 
3061
- function getTextureValue(entry) {
3062
- if (entry instanceof Texture$1) return entry;
3063
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof Texture$1) {
3064
- return entry.value;
3065
- }
3066
- return null;
3067
- }
3068
- function useTextures() {
2292
+ function useTextures(selector) {
3069
2293
  const store = useStore();
3070
- return useMemo(() => {
3071
- const set = store.setState;
2294
+ const registry = useMemo(() => {
3072
2295
  const getState = store.getState;
3073
- const add = (key, value) => {
3074
- set((state) => {
3075
- const newMap = new Map(state.textures);
3076
- newMap.set(key, value);
3077
- return { textures: newMap };
3078
- });
3079
- };
3080
- const addMultiple = (items) => {
3081
- set((state) => {
3082
- const newMap = new Map(state.textures);
3083
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
3084
- for (const [key, value] of entries) {
3085
- newMap.set(key, value);
3086
- }
3087
- return { textures: newMap };
3088
- });
3089
- };
3090
- const remove = (key) => {
3091
- set((state) => {
3092
- const newMap = new Map(state.textures);
3093
- newMap.delete(key);
3094
- return { textures: newMap };
3095
- });
3096
- };
3097
- const removeMultiple = (keys) => {
3098
- set((state) => {
3099
- const newMap = new Map(state.textures);
3100
- for (const key of keys) newMap.delete(key);
3101
- return { textures: newMap };
3102
- });
3103
- };
3104
- const dispose = (key) => {
3105
- const entry = getState().textures.get(key);
3106
- if (entry) {
3107
- const tex = getTextureValue(entry);
3108
- tex?.dispose();
3109
- }
3110
- remove(key);
3111
- };
3112
- const disposeMultiple = (keys) => {
3113
- const textures = getState().textures;
3114
- for (const key of keys) {
3115
- const entry = textures.get(key);
3116
- if (entry) {
3117
- const tex = getTextureValue(entry);
3118
- tex?.dispose();
3119
- }
3120
- }
3121
- removeMultiple(keys);
3122
- };
3123
- const disposeAll = () => {
3124
- const textures = getState().textures;
3125
- for (const entry of textures.values()) {
3126
- const tex = getTextureValue(entry);
3127
- tex?.dispose();
3128
- }
3129
- set({ textures: /* @__PURE__ */ new Map() });
3130
- };
2296
+ const setState = store.setState;
2297
+ const getOne = (key) => getState().textures.get(key);
3131
2298
  return {
3132
- // Getter for the textures Map (reactive via getState)
3133
- get textures() {
2299
+ get all() {
3134
2300
  return getState().textures;
3135
2301
  },
3136
- // Read
3137
- get: (key) => getState().textures.get(key),
2302
+ get(input) {
2303
+ if (typeof input === "string") return getOne(input);
2304
+ if (Array.isArray(input)) return input.map(getOne);
2305
+ const out = {};
2306
+ for (const name in input) out[name] = getOne(input[name]);
2307
+ return out;
2308
+ },
3138
2309
  has: (key) => getState().textures.has(key),
3139
- // Write
3140
- add,
3141
- addMultiple,
3142
- // Remove (cache only)
3143
- remove,
3144
- removeMultiple,
3145
- // Dispose (GPU + cache)
3146
- dispose,
3147
- disposeMultiple,
3148
- disposeAll
2310
+ add(keyOrRecord, texture) {
2311
+ setState((state) => {
2312
+ const textures = new Map(state.textures);
2313
+ if (typeof keyOrRecord === "string") {
2314
+ textures.set(keyOrRecord, texture);
2315
+ } else {
2316
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
2317
+ }
2318
+ return { textures };
2319
+ });
2320
+ },
2321
+ dispose(key, options) {
2322
+ const state = getState();
2323
+ const refs = state._textureRefs.get(key) ?? 0;
2324
+ if (refs > 0 && !options?.force) {
2325
+ console.warn(
2326
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
2327
+ );
2328
+ return false;
2329
+ }
2330
+ state.textures.get(key)?.dispose();
2331
+ setState((s) => {
2332
+ const textures = new Map(s.textures);
2333
+ textures.delete(key);
2334
+ const nextRefs = new Map(s._textureRefs);
2335
+ nextRefs.delete(key);
2336
+ return { textures, _textureRefs: nextRefs };
2337
+ });
2338
+ return true;
2339
+ },
2340
+ disposeAll() {
2341
+ for (const texture of getState().textures.values()) texture.dispose();
2342
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
2343
+ }
3149
2344
  };
3150
2345
  }, [store]);
2346
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
2347
+ const selected = useThree(subscribe);
2348
+ return selector ? selected : registry;
3151
2349
  }
3152
2350
 
3153
2351
  function useRenderTarget(widthOrOptions, heightOrOptions, options) {
@@ -3228,7 +2426,7 @@ function advance(timestamp) {
3228
2426
  getScheduler().step(timestamp);
3229
2427
  }
3230
2428
 
3231
- const version = "10.0.0-alpha.2";
2429
+ const version = "10.0.0-alpha.3";
3232
2430
  const packageData = {
3233
2431
  version: version};
3234
2432
 
@@ -14484,7 +13682,7 @@ function n0() {
14484
13682
  var t0 = n0();
14485
13683
  const r0 = Xb(t0);
14486
13684
 
14487
- const t = 1, o = 8, r = 32, e = 2;
13685
+ const t = 1, o = 8, r = 32, e = 2, i$1 = 268435456;
14488
13686
 
14489
13687
  function createReconciler(config) {
14490
13688
  const reconciler2 = r0(config);
@@ -14649,7 +13847,15 @@ function setFiberRef(fiber, publicInstance) {
14649
13847
  }
14650
13848
  }
14651
13849
  const reconstructed = [];
14652
- function swapInstances() {
13850
+ function flushReconstructedInstances() {
13851
+ if (reconstructed.length === 0) return;
13852
+ try {
13853
+ swapReconstructedInstances();
13854
+ } finally {
13855
+ reconstructed.length = 0;
13856
+ }
13857
+ }
13858
+ function swapReconstructedInstances() {
14653
13859
  for (const [instance] of reconstructed) {
14654
13860
  const parent = instance.parent;
14655
13861
  if (parent) {
@@ -14667,6 +13873,7 @@ function swapInstances() {
14667
13873
  }
14668
13874
  }
14669
13875
  if (instance.isHidden) unhideInstance(instance);
13876
+ if (isObject3D(instance.object)) removeInteractivity(findInitialRoot(instance), instance.object);
14670
13877
  if (instance.object.__r3f) delete instance.object.__r3f;
14671
13878
  if (instance.type !== "primitive") disposeOnIdle(instance.object);
14672
13879
  }
@@ -14679,6 +13886,8 @@ function swapInstances() {
14679
13886
  instance.object.__r3f = instance;
14680
13887
  setFiberRef(fiber, instance.object);
14681
13888
  delete instance.appliedOnce;
13889
+ instance.eventCount = 0;
13890
+ instance.handlers = {};
14682
13891
  applyProps(instance.object, instance.props);
14683
13892
  if (instance.props.attach) {
14684
13893
  attach(parent, instance);
@@ -14695,14 +13904,114 @@ function swapInstances() {
14695
13904
  invalidateInstance(instance);
14696
13905
  }
14697
13906
  }
14698
- reconstructed.length = 0;
14699
13907
  }
14700
13908
  const handleTextInstance = () => {
14701
13909
  };
14702
13910
  const NO_CONTEXT = {};
14703
13911
  let currentUpdatePriority = NoEventPriority;
14704
- const NoFlags = 0;
14705
- const Update = 4;
13912
+ function getEventPriority(type) {
13913
+ switch (type) {
13914
+ case "beforetoggle":
13915
+ case "cancel":
13916
+ case "click":
13917
+ case "close":
13918
+ case "contextmenu":
13919
+ case "copy":
13920
+ case "cut":
13921
+ case "auxclick":
13922
+ case "dblclick":
13923
+ case "dragend":
13924
+ case "dragstart":
13925
+ case "drop":
13926
+ case "focusin":
13927
+ case "focusout":
13928
+ case "input":
13929
+ case "invalid":
13930
+ case "keydown":
13931
+ case "keypress":
13932
+ case "keyup":
13933
+ case "mousedown":
13934
+ case "mouseup":
13935
+ case "paste":
13936
+ case "pause":
13937
+ case "play":
13938
+ case "pointercancel":
13939
+ case "pointerdown":
13940
+ case "pointerup":
13941
+ case "ratechange":
13942
+ case "reset":
13943
+ case "resize":
13944
+ case "seeked":
13945
+ case "submit":
13946
+ case "touchcancel":
13947
+ case "touchend":
13948
+ case "touchstart":
13949
+ case "volumechange":
13950
+ case "change":
13951
+ case "selectionchange":
13952
+ case "compositionstart":
13953
+ case "compositionend":
13954
+ case "compositionupdate":
13955
+ case "beforeinput":
13956
+ case "blur":
13957
+ case "fullscreenchange":
13958
+ case "focus":
13959
+ case "hashchange":
13960
+ case "popstate":
13961
+ case "select":
13962
+ case "selectstart":
13963
+ return e;
13964
+ case "drag":
13965
+ case "dragenter":
13966
+ case "dragexit":
13967
+ case "dragleave":
13968
+ case "dragover":
13969
+ case "mousemove":
13970
+ case "mouseout":
13971
+ case "mouseover":
13972
+ case "pointermove":
13973
+ case "pointerout":
13974
+ case "pointerover":
13975
+ case "scroll":
13976
+ case "touchmove":
13977
+ case "wheel":
13978
+ case "mouseenter":
13979
+ case "mouseleave":
13980
+ case "pointerenter":
13981
+ case "pointerleave":
13982
+ return o;
13983
+ case "message": {
13984
+ switch (unstable_getCurrentPriorityLevel()) {
13985
+ case unstable_ImmediatePriority:
13986
+ return e;
13987
+ case unstable_UserBlockingPriority:
13988
+ return o;
13989
+ case unstable_NormalPriority:
13990
+ case unstable_LowPriority:
13991
+ return r;
13992
+ case unstable_IdlePriority:
13993
+ return i$1;
13994
+ default:
13995
+ return r;
13996
+ }
13997
+ }
13998
+ default:
13999
+ return r;
14000
+ }
14001
+ }
14002
+ function scheduleMicrotask(callback) {
14003
+ if (typeof queueMicrotask === "function") {
14004
+ queueMicrotask(callback);
14005
+ } else if (typeof Promise !== "undefined") {
14006
+ Promise.resolve().then(callback).catch((error) => {
14007
+ setTimeout(() => {
14008
+ throw error;
14009
+ });
14010
+ });
14011
+ } else {
14012
+ setTimeout(callback);
14013
+ }
14014
+ }
14706
14015
  const reconciler = /* @__PURE__ */ createReconciler({
14707
14016
  isPrimaryRenderer: false,
14708
14017
  warnsIfNotActing: false,
@@ -14741,16 +14050,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
14741
14050
  else if (newProps.args?.length !== oldProps.args?.length) reconstruct = true;
14742
14051
  else if (newProps.args?.some((value, index) => value !== oldProps.args?.[index])) reconstruct = true;
14743
14052
  if (reconstruct) {
14744
- reconstructed.push([instance, { ...newProps }, fiber]);
14053
+ reconstructed.push([instance, getInstanceProps(newProps), fiber]);
14745
14054
  } else {
14746
14055
  const changedProps = diffProps(instance, newProps);
14747
- if (Object.keys(changedProps).length) {
14748
- Object.assign(instance.props, changedProps);
14749
- applyProps(instance.object, changedProps);
14750
- }
14056
+ const attach2 = instance.props.attach;
14057
+ instance.props = getInstanceProps(newProps);
14058
+ if (attach2 !== void 0) instance.props.attach = attach2;
14059
+ else delete instance.props.attach;
14060
+ if (Object.keys(changedProps).length) applyProps(instance.object, changedProps);
14751
14061
  }
14752
- const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
14753
- if (isTailSibling) swapInstances();
14754
14062
  },
14755
14063
  finalizeInitialChildren: (instance) => {
14756
14064
  for (const prop in instance.props) {
@@ -14775,8 +14083,9 @@ const reconciler = /* @__PURE__ */ createReconciler({
14775
14083
  const target = container.getState().internal.container ?? container.getState().scene;
14776
14084
  return prepare(target, container, "", {});
14777
14085
  },
14778
- resetAfterCommit: () => {
14779
- },
14086
+ // Reconstructed instances are swapped once all mutations are committed,
14087
+ // before layout effects run so refs point to the new objects
14088
+ resetAfterCommit: flushReconstructedInstances,
14780
14089
  shouldSetTextContent: () => false,
14781
14090
  clearContainer: () => false,
14782
14091
  hideInstance,
@@ -14784,6 +14093,9 @@ const reconciler = /* @__PURE__ */ createReconciler({
14784
14093
  createTextInstance: handleTextInstance,
14785
14094
  hideTextInstance: handleTextInstance,
14786
14095
  unhideTextInstance: handleTextInstance,
14096
+ // Mirrors react-dom, which flushes discrete work in microtasks
14097
+ supportsMicrotasks: true,
14098
+ scheduleMicrotask,
14787
14099
  scheduleTimeout: typeof setTimeout === "function" ? setTimeout : void 0,
14788
14100
  cancelTimeout: typeof clearTimeout === "function" ? clearTimeout : void 0,
14789
14101
  noTimeout: -1,
@@ -14824,28 +14136,9 @@ const reconciler = /* @__PURE__ */ createReconciler({
14824
14136
  },
14825
14137
  resolveUpdatePriority() {
14826
14138
  if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
14827
- switch (typeof window !== "undefined" && window.event?.type) {
14828
- case "click":
14829
- case "contextmenu":
14830
- case "dblclick":
14831
- case "dragenter":
14832
- case "dragleave":
14833
- case "drop":
14834
- case "pointercancel":
14835
- case "pointerdown":
14836
- case "pointerup":
14837
- return e;
14838
- case "dragover":
14839
- case "pointermove":
14840
- case "pointerout":
14841
- case "pointerover":
14842
- case "pointerenter":
14843
- case "pointerleave":
14844
- case "wheel":
14845
- return o;
14846
- default:
14847
- return r;
14848
- }
14139
+ const eventType = typeof window !== "undefined" ? window.event?.type : void 0;
14140
+ if (eventType === void 0) return r;
14141
+ return getEventPriority(eventType);
14849
14142
  },
14850
14143
  resetFormInstance() {
14851
14144
  },
@@ -14894,11 +14187,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
14894
14187
  cloneMutableTextInstance(textInstance) {
14895
14188
  return textInstance;
14896
14189
  },
14190
+ // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
14191
+ // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
14192
+ // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
14193
+ // React never calls them. They stay as no-ops rather than throws: the host config is shared by
14194
+ // every root, and a throw here would turn a React-internal probe into a hard crash for users.
14195
+ // Guarded by a test asserting a full render + unmount never reaches them.
14897
14196
  cloneRootViewTransitionContainer(_rootContainer) {
14898
- throw new Error("Not implemented.");
14899
14197
  },
14900
14198
  removeRootViewTransitionClone(_rootContainer, _clone) {
14901
- throw new Error("Not implemented.");
14902
14199
  },
14903
14200
  // https://github.com/facebook/react/pull/32465
14904
14201
  createFragmentInstance: (_fiber) => null,
@@ -14983,6 +14280,7 @@ function createRoot(canvas) {
14983
14280
  const lastConfiguredProps = {};
14984
14281
  let configured = false;
14985
14282
  let pending = null;
14283
+ let rendererSetup = null;
14986
14284
  return {
14987
14285
  async configure(props = {}) {
14988
14286
  let resolve;
@@ -14998,7 +14296,6 @@ function createRoot(canvas) {
14998
14296
  events,
14999
14297
  onCreated: onCreatedCallback,
15000
14298
  shadows = false,
15001
- textureColorSpace = SRGBColorSpace,
15002
14299
  orthographic = false,
15003
14300
  frameloop = "always",
15004
14301
  dpr = [1, 2],
@@ -15013,7 +14310,8 @@ function createRoot(canvas) {
15013
14310
  _sizeProps,
15014
14311
  forceEven
15015
14312
  } = props;
15016
- const state = store.getState();
14313
+ const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || SRGBColorSpace;
14314
+ let state = store.getState();
15017
14315
  const defaultGPUProps = {
15018
14316
  canvas,
15019
14317
  antialias: true
@@ -15027,46 +14325,64 @@ function createRoot(canvas) {
15027
14325
  throw new Error("Cannot use both gl and renderer props at the same time");
15028
14326
  }
15029
14327
  let renderer = state.internal.actualRenderer;
15030
- if (primaryCanvas && !state.internal.actualRenderer) {
15031
- const primary = await waitForPrimary(primaryCanvas);
15032
- renderer = primary.renderer;
15033
- state.internal.actualRenderer = renderer;
15034
- const canvasTarget = new CanvasTarget(canvas);
15035
- primary.store.setState((prev) => ({
15036
- internal: { ...prev.internal, isMultiCanvas: true }
15037
- }));
15038
- state.set((prev) => ({
15039
- webGPUSupported: primary.store.getState().webGPUSupported,
15040
- renderer,
15041
- primaryStore: primary.store,
15042
- internal: {
15043
- ...prev.internal,
15044
- canvasTarget,
15045
- isMultiCanvas: true,
15046
- isSecondary: true,
15047
- targetId: primaryCanvas
15048
- }
15049
- }));
15050
- } else if (!state.internal.actualRenderer) {
15051
- renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
15052
- if (!renderer.hasInitialized?.()) {
15053
- await renderer.init();
15054
- }
15055
- const backend = renderer.backend;
15056
- const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
15057
- state.internal.actualRenderer = renderer;
15058
- state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
15059
- if (canvasId && !state.internal.isSecondary) {
15060
- const canvasTarget = new CanvasTarget(canvas);
15061
- const unregisterPrimary = registerPrimary(canvasId, renderer, store);
15062
- state.set((prev) => ({
15063
- internal: {
15064
- ...prev.internal,
15065
- canvasTarget,
15066
- unregisterPrimary
14328
+ if (!state.internal.actualRenderer) {
14329
+ if (!rendererSetup) {
14330
+ rendererSetup = (async () => {
14331
+ if (primaryCanvas) {
14332
+ const primary = await waitForPrimary(primaryCanvas);
14333
+ renderer = primary.renderer;
14334
+ state.internal.actualRenderer = renderer;
14335
+ const canvasTarget = new CanvasTarget(canvas);
14336
+ primary.store.setState((prev) => ({
14337
+ internal: { ...prev.internal, isMultiCanvas: true }
14338
+ }));
14339
+ state.set((prev) => ({
14340
+ webGPUSupported: primary.store.getState().webGPUSupported,
14341
+ renderer,
14342
+ primaryStore: primary.store,
14343
+ internal: {
14344
+ ...prev.internal,
14345
+ canvasTarget,
14346
+ isMultiCanvas: true,
14347
+ isSecondary: true,
14348
+ targetId: primaryCanvas
14349
+ }
14350
+ }));
14351
+ } else {
14352
+ renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
14353
+ if (!renderer.hasInitialized?.()) {
14354
+ const size2 = computeInitialSize(canvas, propsSize);
14355
+ if (size2.width > 0 && size2.height > 0) {
14356
+ const pixelRatio = calculateDpr(dpr);
14357
+ canvas.width = size2.width * pixelRatio;
14358
+ canvas.height = size2.height * pixelRatio;
14359
+ }
14360
+ await renderer.init();
14361
+ }
14362
+ const backend = renderer.backend;
14363
+ const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
14364
+ state.internal.actualRenderer = renderer;
14365
+ state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14366
+ if (canvasId && !state.internal.isSecondary) {
14367
+ const canvasTarget = new CanvasTarget(canvas);
14368
+ const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14369
+ state.set((prev) => ({
14370
+ internal: {
14371
+ ...prev.internal,
14372
+ canvasTarget,
14373
+ unregisterPrimary
14374
+ }
14375
+ }));
14376
+ }
15067
14377
  }
15068
- }));
14378
+ })().catch((err) => {
14379
+ rendererSetup = null;
14380
+ throw err;
14381
+ });
15069
14382
  }
14383
+ await rendererSetup;
14384
+ state = store.getState();
14385
+ renderer = state.internal.actualRenderer;
15070
14386
  }
15071
14387
  let raycaster = state.raycaster;
15072
14388
  if (!raycaster) state.set({ raycaster: raycaster = new Raycaster() });
@@ -15159,7 +14475,7 @@ function createRoot(canvas) {
15159
14475
  lastConfiguredProps.performance = performance;
15160
14476
  }
15161
14477
  if (!state.xr) {
15162
- const handleXRFrame = (timestamp, frame) => {
14478
+ const handleXRFrame = (timestamp, _frame) => {
15163
14479
  const state2 = store.getState();
15164
14480
  if (state2.frameloop === "never") return;
15165
14481
  advance(timestamp);
@@ -15195,15 +14511,22 @@ function createRoot(canvas) {
15195
14511
  const oldType = renderer.shadowMap.type;
15196
14512
  renderer.shadowMap.enabled = !!shadows;
15197
14513
  if (is.boo(shadows)) {
15198
- renderer.shadowMap.type = PCFSoftShadowMap;
14514
+ renderer.shadowMap.type = PCFShadowMap;
15199
14515
  } else if (is.str(shadows)) {
14516
+ if (shadows === "soft") {
14517
+ notifyDepreciated({
14518
+ heading: 'shadows="soft" is deprecated',
14519
+ body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
14520
+ link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
14521
+ });
14522
+ }
15200
14523
  const types = {
15201
14524
  basic: BasicShadowMap,
15202
14525
  percentage: PCFShadowMap,
15203
- soft: PCFSoftShadowMap,
14526
+ soft: PCFShadowMap,
15204
14527
  variance: VSMShadowMap
15205
14528
  };
15206
- renderer.shadowMap.type = types[shadows] ?? PCFSoftShadowMap;
14529
+ renderer.shadowMap.type = types[shadows] ?? PCFShadowMap;
15207
14530
  } else if (is.obj(shadows)) {
15208
14531
  Object.assign(renderer.shadowMap, shadows);
15209
14532
  }
@@ -15219,13 +14542,24 @@ function createRoot(canvas) {
15219
14542
  if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
15220
14543
  lastConfiguredProps.textureColorSpace = textureColorSpace;
15221
14544
  }
14545
+ const r3fProps = ["textureColorSpace"];
14546
+ const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
14547
+ const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
15222
14548
  if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
15223
- applyProps(renderer, glConfig);
14549
+ const glProps = {};
14550
+ for (const key in glConfig) {
14551
+ if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
14552
+ }
14553
+ applyProps(renderer, glProps);
15224
14554
  }
15225
14555
  if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
15226
14556
  const currentRenderer = state.renderer;
15227
14557
  if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
15228
- applyProps(currentRenderer, rendererConfig);
14558
+ const rendererProps = {};
14559
+ for (const key in rendererConfig) {
14560
+ if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
14561
+ }
14562
+ applyProps(currentRenderer, rendererProps);
15229
14563
  }
15230
14564
  }
15231
14565
  const scheduler = getScheduler();
@@ -15251,6 +14585,18 @@ function createRoot(canvas) {
15251
14585
  system: true
15252
14586
  }
15253
14587
  );
14588
+ const unregisterEventsFlush = scheduler.register(
14589
+ () => {
14590
+ const state2 = store.getState();
14591
+ state2.events.flush?.();
14592
+ },
14593
+ {
14594
+ id: `${newRootId}_events`,
14595
+ rootId: newRootId,
14596
+ phase: "input",
14597
+ system: true
14598
+ }
14599
+ );
15254
14600
  const unregisterFrustum = scheduler.register(
15255
14601
  () => {
15256
14602
  const state2 = store.getState();
@@ -15261,7 +14607,7 @@ function createRoot(canvas) {
15261
14607
  {
15262
14608
  id: `${newRootId}_frustum`,
15263
14609
  rootId: newRootId,
15264
- phase: "preRender",
14610
+ before: "render",
15265
14611
  system: true
15266
14612
  }
15267
14613
  );
@@ -15273,7 +14619,7 @@ function createRoot(canvas) {
15273
14619
  {
15274
14620
  id: `${newRootId}_visibility`,
15275
14621
  rootId: newRootId,
15276
- phase: "preRender",
14622
+ before: "render",
15277
14623
  system: true,
15278
14624
  after: `${newRootId}_frustum`
15279
14625
  }
@@ -15285,7 +14631,7 @@ function createRoot(canvas) {
15285
14631
  const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
15286
14632
  if (userHandlesRender || state2.internal.priority) return;
15287
14633
  try {
15288
- if (state2.postProcessing?.render) state2.postProcessing.render();
14634
+ if (state2.renderPipeline?.render) state2.renderPipeline.render();
15289
14635
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
15290
14636
  } catch (error) {
15291
14637
  state2.setError(error instanceof Error ? error : new Error(String(error)));
@@ -15310,6 +14656,7 @@ function createRoot(canvas) {
15310
14656
  unregisterRoot: () => {
15311
14657
  unregisterRoot();
15312
14658
  unregisterCanvasTarget();
14659
+ unregisterEventsFlush();
15313
14660
  unregisterFrustum();
15314
14661
  unregisterVisibility();
15315
14662
  unregisterRender();
@@ -15385,7 +14732,8 @@ function unmountComponentAtNode(canvas, callback) {
15385
14732
  dispose(state.scene);
15386
14733
  _roots.delete(canvas);
15387
14734
  if (callback) callback(canvas);
15388
- } catch {
14735
+ } catch (error) {
14736
+ console.warn("[R3F] Error while unmounting root; teardown may be incomplete:", error);
15389
14737
  }
15390
14738
  }, 500);
15391
14739
  }
@@ -15448,6 +14796,7 @@ function PortalInner({ state = {}, children, container }) {
15448
14796
  viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
15449
14797
  if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
15450
14798
  }
14799
+ const set = injectState.set;
15451
14800
  return {
15452
14801
  // The intersect consists of the previous root state
15453
14802
  ...rootState,
@@ -15466,7 +14815,7 @@ function PortalInner({ state = {}, children, container }) {
15466
14815
  size: resolvedSize,
15467
14816
  viewport: { ...rootState.viewport, ...viewport },
15468
14817
  // Layers are allowed to override events
15469
- setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
14818
+ setEvents: (events2) => set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
15470
14819
  // Container for child attachment - the portalScene (injected or container itself)
15471
14820
  internal: { ...rootState.internal, ...injectState.internal, container: portalScene }
15472
14821
  };
@@ -15475,9 +14824,13 @@ function PortalInner({ state = {}, children, container }) {
15475
14824
  const store = createWithEqualityFn((set, get) => ({ ...rest, set, get }));
15476
14825
  const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
15477
14826
  onMutate(previousRoot.getState());
15478
- previousRoot.subscribe(onMutate);
15479
14827
  return store;
15480
14828
  }, [previousRoot, container]);
14829
+ useIsomorphicLayoutEffect(() => {
14830
+ const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
14831
+ const unsubscribe = previousRoot.subscribe(onMutate);
14832
+ return unsubscribe;
14833
+ }, [previousRoot, usePortalStore]);
15481
14834
  return (
15482
14835
  // @ts-ignore, reconciler types are not maintained
15483
14836
  /* @__PURE__ */ jsx(Fragment, { children: reconciler.createPortal(
@@ -15491,6 +14844,178 @@ function flushSync(fn) {
15491
14844
  return reconciler.flushSyncFromReconciler(fn);
15492
14845
  }
15493
14846
 
14847
+ function parseBackground(background) {
14848
+ if (!background) return null;
14849
+ if (typeof background === "object" && !background.isColor) {
14850
+ const { backgroundMap, envMap, files, preset, ...rest } = background;
14851
+ return {
14852
+ ...rest,
14853
+ preset,
14854
+ files: envMap || files,
14855
+ backgroundFiles: backgroundMap,
14856
+ background: true
14857
+ };
14858
+ }
14859
+ if (typeof background === "number") {
14860
+ return { color: background, background: true };
14861
+ }
14862
+ if (typeof background === "string") {
14863
+ if (background in presetsObj) {
14864
+ return { preset: background, background: true };
14865
+ }
14866
+ if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
14867
+ return { files: background, background: true };
14868
+ }
14869
+ return { color: background, background: true };
14870
+ }
14871
+ if (background.isColor) {
14872
+ return { color: background, background: true };
14873
+ }
14874
+ return null;
14875
+ }
14876
+
14877
+ function parseRendererConfig(rendererProp) {
14878
+ const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
14879
+ if (isRendererConfig) {
14880
+ const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
14881
+ return {
14882
+ primaryCanvas: pc,
14883
+ scheduler: sc,
14884
+ renderer: Object.keys(rest).length > 0 ? rest : rendererProp
14885
+ };
14886
+ }
14887
+ return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
14888
+ }
14889
+
14890
+ const RESOURCE_KINDS = ["nodes", "uniforms", "buffers", "gpuStorage"];
14891
+ const ROOT_SCOPE = "";
14892
+ const registries = /* @__PURE__ */ new WeakMap();
14893
+ function getKindRegistry(store, kind) {
14894
+ let registry = registries.get(store);
14895
+ if (!registry) {
14896
+ registry = {
14897
+ nodes: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14898
+ uniforms: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14899
+ buffers: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14900
+ gpuStorage: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() }
14901
+ };
14902
+ registries.set(store, registry);
14903
+ }
14904
+ return registry[kind];
14905
+ }
14906
+ function resolvePrimary$1(store) {
14907
+ return store.getState().primaryStore ?? store;
14908
+ }
14909
+ function isScopeValid(store, kind, scopeKey) {
14910
+ return getKindRegistry(store, kind).valid.has(scopeKey);
14911
+ }
14912
+ function peekStaged(store, kind, scopeKey) {
14913
+ return getKindRegistry(store, kind).staged.get(scopeKey);
14914
+ }
14915
+ function stageEntries(store, kind, scopeKey, entries) {
14916
+ const { staged } = getKindRegistry(store, kind);
14917
+ const current = staged.get(scopeKey);
14918
+ if (current) Object.assign(current, entries);
14919
+ else staged.set(scopeKey, { ...entries });
14920
+ }
14921
+ function readCommittedScope(map, scopeKey, isLeaf) {
14922
+ if (scopeKey === ROOT_SCOPE) {
14923
+ const leaves = {};
14924
+ for (const [key, value] of Object.entries(map)) if (isLeaf(value)) leaves[key] = value;
14925
+ return leaves;
14926
+ }
14927
+ const scopeData = map[scopeKey];
14928
+ if (scopeData && typeof scopeData === "object" && !isLeaf(scopeData)) return scopeData;
14929
+ return {};
14930
+ }
14931
+ function withStagedOverlay(store, kind, committed) {
14932
+ const { staged } = getKindRegistry(store, kind);
14933
+ if (staged.size === 0) return committed;
14934
+ const view = { ...committed };
14935
+ for (const [scopeKey, entries] of staged) {
14936
+ if (scopeKey === ROOT_SCOPE) Object.assign(view, entries);
14937
+ else view[scopeKey] = { ...view[scopeKey], ...entries };
14938
+ }
14939
+ return view;
14940
+ }
14941
+ function flushStagedScope(store, kind, scopeKey, isLeaf) {
14942
+ const registry = getKindRegistry(store, kind);
14943
+ const staged = registry.staged.get(scopeKey);
14944
+ if (!staged) return;
14945
+ registry.staged.delete(scopeKey);
14946
+ const replace = !registry.valid.has(scopeKey);
14947
+ store.setState((state) => {
14948
+ const map = state[kind];
14949
+ if (scopeKey === ROOT_SCOPE) {
14950
+ if (!replace) return { [kind]: { ...map, ...staged } };
14951
+ const next = {};
14952
+ for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
14953
+ return { [kind]: { ...next, ...staged } };
14954
+ }
14955
+ const current = replace ? {} : map[scopeKey] ?? {};
14956
+ return { [kind]: { ...map, [scopeKey]: { ...current, ...staged } } };
14957
+ });
14958
+ registry.valid.add(scopeKey);
14959
+ }
14960
+ function invalidateResource(store, kind, scopeKey) {
14961
+ const registry = getKindRegistry(store, kind);
14962
+ if (scopeKey === void 0) {
14963
+ registry.valid.clear();
14964
+ registry.staged.clear();
14965
+ } else {
14966
+ registry.valid.delete(scopeKey);
14967
+ registry.staged.delete(scopeKey);
14968
+ }
14969
+ }
14970
+ function rebuildResource(store, kind, userScope) {
14971
+ const primary = resolvePrimary$1(store);
14972
+ invalidateResource(primary, kind, userScope === "root" ? ROOT_SCOPE : userScope);
14973
+ primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
14974
+ }
14975
+ function invalidateAllResources(store) {
14976
+ for (const kind of RESOURCE_KINDS) invalidateResource(store, kind);
14977
+ }
14978
+ function removeResourceEntries(store, kind, names, scope, isLeaf) {
14979
+ const staged = peekStaged(store, kind, scope ?? ROOT_SCOPE);
14980
+ if (staged) for (const name of names) delete staged[name];
14981
+ store.setState((state) => {
14982
+ const map = state[kind];
14983
+ if (scope) {
14984
+ const currentScope = { ...map[scope] };
14985
+ for (const name of names) delete currentScope[name];
14986
+ return { [kind]: { ...map, [scope]: currentScope } };
14987
+ }
14988
+ const next = { ...map };
14989
+ for (const name of names) if (isLeaf(next[name])) delete next[name];
14990
+ return { [kind]: next };
14991
+ });
14992
+ }
14993
+ function clearResourceEntries(store, kind, scope, isLeaf) {
14994
+ const registry = getKindRegistry(store, kind);
14995
+ if (scope && scope !== "root") registry.staged.delete(scope);
14996
+ else if (scope === "root") registry.staged.delete(ROOT_SCOPE);
14997
+ else registry.staged.clear();
14998
+ store.setState((state) => {
14999
+ const map = state[kind];
15000
+ if (scope && scope !== "root") {
15001
+ const { [scope]: _, ...rest } = map;
15002
+ return { [kind]: rest };
15003
+ }
15004
+ if (scope === "root") {
15005
+ const next = {};
15006
+ for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
15007
+ return { [kind]: next };
15008
+ }
15009
+ return { [kind]: {} };
15010
+ });
15011
+ }
15012
+
15013
+ function clearHmrCaches(store) {
15014
+ const primary = store.getState().primaryStore ?? store;
15015
+ invalidateAllResources(primary);
15016
+ primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
15017
+ }
15018
+
15494
15019
  function CanvasImpl({
15495
15020
  ref,
15496
15021
  children,
@@ -15511,6 +15036,8 @@ function CanvasImpl({
15511
15036
  raycaster,
15512
15037
  camera,
15513
15038
  scene,
15039
+ autoUpdateFrustum,
15040
+ occlusion,
15514
15041
  onPointerMissed,
15515
15042
  onDragOverMissed,
15516
15043
  onDropMissed,
@@ -15522,39 +15049,10 @@ function CanvasImpl({
15522
15049
  forceEven,
15523
15050
  ...props
15524
15051
  }) {
15525
- 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 };
15526
- const renderer = Object.keys(rendererConfig).length > 0 ? rendererConfig : rendererProp;
15052
+ const { primaryCanvas, scheduler, renderer } = parseRendererConfig(rendererProp);
15527
15053
  React.useMemo(() => extend(THREE), []);
15528
15054
  const Bridge = useBridge();
15529
- const backgroundProps = React.useMemo(() => {
15530
- if (!background) return null;
15531
- if (typeof background === "object" && !background.isColor) {
15532
- const { backgroundMap, envMap, files, preset, ...rest } = background;
15533
- return {
15534
- ...rest,
15535
- preset,
15536
- files: envMap || files,
15537
- backgroundFiles: backgroundMap,
15538
- background: true
15539
- };
15540
- }
15541
- if (typeof background === "number") {
15542
- return { color: background, background: true };
15543
- }
15544
- if (typeof background === "string") {
15545
- if (background in presetsObj) {
15546
- return { preset: background, background: true };
15547
- }
15548
- if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
15549
- return { files: background, background: true };
15550
- }
15551
- return { color: background, background: true };
15552
- }
15553
- if (background.isColor) {
15554
- return { color: background, background: true };
15555
- }
15556
- return null;
15557
- }, [background]);
15055
+ const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
15558
15056
  const hasInitialSizeRef = React.useRef(false);
15559
15057
  const measureConfig = React.useMemo(() => {
15560
15058
  if (!hasInitialSizeRef.current) {
@@ -15596,6 +15094,7 @@ function CanvasImpl({
15596
15094
  const handleDropMissed = useMutableCallback(onDropMissed);
15597
15095
  const [block, setBlock] = React.useState(false);
15598
15096
  const [error, setError] = React.useState(false);
15097
+ const [fallbackVisible, setFallbackVisible] = React.useState(false);
15599
15098
  if (block) throw block;
15600
15099
  if (error) throw error;
15601
15100
  const root = React.useRef(null);
@@ -15603,6 +15102,7 @@ function CanvasImpl({
15603
15102
  const unsubscribeErrorRef = React.useRef(null);
15604
15103
  useIsomorphicLayoutEffect(() => {
15605
15104
  effectActiveRef.current = true;
15105
+ if (fallbackVisible) return;
15606
15106
  const canvas = canvasRef.current;
15607
15107
  if (effectiveSize.width > 0 && effectiveSize.height > 0 && canvas) {
15608
15108
  if (!root.current) {
@@ -15623,7 +15123,7 @@ function CanvasImpl({
15623
15123
  }
15624
15124
  async function run() {
15625
15125
  if (!effectActiveRef.current || !root.current) return;
15626
- await root.current.configure({
15126
+ const configured = await root.current.configure({
15627
15127
  id,
15628
15128
  primaryCanvas,
15629
15129
  scheduler,
@@ -15638,6 +15138,8 @@ function CanvasImpl({
15638
15138
  performance,
15639
15139
  raycaster,
15640
15140
  camera,
15141
+ autoUpdateFrustum,
15142
+ occlusion,
15641
15143
  size: effectiveSize,
15642
15144
  // Store size props for reset functionality
15643
15145
  _sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
@@ -15662,8 +15164,20 @@ function CanvasImpl({
15662
15164
  }
15663
15165
  onCreated?.(state);
15664
15166
  }
15665
- });
15666
- if (!effectActiveRef.current || !root.current) return;
15167
+ }).then(
15168
+ () => true,
15169
+ // Renderer setup failed (e.g. no WebGL/WebGPU support). The `fallback` prop lives
15170
+ // inside <canvas>, which browsers don't display, so surface it as visible DOM
15171
+ // instead; with no fallback, rethrow to an external error boundary. (#3757)
15172
+ (setupError) => {
15173
+ if (effectActiveRef.current) {
15174
+ if (fallback != null) setFallbackVisible(true);
15175
+ else setError(setupError);
15176
+ }
15177
+ return false;
15178
+ }
15179
+ );
15180
+ if (!configured || !effectActiveRef.current || !root.current) return;
15667
15181
  root.current.render(
15668
15182
  /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxs(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: [
15669
15183
  backgroundProps && /* @__PURE__ */ jsx(Environment, { ...backgroundProps }),
@@ -15697,20 +15211,13 @@ function CanvasImpl({
15697
15211
  const handleHMR = () => {
15698
15212
  queueMicrotask(() => {
15699
15213
  const rootEntry = _roots.get(canvas);
15700
- if (rootEntry?.store) {
15701
- rootEntry.store.setState((state) => ({
15702
- nodes: {},
15703
- uniforms: {},
15704
- _hmrVersion: state._hmrVersion + 1
15705
- }));
15706
- }
15214
+ if (rootEntry?.store) clearHmrCaches(rootEntry.store);
15707
15215
  });
15708
15216
  };
15709
15217
  if (typeof import.meta !== "undefined" && import.meta.hot) {
15710
15218
  const hot = import.meta.hot;
15711
15219
  hot.on("vite:afterUpdate", handleHMR);
15712
- return () => hot.dispose?.(() => {
15713
- });
15220
+ return () => hot.off?.("vite:afterUpdate", handleHMR);
15714
15221
  }
15715
15222
  if (typeof module !== "undefined" && module.hot) {
15716
15223
  const hot = module.hot;
@@ -15733,7 +15240,20 @@ function CanvasImpl({
15733
15240
  ...style
15734
15241
  },
15735
15242
  ...props,
15736
- children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, id, className: "r3f-canvas", style: { display: "block" }, children: fallback }) })
15243
+ children: fallbackVisible ? (
15244
+ // Renderer setup failed: render the fallback as visible DOM. Inside <canvas> (below)
15245
+ // it exists in the tree but browsers never paint it, which is the whole bug (#3757).
15246
+ fallback
15247
+ ) : /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
15248
+ "canvas",
15249
+ {
15250
+ ref: canvasRef,
15251
+ id,
15252
+ className: "r3f-canvas",
15253
+ style: { display: "block", width: "100%", height: "100%" },
15254
+ children: fallback
15255
+ }
15256
+ ) })
15737
15257
  }
15738
15258
  );
15739
15259
  }
@@ -15806,23 +15326,50 @@ let ScopedStore = _ScopedStore;
15806
15326
  function createScopedStore(data) {
15807
15327
  return new ScopedStore(data);
15808
15328
  }
15809
- function createLazyCreatorState(state) {
15329
+ function createLazyCreatorState(state, store) {
15810
15330
  let _uniforms = null;
15811
15331
  let _nodes = null;
15332
+ let _buffers = null;
15333
+ let _gpuStorage = null;
15334
+ const view = (kind) => store ? withStagedOverlay(store, kind, state[kind]) : state[kind];
15812
15335
  return Object.create(state, {
15813
15336
  uniforms: {
15814
15337
  get() {
15815
- return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
15338
+ return _uniforms ?? (_uniforms = createScopedStore(view("uniforms")));
15816
15339
  }
15817
15340
  },
15818
15341
  nodes: {
15819
15342
  get() {
15820
- return _nodes ?? (_nodes = createScopedStore(state.nodes));
15343
+ return _nodes ?? (_nodes = createScopedStore(view("nodes")));
15344
+ }
15345
+ },
15346
+ buffers: {
15347
+ get() {
15348
+ return _buffers ?? (_buffers = createScopedStore(view("buffers")));
15349
+ }
15350
+ },
15351
+ gpuStorage: {
15352
+ get() {
15353
+ return _gpuStorage ?? (_gpuStorage = createScopedStore(view("gpuStorage")));
15821
15354
  }
15822
15355
  }
15823
15356
  });
15824
15357
  }
15825
15358
 
15359
+ function resolvePrimary(local) {
15360
+ return local.getState().primaryStore ?? local;
15361
+ }
15362
+ function usePrimaryStore() {
15363
+ const local = useStore();
15364
+ const primary = local.getState().primaryStore;
15365
+ return useMemo(() => primary ?? local, [primary, local]);
15366
+ }
15367
+ function usePrimaryThree(selector = (state) => state, equalityFn) {
15368
+ const local = useStore();
15369
+ const primary = resolvePrimary(local);
15370
+ return primary(selector, equalityFn);
15371
+ }
15372
+
15826
15373
  function addTexture(set, key, value) {
15827
15374
  set((state) => {
15828
15375
  const newMap = new Map(state.textures);
@@ -15901,6 +15448,10 @@ function vectorize(inObject) {
15901
15448
  if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
15902
15449
  if (obj.isMatrix3 || obj.isMatrix4) return inObject;
15903
15450
  if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
15451
+ if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
15452
+ const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
15453
+ return new Color(obj.r * scale, obj.g * scale, obj.b * scale);
15454
+ }
15904
15455
  if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
15905
15456
  if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
15906
15457
  return new Vector4(obj.x, obj.y, obj.z, obj.w);
@@ -15922,65 +15473,56 @@ function useCompareMemoize(value, deep) {
15922
15473
  return ref.current;
15923
15474
  }
15924
15475
 
15476
+ function useScopedResource(options) {
15477
+ const { store, kind, scope, isLeaf, create, prepare, reconcile, input } = options;
15478
+ const scopeKey = scope ?? ROOT_SCOPE;
15479
+ const version = store((s) => s._hmrVersion);
15480
+ const entries = useMemo(() => {
15481
+ const staged = peekStaged(store, kind, scopeKey);
15482
+ const reusable = isScopeValid(store, kind, scopeKey);
15483
+ const map = store.getState()[kind];
15484
+ const committed = reusable ? readCommittedScope(map, scopeKey, isLeaf) : void 0;
15485
+ const result = {};
15486
+ let fresh = null;
15487
+ for (const [name, candidate] of Object.entries(create())) {
15488
+ const existing = staged?.[name] ?? committed?.[name];
15489
+ if (existing !== void 0) {
15490
+ result[name] = existing;
15491
+ reconcile?.(existing, candidate);
15492
+ } else {
15493
+ const value = prepare ? prepare(name, candidate) : candidate;
15494
+ result[name] = value;
15495
+ (fresh ?? (fresh = {}))[name] = value;
15496
+ }
15497
+ }
15498
+ if (fresh) stageEntries(store, kind, scopeKey, fresh);
15499
+ return result;
15500
+ }, [store, kind, scopeKey, version, input]);
15501
+ useIsomorphicLayoutEffect(() => {
15502
+ flushStagedScope(store, kind, scopeKey, isLeaf);
15503
+ });
15504
+ return entries;
15505
+ }
15506
+
15925
15507
  const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
15926
15508
  function useUniforms(creatorOrScope, scope) {
15927
- const store = useStore();
15509
+ const store = usePrimaryStore();
15928
15510
  const removeUniforms2 = useCallback(
15929
- (names, targetScope) => {
15930
- const nameArray = Array.isArray(names) ? names : [names];
15931
- store.setState((state) => {
15932
- if (targetScope) {
15933
- const currentScope = { ...state.uniforms[targetScope] };
15934
- for (const name of nameArray) delete currentScope[name];
15935
- return { uniforms: { ...state.uniforms, [targetScope]: currentScope } };
15936
- }
15937
- const uniforms2 = { ...state.uniforms };
15938
- for (const name of nameArray) if (isUniformNode$1(uniforms2[name])) delete uniforms2[name];
15939
- return { uniforms: uniforms2 };
15940
- });
15941
- },
15511
+ (names, targetScope) => removeResourceEntries(store, "uniforms", Array.isArray(names) ? names : [names], targetScope, isUniformNode$1),
15942
15512
  [store]
15943
15513
  );
15944
15514
  const clearUniforms = useCallback(
15945
- (targetScope) => {
15946
- store.setState((state) => {
15947
- if (targetScope && targetScope !== "root") {
15948
- const { [targetScope]: _, ...rest } = state.uniforms;
15949
- return { uniforms: rest };
15950
- }
15951
- if (targetScope === "root") {
15952
- const uniforms2 = {};
15953
- for (const [key, value] of Object.entries(state.uniforms)) {
15954
- if (!isUniformNode$1(value)) uniforms2[key] = value;
15955
- }
15956
- return { uniforms: uniforms2 };
15957
- }
15958
- return { uniforms: {} };
15959
- });
15960
- },
15515
+ (targetScope) => clearResourceEntries(store, "uniforms", targetScope, isUniformNode$1),
15961
15516
  [store]
15962
15517
  );
15963
15518
  const rebuildUniforms = useCallback(
15964
- (targetScope) => {
15965
- store.setState((state) => {
15966
- let newUniforms = {};
15967
- if (targetScope && targetScope !== "root") {
15968
- const { [targetScope]: _, ...rest } = state.uniforms;
15969
- newUniforms = rest;
15970
- } else if (targetScope === "root") {
15971
- for (const [key, value] of Object.entries(state.uniforms)) {
15972
- if (!isUniformNode$1(value)) newUniforms[key] = value;
15973
- }
15974
- }
15975
- return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
15976
- });
15977
- },
15519
+ (targetScope) => rebuildResource(store, "uniforms", targetScope),
15978
15520
  [store]
15979
15521
  );
15980
15522
  const inputForMemoization = useMemo(() => {
15981
15523
  let raw = creatorOrScope;
15982
15524
  if (is.fun(creatorOrScope)) {
15983
- const wrappedState = createLazyCreatorState(store.getState());
15525
+ const wrappedState = createLazyCreatorState(store.getState(), store);
15984
15526
  raw = creatorOrScope(wrappedState);
15985
15527
  }
15986
15528
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
@@ -15994,80 +15536,44 @@ function useUniforms(creatorOrScope, scope) {
15994
15536
  }, [creatorOrScope, store]);
15995
15537
  const memoizedInput = useCompareMemoize(inputForMemoization);
15996
15538
  const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
15997
- const storeUniforms = useThree((s) => s.uniforms);
15998
- const hmrVersion = useThree((s) => s._hmrVersion);
15999
- const readerDep = isReader ? storeUniforms : null;
16000
- const creatorDep = isReader ? null : hmrVersion;
16001
- const uniforms = useMemo(() => {
16002
- if (memoizedInput === void 0) {
16003
- return storeUniforms;
16004
- }
16005
- if (typeof memoizedInput === "string") {
16006
- const scopeData = storeUniforms[memoizedInput];
16007
- if (scopeData && !isUniformNode$1(scopeData)) return scopeData;
16008
- return {};
16009
- }
16010
- const state = store.getState();
16011
- const set = store.setState;
16012
- if (typeof memoizedInput !== "object" || memoizedInput === null) {
16013
- throw new Error("Invalid uniform input");
16014
- }
16015
- const created = memoizedInput;
16016
- const result = {};
16017
- let hasNewUniforms = false;
16018
- let targetRecord = state.uniforms;
16019
- if (scope) {
16020
- if (!state.uniforms[scope]) state.uniforms[scope] = {};
16021
- targetRecord = state.uniforms[scope];
16022
- }
16023
- for (const [name, node] of Object.entries(created)) {
16024
- if (targetRecord[name]) {
16025
- result[name] = targetRecord[name];
16026
- const existingVal = result[name].value;
16027
- const newVal = vectorize(node);
16028
- let equals = newVal === existingVal;
16029
- if (!equals && hasEqualsMethod(existingVal) && hasEqualsMethod(newVal)) {
16030
- if (isThreeVector(existingVal) && isThreeVector(newVal)) {
16031
- equals = vectorEquals(existingVal, newVal);
16032
- } else if (isSameThreeType(existingVal, newVal)) {
16033
- equals = existingVal.equals(newVal);
16034
- }
15539
+ const storeUniforms = usePrimaryThree((s) => s.uniforms);
15540
+ const created = useScopedResource({
15541
+ store,
15542
+ kind: "uniforms",
15543
+ scope: isReader ? void 0 : scope,
15544
+ isLeaf: isUniformNode$1,
15545
+ input: memoizedInput,
15546
+ create: () => {
15547
+ if (isReader) return {};
15548
+ if (typeof memoizedInput !== "object" || memoizedInput === null) throw new Error("Invalid uniform input");
15549
+ return memoizedInput;
15550
+ },
15551
+ prepare: (name, candidate) => createUniform(name, candidate, scope),
15552
+ reconcile: (existing, candidate) => {
15553
+ const existingVal = existing.value;
15554
+ const newVal = vectorize(candidate);
15555
+ let equals = newVal === existingVal;
15556
+ if (!equals && hasEqualsMethod(existingVal) && hasEqualsMethod(newVal)) {
15557
+ if (isThreeVector(existingVal) && isThreeVector(newVal)) {
15558
+ equals = vectorEquals(existingVal, newVal);
15559
+ } else if (isSameThreeType(existingVal, newVal)) {
15560
+ equals = existingVal.equals(newVal);
16035
15561
  }
16036
- if (!equals) result[name].value = newVal;
16037
- } else {
16038
- result[name] = createUniform(name, node, scope);
16039
- hasNewUniforms = true;
16040
15562
  }
15563
+ if (!equals) existing.value = newVal;
16041
15564
  }
16042
- if (hasNewUniforms) {
16043
- if (scope) {
16044
- set((s) => ({
16045
- uniforms: {
16046
- ...s.uniforms,
16047
- [scope]: { ...s.uniforms[scope], ...result }
16048
- }
16049
- }));
16050
- } else {
16051
- set((s) => ({ uniforms: { ...s.uniforms, ...result } }));
16052
- }
16053
- }
16054
- return result;
16055
- }, [store, memoizedInput, scope, readerDep, creatorDep]);
15565
+ });
15566
+ let uniforms = created;
15567
+ if (memoizedInput === void 0) {
15568
+ uniforms = storeUniforms;
15569
+ } else if (typeof memoizedInput === "string") {
15570
+ const scopeData = storeUniforms[memoizedInput];
15571
+ uniforms = scopeData && !isUniformNode$1(scopeData) ? scopeData : {};
15572
+ }
16056
15573
  return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
16057
15574
  }
16058
15575
  function rebuildAllUniforms(store, scope) {
16059
- store.setState((state) => {
16060
- let newUniforms = {};
16061
- if (scope && scope !== "root") {
16062
- const { [scope]: _, ...rest } = state.uniforms;
16063
- newUniforms = rest;
16064
- } else if (scope === "root") {
16065
- for (const [key, value] of Object.entries(state.uniforms)) {
16066
- if (!isUniformNode$1(value)) newUniforms[key] = value;
16067
- }
16068
- }
16069
- return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
16070
- });
15576
+ rebuildResource(store, "uniforms", scope);
16071
15577
  }
16072
15578
  function removeUniforms(set, names, scope) {
16073
15579
  set((state) => {
@@ -16131,191 +15637,91 @@ function isSameThreeType(a, b) {
16131
15637
  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;
16132
15638
  }
16133
15639
 
15640
+ const uniformOf = uniform;
16134
15641
  const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
16135
15642
  const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
16136
15643
  function useUniform(name, value) {
16137
15644
  const store = useStore();
16138
- const hmrVersion = useThree((s) => s._hmrVersion);
16139
- return useMemo(() => {
16140
- const state = store.getState();
16141
- const set = store.setState;
16142
- const existing = state.uniforms[name];
16143
- if (existing && isUniformNode(existing)) {
15645
+ const registered = useScopedResource({
15646
+ store,
15647
+ kind: "uniforms",
15648
+ scope: void 0,
15649
+ isLeaf: isUniformNode,
15650
+ input: name,
15651
+ create: () => value === void 0 ? {} : { [name]: value },
15652
+ prepare: createNamedUniform,
15653
+ reconcile: (existing2) => {
16144
15654
  if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
16145
- existing.value = typeof value === "string" ? new Color(value) : value;
15655
+ existing2.value = typeof value === "string" ? new Color(value) : value;
16146
15656
  }
16147
- return existing;
16148
- }
16149
- if (value === void 0) {
16150
- throw new Error(
16151
- `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
16152
- );
16153
- }
16154
- if (isUniformNode(value)) {
16155
- const node2 = value;
16156
- if (typeof node2.setName === "function") {
16157
- node2.setName(name);
16158
- }
16159
- set((s) => ({
16160
- uniforms: { ...s.uniforms, [name]: node2 }
16161
- }));
16162
- return node2;
16163
15657
  }
16164
- let node;
16165
- if (isTSLNode$1(value)) {
16166
- node = uniform(value);
16167
- } else if (typeof value === "string") {
16168
- node = uniform(new Color(value));
16169
- } else {
16170
- node = uniform(value);
16171
- }
16172
- if (typeof node.setName === "function") {
16173
- node.setName(name);
16174
- }
16175
- set((s) => ({
16176
- uniforms: {
16177
- ...s.uniforms,
16178
- [name]: node
16179
- }
16180
- }));
16181
- return node;
16182
- }, [store, name, hmrVersion]);
15658
+ });
15659
+ if (registered[name]) return registered[name];
15660
+ const existing = peekStaged(store, "uniforms", ROOT_SCOPE)?.[name] ?? store.getState().uniforms[name];
15661
+ if (existing && isUniformNode(existing)) return existing;
15662
+ throw new Error(
15663
+ `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
15664
+ );
15665
+ }
15666
+ function createNamedUniform(name, value) {
15667
+ let node;
15668
+ if (isUniformNode(value)) {
15669
+ node = value;
15670
+ } else if (isTSLNode$1(value)) {
15671
+ node = uniformOf(value);
15672
+ } else if (typeof value === "string") {
15673
+ node = uniform(new Color(value));
15674
+ } else {
15675
+ node = uniformOf(value);
15676
+ }
15677
+ if (typeof node.setName === "function") {
15678
+ node.setName(name);
15679
+ }
15680
+ return node;
16183
15681
  }
16184
15682
 
16185
15683
  const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
16186
15684
  function useNodes(creatorOrScope, scope) {
16187
- const store = useStore();
15685
+ const store = usePrimaryStore();
16188
15686
  const removeNodes2 = useCallback(
16189
- (names, targetScope) => {
16190
- const nameArray = Array.isArray(names) ? names : [names];
16191
- store.setState((state) => {
16192
- if (targetScope) {
16193
- const currentScope = { ...state.nodes[targetScope] };
16194
- for (const name of nameArray) delete currentScope[name];
16195
- return { nodes: { ...state.nodes, [targetScope]: currentScope } };
16196
- }
16197
- const nodes2 = { ...state.nodes };
16198
- for (const name of nameArray) if (isTSLNode(nodes2[name])) delete nodes2[name];
16199
- return { nodes: nodes2 };
16200
- });
16201
- },
15687
+ (names, targetScope) => removeResourceEntries(store, "nodes", Array.isArray(names) ? names : [names], targetScope, isTSLNode),
16202
15688
  [store]
16203
15689
  );
16204
15690
  const clearNodes = useCallback(
16205
- (targetScope) => {
16206
- store.setState((state) => {
16207
- if (targetScope && targetScope !== "root") {
16208
- const { [targetScope]: _, ...rest } = state.nodes;
16209
- return { nodes: rest };
16210
- }
16211
- if (targetScope === "root") {
16212
- const nodes2 = {};
16213
- for (const [key, value] of Object.entries(state.nodes)) {
16214
- if (!isTSLNode(value)) nodes2[key] = value;
16215
- }
16216
- return { nodes: nodes2 };
16217
- }
16218
- return { nodes: {} };
16219
- });
16220
- },
15691
+ (targetScope) => clearResourceEntries(store, "nodes", targetScope, isTSLNode),
16221
15692
  [store]
16222
15693
  );
16223
15694
  const rebuildNodes = useCallback(
16224
- (targetScope) => {
16225
- store.setState((state) => {
16226
- let newNodes = state.nodes;
16227
- if (targetScope && targetScope !== "root") {
16228
- const { [targetScope]: _, ...rest } = state.nodes;
16229
- newNodes = rest;
16230
- } else if (targetScope === "root") {
16231
- newNodes = {};
16232
- for (const [key, value] of Object.entries(state.nodes)) {
16233
- if (!isTSLNode(value)) newNodes[key] = value;
16234
- }
16235
- } else {
16236
- newNodes = {};
16237
- }
16238
- return { nodes: newNodes, _hmrVersion: state._hmrVersion + 1 };
16239
- });
16240
- },
15695
+ (targetScope) => rebuildResource(store, "nodes", targetScope),
16241
15696
  [store]
16242
15697
  );
16243
15698
  const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
16244
- const storeNodes = useThree((s) => s.nodes);
16245
- const hmrVersion = useThree((s) => s._hmrVersion);
16246
- const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
16247
- const readerDep = isReader ? storeNodes : null;
16248
- const creatorDep = isReader ? null : hmrVersion;
16249
- const nodes = useMemo(() => {
16250
- if (creatorOrScope === void 0) {
16251
- return storeNodes;
16252
- }
16253
- if (typeof creatorOrScope === "string") {
16254
- const scopeData = storeNodes[creatorOrScope];
16255
- if (scopeData && !isTSLNode(scopeData)) return scopeData;
16256
- return {};
16257
- }
16258
- const state = store.getState();
16259
- const set = store.setState;
16260
- const creator = creatorOrScope;
16261
- const wrappedState = createLazyCreatorState(state);
16262
- const created = creator(wrappedState);
16263
- const result = {};
16264
- let hasNewNodes = false;
16265
- if (scope) {
16266
- const currentScope = state.nodes[scope] ?? {};
16267
- for (const [name, node] of Object.entries(created)) {
16268
- if (currentScope[name]) {
16269
- result[name] = currentScope[name];
16270
- } else {
16271
- node.setName?.(`${scope}.${name}`);
16272
- result[name] = node;
16273
- hasNewNodes = true;
16274
- }
16275
- }
16276
- if (hasNewNodes) {
16277
- set((s) => ({
16278
- nodes: {
16279
- ...s.nodes,
16280
- [scope]: { ...s.nodes[scope], ...result }
16281
- }
16282
- }));
16283
- }
16284
- return result;
16285
- }
16286
- for (const [name, node] of Object.entries(created)) {
16287
- const existing = state.nodes[name];
16288
- if (existing && isTSLNode(existing)) {
16289
- result[name] = existing;
16290
- } else {
16291
- node.setName?.(name);
16292
- result[name] = node;
16293
- hasNewNodes = true;
16294
- }
16295
- }
16296
- if (hasNewNodes) {
16297
- set((s) => ({ nodes: { ...s.nodes, ...result } }));
15699
+ const storeNodes = usePrimaryThree((s) => s.nodes);
15700
+ const created = useScopedResource({
15701
+ store,
15702
+ kind: "nodes",
15703
+ scope: isReader ? void 0 : scope,
15704
+ isLeaf: isTSLNode,
15705
+ create: () => {
15706
+ if (isReader) return {};
15707
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15708
+ },
15709
+ prepare: (name, node) => {
15710
+ node.setName?.(scope ? `${scope}.${name}` : name);
15711
+ return node;
16298
15712
  }
16299
- return result;
16300
- }, [store, scopeDep, readerDep, creatorDep]);
15713
+ });
15714
+ let nodes = created;
15715
+ if (creatorOrScope === void 0) {
15716
+ nodes = storeNodes;
15717
+ } else if (typeof creatorOrScope === "string") {
15718
+ const scopeData = storeNodes[creatorOrScope];
15719
+ nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
15720
+ }
16301
15721
  return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
16302
15722
  }
16303
15723
  function rebuildAllNodes(store, scope) {
16304
- store.setState((state) => {
16305
- let newNodes = state.nodes;
16306
- if (scope && scope !== "root") {
16307
- const { [scope]: _, ...rest } = state.nodes;
16308
- newNodes = rest;
16309
- } else if (scope === "root") {
16310
- newNodes = {};
16311
- for (const [key, value] of Object.entries(state.nodes)) {
16312
- if (!isTSLNode(value)) newNodes[key] = value;
16313
- }
16314
- } else {
16315
- newNodes = {};
16316
- }
16317
- return { nodes: newNodes, _hmrVersion: state._hmrVersion + 1 };
16318
- });
15724
+ rebuildResource(store, "nodes", scope);
16319
15725
  }
16320
15726
  function removeNodes(set, names, scope) {
16321
15727
  set((state) => {
@@ -16345,18 +15751,172 @@ function clearRootNodes(set) {
16345
15751
  });
16346
15752
  }
16347
15753
  function useLocalNodes(creator) {
16348
- const store = useStore();
16349
- const uniforms = useThree((s) => s.uniforms);
16350
- const nodes = useThree((s) => s.nodes);
16351
- const textures = useThree((s) => s.textures);
16352
- const hmrVersion = useThree((s) => s._hmrVersion);
15754
+ const store = usePrimaryStore();
15755
+ const uniforms = usePrimaryThree((s) => s.uniforms);
15756
+ const nodes = usePrimaryThree((s) => s.nodes);
15757
+ const textures = usePrimaryThree((s) => s.textures);
15758
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
16353
15759
  return useMemo(() => {
16354
- const wrappedState = createLazyCreatorState(store.getState());
15760
+ const wrappedState = createLazyCreatorState(store.getState(), store);
16355
15761
  return creator(wrappedState);
16356
15762
  }, [store, creator, uniforms, nodes, textures, hmrVersion]);
16357
15763
  }
16358
15764
 
16359
- function usePostProcessing(mainCB, setupCB) {
15765
+ const isBufferLike = (value) => {
15766
+ if (value === null || typeof value !== "object") return false;
15767
+ if (ArrayBuffer.isView(value)) return true;
15768
+ if ("isBufferAttribute" in value) return true;
15769
+ if ("uuid" in value || "nodeType" in value) return true;
15770
+ return false;
15771
+ };
15772
+ const disposeBuffer = (buffer) => {
15773
+ if (buffer === null || typeof buffer !== "object") return;
15774
+ if ("dispose" in buffer && typeof buffer.dispose === "function") {
15775
+ buffer.dispose();
15776
+ }
15777
+ };
15778
+ function useBuffers(creatorOrScope, scope) {
15779
+ const store = usePrimaryStore();
15780
+ const removeBuffers = useCallback(
15781
+ (names, targetScope) => removeResourceEntries(store, "buffers", Array.isArray(names) ? names : [names], targetScope, isBufferLike),
15782
+ [store]
15783
+ );
15784
+ const clearBuffers = useCallback(
15785
+ (targetScope) => clearResourceEntries(store, "buffers", targetScope, isBufferLike),
15786
+ [store]
15787
+ );
15788
+ const rebuildBuffers = useCallback(
15789
+ (targetScope) => rebuildResource(store, "buffers", targetScope),
15790
+ [store]
15791
+ );
15792
+ const disposeBuffers = useCallback(
15793
+ (names, targetScope) => {
15794
+ const nameArray = Array.isArray(names) ? names : [names];
15795
+ const state = store.getState();
15796
+ for (const name of nameArray) {
15797
+ const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
15798
+ if (buffer && isBufferLike(buffer)) {
15799
+ disposeBuffer(buffer);
15800
+ }
15801
+ }
15802
+ removeBuffers(names, targetScope);
15803
+ },
15804
+ [store, removeBuffers]
15805
+ );
15806
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15807
+ const storeBuffers = usePrimaryThree((s) => s.buffers);
15808
+ const created = useScopedResource({
15809
+ store,
15810
+ kind: "buffers",
15811
+ scope: isReader ? void 0 : scope,
15812
+ isLeaf: isBufferLike,
15813
+ create: () => {
15814
+ if (isReader) return {};
15815
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15816
+ },
15817
+ prepare: (name, buffer) => {
15818
+ if ("setName" in buffer && typeof buffer.setName === "function") {
15819
+ buffer.setName(scope ? `${scope}.${name}` : name);
15820
+ }
15821
+ return buffer;
15822
+ }
15823
+ });
15824
+ let buffers = created;
15825
+ if (creatorOrScope === void 0) {
15826
+ buffers = storeBuffers;
15827
+ } else if (typeof creatorOrScope === "string") {
15828
+ const scopeData = storeBuffers[creatorOrScope];
15829
+ buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
15830
+ }
15831
+ return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
15832
+ }
15833
+ function rebuildAllBuffers(store, scope) {
15834
+ rebuildResource(store, "buffers", scope);
15835
+ }
15836
+
15837
+ const isStorageLike = (value) => {
15838
+ if (value === null || typeof value !== "object") return false;
15839
+ if ("isTexture" in value) return true;
15840
+ if ("isData3DTexture" in value) return true;
15841
+ if ("uuid" in value || "nodeType" in value) return true;
15842
+ return false;
15843
+ };
15844
+ const disposeStorage = (storage) => {
15845
+ if (storage === null || typeof storage !== "object") return;
15846
+ if ("dispose" in storage && typeof storage.dispose === "function") {
15847
+ storage.dispose();
15848
+ }
15849
+ };
15850
+ function useGPUStorage(creatorOrScope, scope) {
15851
+ const store = usePrimaryStore();
15852
+ const removeStorage = useCallback(
15853
+ (names, targetScope) => removeResourceEntries(store, "gpuStorage", Array.isArray(names) ? names : [names], targetScope, isStorageLike),
15854
+ [store]
15855
+ );
15856
+ const clearStorage = useCallback(
15857
+ (targetScope) => clearResourceEntries(store, "gpuStorage", targetScope, isStorageLike),
15858
+ [store]
15859
+ );
15860
+ const rebuildStorage = useCallback(
15861
+ (targetScope) => rebuildResource(store, "gpuStorage", targetScope),
15862
+ [store]
15863
+ );
15864
+ const disposeStorageFn = useCallback(
15865
+ (names, targetScope) => {
15866
+ const nameArray = Array.isArray(names) ? names : [names];
15867
+ const state = store.getState();
15868
+ for (const name of nameArray) {
15869
+ const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
15870
+ if (storage && isStorageLike(storage)) {
15871
+ disposeStorage(storage);
15872
+ }
15873
+ }
15874
+ removeStorage(names, targetScope);
15875
+ },
15876
+ [store, removeStorage]
15877
+ );
15878
+ const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
15879
+ const storeStorage = usePrimaryThree((s) => s.gpuStorage);
15880
+ const created = useScopedResource({
15881
+ store,
15882
+ kind: "gpuStorage",
15883
+ scope: isReader ? void 0 : scope,
15884
+ isLeaf: isStorageLike,
15885
+ create: () => {
15886
+ if (isReader) return {};
15887
+ return creatorOrScope(createLazyCreatorState(store.getState(), store));
15888
+ },
15889
+ prepare: (name, storage) => {
15890
+ const label = scope ? `${scope}.${name}` : name;
15891
+ if ("setName" in storage && typeof storage.setName === "function") {
15892
+ storage.setName(label);
15893
+ }
15894
+ if ("name" in storage && typeof storage.name === "string") {
15895
+ storage.name = label;
15896
+ }
15897
+ return storage;
15898
+ }
15899
+ });
15900
+ let gpuStorage = created;
15901
+ if (creatorOrScope === void 0) {
15902
+ gpuStorage = storeStorage;
15903
+ } else if (typeof creatorOrScope === "string") {
15904
+ const scopeData = storeStorage[creatorOrScope];
15905
+ gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
15906
+ }
15907
+ return {
15908
+ ...gpuStorage,
15909
+ removeStorage,
15910
+ clearStorage,
15911
+ rebuildStorage,
15912
+ disposeStorage: disposeStorageFn
15913
+ };
15914
+ }
15915
+ function rebuildAllStorage(store, scope) {
15916
+ rebuildResource(store, "gpuStorage", scope);
15917
+ }
15918
+
15919
+ function useRenderPipeline(mainCB, setupCB) {
16360
15920
  const store = useStore();
16361
15921
  const { scene, camera, renderer, isLegacy } = useThree();
16362
15922
  const callbacksRanRef = useRef(false);
@@ -16375,7 +15935,7 @@ function usePostProcessing(mainCB, setupCB) {
16375
15935
  }, [store]);
16376
15936
  const reset = useCallback(() => {
16377
15937
  store.setState({
16378
- postProcessing: null,
15938
+ renderPipeline: null,
16379
15939
  passes: {}
16380
15940
  });
16381
15941
  callbacksRanRef.current = false;
@@ -16388,18 +15948,18 @@ function usePostProcessing(mainCB, setupCB) {
16388
15948
  }, []);
16389
15949
  useLayoutEffect(() => {
16390
15950
  if (isLegacy) {
16391
- throw new Error("usePostProcessing is only available with WebGPU renderer. Set renderer prop on Canvas.");
15951
+ throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
16392
15952
  }
16393
15953
  if (!renderer || !scene || !camera) return;
16394
15954
  const state = store.getState();
16395
15955
  const set = store.setState;
16396
15956
  try {
16397
- let pp = state.postProcessing;
15957
+ let pipeline = state.renderPipeline;
16398
15958
  let currentPasses = { ...state.passes };
16399
- let justCreatedPP = false;
16400
- if (!pp) {
16401
- pp = new PostProcessing(renderer);
16402
- justCreatedPP = true;
15959
+ let justCreatedPipeline = false;
15960
+ if (!pipeline) {
15961
+ pipeline = new RenderPipeline(renderer);
15962
+ justCreatedPipeline = true;
16403
15963
  }
16404
15964
  const cacheValid = scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
16405
15965
  let scenePass;
@@ -16410,9 +15970,9 @@ function usePostProcessing(mainCB, setupCB) {
16410
15970
  scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
16411
15971
  }
16412
15972
  currentPasses.scenePass = scenePass;
16413
- if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
16414
- set({ postProcessing: pp, passes: currentPasses });
16415
- const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
15973
+ if (!pipeline.outputNode || justCreatedPipeline) pipeline.outputNode = scenePass;
15974
+ set({ renderPipeline: pipeline, passes: currentPasses });
15975
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
16416
15976
  if (shouldRunCallbacks) {
16417
15977
  if (setupCBRef.current) {
16418
15978
  const freshState = store.getState();
@@ -16433,22 +15993,22 @@ function usePostProcessing(mainCB, setupCB) {
16433
15993
  callbacksRanRef.current = true;
16434
15994
  }
16435
15995
  } catch (error) {
16436
- console.error("[usePostProcessing] Setup error:", error);
15996
+ console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
16437
15997
  }
16438
15998
  }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
16439
15999
  const passes = useThree((s) => s.passes);
16440
- const postProcessing = useThree((s) => s.postProcessing);
16000
+ const renderPipeline = useThree((s) => s.renderPipeline);
16441
16001
  return {
16442
16002
  passes,
16443
- postProcessing,
16003
+ renderPipeline,
16444
16004
  clearPasses,
16445
16005
  reset,
16446
16006
  rebuild,
16447
- // isReady indicates if PostProcessing is configured and ready for rendering
16448
- isReady: postProcessing !== null
16007
+ // isReady indicates if RenderPipeline is configured and ready for rendering
16008
+ isReady: renderPipeline !== null
16449
16009
  };
16450
16010
  }
16451
16011
 
16452
16012
  extend(THREE);
16453
16013
 
16454
- export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, clearNodeScope, clearRootNodes, clearRootUniforms, clearScope, context, createEvents, createPointerEvents, createPortal, createRoot, createScopedStore, createStore, createTextureOperations, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getScheduler, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, rebuildAllNodes, rebuildAllUniforms, reconciler, registerPrimary, removeInteractivity, removeNodes, removeUniforms, resolve, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useEnvironment, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useLocalNodes, useMutableCallback, useNodes, usePostProcessing, useRenderTarget, useStore, useTexture, useTextures, useThree, useUniform, useUniforms, waitForPrimary };
16014
+ export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, clearNodeScope, clearRootNodes, clearRootUniforms, clearScope, context, createEvents, createPointerEvents, createPortal, createRoot, createScopedStore, createStore, createTextureOperations, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, rebuildAllBuffers, rebuildAllNodes, rebuildAllStorage, rebuildAllUniforms, reconciler, registerPrimary, removeInteractivity, removeNodes, removeUniforms, resolve, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useBuffers, useEnvironment, useFrame, useGPUStorage, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useLocalNodes, useMutableCallback, useNodes, useRenderPipeline, useRenderTarget, useStore, useTexture, useTextures, useThree, useUniform, useUniforms, waitForPrimary };