@react-three/fiber 10.0.0-canary.b0fafc8 → 10.0.0-canary.b6c54f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +714 -1383
- package/dist/index.d.cts +291 -806
- package/dist/index.d.mts +291 -806
- package/dist/index.d.ts +291 -806
- package/dist/index.mjs +689 -1357
- package/dist/legacy.cjs +669 -1344
- package/dist/legacy.d.cts +292 -806
- package/dist/legacy.d.mts +292 -806
- package/dist/legacy.d.ts +292 -806
- package/dist/legacy.mjs +643 -1317
- package/dist/webgpu/index.cjs +1170 -1698
- package/dist/webgpu/index.d.cts +407 -820
- package/dist/webgpu/index.d.mts +407 -820
- package/dist/webgpu/index.d.ts +407 -820
- package/dist/webgpu/index.mjs +1139 -1670
- package/package.json +4 -3
- package/readme.md +2 -3
package/dist/webgpu/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as webgpu from 'three/webgpu';
|
|
2
|
-
import { RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene,
|
|
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';
|
|
@@ -14,6 +14,8 @@ import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
|
|
|
14
14
|
import { GainMapLoader } from '@monogrid/gainmap-js';
|
|
15
15
|
import Tb, { unstable_scheduleCallback, unstable_IdlePriority } 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
|
|
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 =
|
|
341
|
-
|
|
342
|
-
camera.current.update(
|
|
343
|
-
|
|
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
|
-
}, [
|
|
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 =
|
|
357
|
-
|
|
358
|
-
camera.current.update(
|
|
359
|
-
|
|
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,9 +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$
|
|
424
|
-
var __defNormalProp$
|
|
425
|
-
var __publicField$
|
|
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);
|
|
426
447
|
const act = React["act"];
|
|
427
448
|
const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React.useLayoutEffect : React.useEffect;
|
|
428
449
|
function useMutableCallback(fn) {
|
|
@@ -454,7 +475,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
|
|
|
454
475
|
return _a = class extends React.Component {
|
|
455
476
|
constructor() {
|
|
456
477
|
super(...arguments);
|
|
457
|
-
__publicField$
|
|
478
|
+
__publicField$1(this, "state", { error: false });
|
|
458
479
|
}
|
|
459
480
|
componentDidCatch(err) {
|
|
460
481
|
this.props.set(err);
|
|
@@ -462,7 +483,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
|
|
|
462
483
|
render() {
|
|
463
484
|
return this.state.error ? null : this.props.children;
|
|
464
485
|
}
|
|
465
|
-
}, __publicField$
|
|
486
|
+
}, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
|
|
466
487
|
})();
|
|
467
488
|
|
|
468
489
|
const is = {
|
|
@@ -599,8 +620,7 @@ function prepare(target, root, type, props) {
|
|
|
599
620
|
object,
|
|
600
621
|
eventCount: 0,
|
|
601
622
|
handlers: {},
|
|
602
|
-
isHidden: false
|
|
603
|
-
deferredRefs: []
|
|
623
|
+
isHidden: false
|
|
604
624
|
};
|
|
605
625
|
if (object) object.__r3f = instance;
|
|
606
626
|
}
|
|
@@ -757,6 +777,17 @@ function unregisterVisibility(store, object) {
|
|
|
757
777
|
internal.visibilityRegistry.delete(object.uuid);
|
|
758
778
|
internal.occlusionCache.delete(object);
|
|
759
779
|
}
|
|
780
|
+
function updateVisibilityHandlers(store, object, handlers) {
|
|
781
|
+
const { internal } = store.getState();
|
|
782
|
+
const entry = internal.visibilityRegistry.get(object.uuid);
|
|
783
|
+
if (!entry) return false;
|
|
784
|
+
entry.handlers = handlers;
|
|
785
|
+
if ((handlers.onOccluded || handlers.onVisible) && !internal.occlusionEnabled) {
|
|
786
|
+
object.occlusionTest = true;
|
|
787
|
+
enableOcclusion(store);
|
|
788
|
+
}
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
760
791
|
function checkVisibility(state) {
|
|
761
792
|
const { internal, camera } = state;
|
|
762
793
|
const registry = internal.visibilityRegistry;
|
|
@@ -944,10 +975,7 @@ function applyProps(object, props) {
|
|
|
944
975
|
continue;
|
|
945
976
|
}
|
|
946
977
|
if (value === void 0) continue;
|
|
947
|
-
if (isFromRef(value))
|
|
948
|
-
instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
978
|
+
if (isFromRef(value)) continue;
|
|
951
979
|
if (isOnce(value)) {
|
|
952
980
|
if (instance?.appliedOnce?.has(prop)) continue;
|
|
953
981
|
if (instance) {
|
|
@@ -983,8 +1011,24 @@ function applyProps(object, props) {
|
|
|
983
1011
|
} else if (isVectorLike(target) && is.num(value)) {
|
|
984
1012
|
if ("setScalar" in target && typeof target.setScalar === "function") target.setScalar(value);
|
|
985
1013
|
else target.set(value);
|
|
1014
|
+
} else if (root instanceof ShaderMaterial && key === "uniforms" && is.obj(value)) {
|
|
1015
|
+
if (!is.obj(root.uniforms)) root.uniforms = {};
|
|
1016
|
+
const uniforms = root.uniforms;
|
|
1017
|
+
const nextUniforms = value;
|
|
1018
|
+
for (const name in nextUniforms) {
|
|
1019
|
+
const uniform = nextUniforms[name];
|
|
1020
|
+
const targetUniform = uniforms[name];
|
|
1021
|
+
if (targetUniform) Object.assign(targetUniform, uniform);
|
|
1022
|
+
else {
|
|
1023
|
+
const nextUniform = uniform instanceof Uniform ? uniform.clone() : new Uniform(uniform.value);
|
|
1024
|
+
uniforms[name] = nextUniform;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
986
1027
|
} else {
|
|
987
1028
|
root[key] = value;
|
|
1029
|
+
if (key.endsWith("Node") && root.isMaterial) {
|
|
1030
|
+
root.needsUpdate = true;
|
|
1031
|
+
}
|
|
988
1032
|
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
1033
|
root[key].format === RGBAFormat && root[key].type === UnsignedByteType) {
|
|
990
1034
|
root[key].colorSpace = rootState.textureColorSpace;
|
|
@@ -998,6 +1042,9 @@ function applyProps(object, props) {
|
|
|
998
1042
|
if (instance.eventCount && object2.raycast !== null) {
|
|
999
1043
|
rootState.internal.interaction.push(object2);
|
|
1000
1044
|
}
|
|
1045
|
+
}
|
|
1046
|
+
if (instance?.parent && rootState?.internal && instance.object?.isObject3D) {
|
|
1047
|
+
const object2 = instance.object;
|
|
1001
1048
|
const root = findInitialRoot(instance);
|
|
1002
1049
|
const visibilityHandlers = {
|
|
1003
1050
|
onFramed: instance.handlers.onFramed,
|
|
@@ -1005,7 +1052,9 @@ function applyProps(object, props) {
|
|
|
1005
1052
|
onVisible: instance.handlers.onVisible
|
|
1006
1053
|
};
|
|
1007
1054
|
if (hasVisibilityHandlers(visibilityHandlers)) {
|
|
1008
|
-
|
|
1055
|
+
if (!updateVisibilityHandlers(root, object2, visibilityHandlers)) {
|
|
1056
|
+
registerVisibility(root, object2, visibilityHandlers);
|
|
1057
|
+
}
|
|
1009
1058
|
} else {
|
|
1010
1059
|
unregisterVisibility(root, object2);
|
|
1011
1060
|
}
|
|
@@ -1018,38 +1067,60 @@ function applyProps(object, props) {
|
|
|
1018
1067
|
return object;
|
|
1019
1068
|
}
|
|
1020
1069
|
|
|
1070
|
+
const DEFAULT_POINTER_ID = 0;
|
|
1071
|
+
const XR_POINTER_ID_START = 1e3;
|
|
1072
|
+
function getPointerState(internal, pointerId) {
|
|
1073
|
+
let state = internal.pointerMap.get(pointerId);
|
|
1074
|
+
if (!state) {
|
|
1075
|
+
state = {
|
|
1076
|
+
hovered: /* @__PURE__ */ new Map(),
|
|
1077
|
+
captured: /* @__PURE__ */ new Map(),
|
|
1078
|
+
initialClick: [0, 0],
|
|
1079
|
+
initialHits: []
|
|
1080
|
+
};
|
|
1081
|
+
internal.pointerMap.set(pointerId, state);
|
|
1082
|
+
}
|
|
1083
|
+
return state;
|
|
1084
|
+
}
|
|
1085
|
+
function getPointerId(event) {
|
|
1086
|
+
return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
|
|
1087
|
+
}
|
|
1021
1088
|
function makeId(event) {
|
|
1022
1089
|
return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
|
|
1023
1090
|
}
|
|
1024
|
-
function releaseInternalPointerCapture(
|
|
1025
|
-
const
|
|
1091
|
+
function releaseInternalPointerCapture(internal, obj, pointerId) {
|
|
1092
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1093
|
+
if (!pointerState) return;
|
|
1094
|
+
const captureData = pointerState.captured.get(obj);
|
|
1026
1095
|
if (captureData) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
capturedMap.delete(pointerId);
|
|
1030
|
-
captureData.target.releasePointerCapture(pointerId);
|
|
1031
|
-
}
|
|
1096
|
+
pointerState.captured.delete(obj);
|
|
1097
|
+
captureData.target.releasePointerCapture(pointerId);
|
|
1032
1098
|
}
|
|
1033
1099
|
}
|
|
1034
1100
|
function removeInteractivity(store, object) {
|
|
1035
1101
|
const { internal } = store.getState();
|
|
1036
1102
|
internal.interaction = internal.interaction.filter((o) => o !== object);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1103
|
+
for (const [pointerId, pointerState] of internal.pointerMap) {
|
|
1104
|
+
pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
|
|
1105
|
+
pointerState.hovered.forEach((value, key) => {
|
|
1106
|
+
if (value.eventObject === object || value.object === object) {
|
|
1107
|
+
pointerState.hovered.delete(key);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
if (pointerState.captured.has(object)) {
|
|
1111
|
+
releaseInternalPointerCapture(internal, object, pointerId);
|
|
1041
1112
|
}
|
|
1042
|
-
}
|
|
1043
|
-
internal.capturedMap.forEach((captures, pointerId) => {
|
|
1044
|
-
releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
|
|
1045
|
-
});
|
|
1113
|
+
}
|
|
1046
1114
|
unregisterVisibility(store, object);
|
|
1047
1115
|
}
|
|
1048
1116
|
function createEvents(store) {
|
|
1049
|
-
function calculateDistance(event) {
|
|
1117
|
+
function calculateDistance(event, pointerId) {
|
|
1050
1118
|
const { internal } = store.getState();
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1119
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1120
|
+
if (!pointerState) return 0;
|
|
1121
|
+
const [initialX, initialY] = pointerState.initialClick;
|
|
1122
|
+
const dx = event.offsetX - initialX;
|
|
1123
|
+
const dy = event.offsetY - initialY;
|
|
1053
1124
|
return Math.round(Math.sqrt(dx * dx + dy * dy));
|
|
1054
1125
|
}
|
|
1055
1126
|
function filterPointerEvents(objects) {
|
|
@@ -1085,6 +1156,15 @@ function createEvents(store) {
|
|
|
1085
1156
|
return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
|
|
1086
1157
|
}
|
|
1087
1158
|
let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
|
|
1159
|
+
const aInteractivePriority = a.object.userData?.interactivePriority;
|
|
1160
|
+
const bInteractivePriority = b.object.userData?.interactivePriority;
|
|
1161
|
+
if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
|
|
1162
|
+
if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
|
|
1163
|
+
if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
|
|
1164
|
+
if (aInteractivePriority !== bInteractivePriority) {
|
|
1165
|
+
return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1088
1168
|
const aState = getRootState(a.object);
|
|
1089
1169
|
const bState = getRootState(b.object);
|
|
1090
1170
|
const aPriority = aState?.events?.priority ?? 1;
|
|
@@ -1106,9 +1186,13 @@ function createEvents(store) {
|
|
|
1106
1186
|
eventObject = eventObject.parent;
|
|
1107
1187
|
}
|
|
1108
1188
|
}
|
|
1109
|
-
if ("pointerId" in event
|
|
1110
|
-
|
|
1111
|
-
|
|
1189
|
+
if ("pointerId" in event) {
|
|
1190
|
+
const pointerId = event.pointerId;
|
|
1191
|
+
const pointerState = state.internal.pointerMap.get(pointerId);
|
|
1192
|
+
if (pointerState?.captured.size) {
|
|
1193
|
+
for (const captureData of pointerState.captured.values()) {
|
|
1194
|
+
if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
|
|
1195
|
+
}
|
|
1112
1196
|
}
|
|
1113
1197
|
}
|
|
1114
1198
|
return intersections;
|
|
@@ -1121,27 +1205,25 @@ function createEvents(store) {
|
|
|
1121
1205
|
if (state) {
|
|
1122
1206
|
const { raycaster, pointer, camera, internal } = state;
|
|
1123
1207
|
const unprojectedPoint = new Vector3(pointer.x, pointer.y, 0).unproject(camera);
|
|
1124
|
-
const hasPointerCapture = (id) =>
|
|
1208
|
+
const hasPointerCapture = (id) => {
|
|
1209
|
+
const pointerState = internal.pointerMap.get(id);
|
|
1210
|
+
return pointerState?.captured.has(hit.eventObject) ?? false;
|
|
1211
|
+
};
|
|
1125
1212
|
const setPointerCapture = (id) => {
|
|
1126
1213
|
const captureData = { intersection: hit, target: event.target };
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
} else {
|
|
1130
|
-
internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
|
|
1131
|
-
}
|
|
1214
|
+
const pointerState = getPointerState(internal, id);
|
|
1215
|
+
pointerState.captured.set(hit.eventObject, captureData);
|
|
1132
1216
|
event.target.setPointerCapture(id);
|
|
1133
1217
|
};
|
|
1134
1218
|
const releasePointerCapture = (id) => {
|
|
1135
|
-
|
|
1136
|
-
if (captures) {
|
|
1137
|
-
releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
|
|
1138
|
-
}
|
|
1219
|
+
releaseInternalPointerCapture(internal, hit.eventObject, id);
|
|
1139
1220
|
};
|
|
1140
1221
|
const extractEventProps = {};
|
|
1141
1222
|
for (const prop in event) {
|
|
1142
1223
|
const property = event[prop];
|
|
1143
1224
|
if (typeof property !== "function") extractEventProps[prop] = property;
|
|
1144
1225
|
}
|
|
1226
|
+
const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
|
|
1145
1227
|
const raycastEvent = {
|
|
1146
1228
|
...hit,
|
|
1147
1229
|
...extractEventProps,
|
|
@@ -1152,18 +1234,19 @@ function createEvents(store) {
|
|
|
1152
1234
|
unprojectedPoint,
|
|
1153
1235
|
ray: raycaster.ray,
|
|
1154
1236
|
camera,
|
|
1237
|
+
pointerId: eventPointerId,
|
|
1155
1238
|
// Hijack stopPropagation, which just sets a flag
|
|
1156
1239
|
stopPropagation() {
|
|
1157
|
-
const
|
|
1240
|
+
const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
|
|
1158
1241
|
if (
|
|
1159
1242
|
// ...if this pointer hasn't been captured
|
|
1160
|
-
!
|
|
1161
|
-
|
|
1243
|
+
!pointerState?.captured.size || // ... or if the hit object is capturing the pointer
|
|
1244
|
+
pointerState.captured.has(hit.eventObject)
|
|
1162
1245
|
) {
|
|
1163
1246
|
raycastEvent.stopped = localState.stopped = true;
|
|
1164
|
-
if (
|
|
1247
|
+
if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
|
|
1165
1248
|
const higher = intersections.slice(0, intersections.indexOf(hit));
|
|
1166
|
-
cancelPointer([...higher, hit]);
|
|
1249
|
+
cancelPointer([...higher, hit], eventPointerId);
|
|
1167
1250
|
}
|
|
1168
1251
|
}
|
|
1169
1252
|
},
|
|
@@ -1179,15 +1262,18 @@ function createEvents(store) {
|
|
|
1179
1262
|
}
|
|
1180
1263
|
return intersections;
|
|
1181
1264
|
}
|
|
1182
|
-
function cancelPointer(intersections) {
|
|
1265
|
+
function cancelPointer(intersections, pointerId) {
|
|
1183
1266
|
const { internal } = store.getState();
|
|
1184
|
-
|
|
1267
|
+
const pid = pointerId ?? DEFAULT_POINTER_ID;
|
|
1268
|
+
const pointerState = internal.pointerMap.get(pid);
|
|
1269
|
+
if (!pointerState) return;
|
|
1270
|
+
for (const [hoveredId, hoveredObj] of pointerState.hovered) {
|
|
1185
1271
|
if (!intersections.length || !intersections.find(
|
|
1186
1272
|
(hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
|
|
1187
1273
|
)) {
|
|
1188
1274
|
const eventObject = hoveredObj.eventObject;
|
|
1189
1275
|
const instance = eventObject.__r3f;
|
|
1190
|
-
|
|
1276
|
+
pointerState.hovered.delete(hoveredId);
|
|
1191
1277
|
if (instance?.eventCount) {
|
|
1192
1278
|
const handlers = instance.handlers;
|
|
1193
1279
|
const data = { ...hoveredObj, intersections };
|
|
@@ -1216,41 +1302,118 @@ function createEvents(store) {
|
|
|
1216
1302
|
instance?.handlers.onDropMissed?.(event);
|
|
1217
1303
|
}
|
|
1218
1304
|
}
|
|
1305
|
+
function cleanupPointer(pointerId) {
|
|
1306
|
+
const { internal } = store.getState();
|
|
1307
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1308
|
+
if (pointerState) {
|
|
1309
|
+
for (const [, hoveredObj] of pointerState.hovered) {
|
|
1310
|
+
const eventObject = hoveredObj.eventObject;
|
|
1311
|
+
const instance = eventObject.__r3f;
|
|
1312
|
+
if (instance?.eventCount) {
|
|
1313
|
+
const handlers = instance.handlers;
|
|
1314
|
+
const data = { ...hoveredObj, intersections: [] };
|
|
1315
|
+
handlers.onPointerOut?.(data);
|
|
1316
|
+
handlers.onPointerLeave?.(data);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
internal.pointerMap.delete(pointerId);
|
|
1320
|
+
}
|
|
1321
|
+
internal.pointerDirty.delete(pointerId);
|
|
1322
|
+
}
|
|
1323
|
+
function processDeferredPointer(event, pointerId) {
|
|
1324
|
+
const state = store.getState();
|
|
1325
|
+
const { internal } = state;
|
|
1326
|
+
if (!state.events.enabled) return;
|
|
1327
|
+
const filter = filterPointerEvents;
|
|
1328
|
+
const hits = intersect(event, filter);
|
|
1329
|
+
cancelPointer(hits, pointerId);
|
|
1330
|
+
function onIntersect(data) {
|
|
1331
|
+
const eventObject = data.eventObject;
|
|
1332
|
+
const instance = eventObject.__r3f;
|
|
1333
|
+
if (!instance?.eventCount) return;
|
|
1334
|
+
const handlers = instance.handlers;
|
|
1335
|
+
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
|
|
1336
|
+
const id = makeId(data);
|
|
1337
|
+
const pointerState = getPointerState(internal, pointerId);
|
|
1338
|
+
const hoveredItem = pointerState.hovered.get(id);
|
|
1339
|
+
if (!hoveredItem) {
|
|
1340
|
+
pointerState.hovered.set(id, data);
|
|
1341
|
+
handlers.onPointerOver?.(data);
|
|
1342
|
+
handlers.onPointerEnter?.(data);
|
|
1343
|
+
} else if (hoveredItem.stopped) {
|
|
1344
|
+
data.stopPropagation();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
handlers.onPointerMove?.(data);
|
|
1348
|
+
}
|
|
1349
|
+
handleIntersects(hits, event, 0, onIntersect);
|
|
1350
|
+
}
|
|
1219
1351
|
function handlePointer(name) {
|
|
1220
1352
|
switch (name) {
|
|
1221
1353
|
case "onPointerLeave":
|
|
1222
|
-
case "onPointerCancel":
|
|
1223
1354
|
case "onDragLeave":
|
|
1224
1355
|
return () => cancelPointer([]);
|
|
1356
|
+
// Global cancel of these events
|
|
1357
|
+
case "onPointerCancel":
|
|
1358
|
+
return (event) => {
|
|
1359
|
+
const pointerId = getPointerId(event);
|
|
1360
|
+
cleanupPointer(pointerId);
|
|
1361
|
+
};
|
|
1225
1362
|
case "onLostPointerCapture":
|
|
1226
1363
|
return (event) => {
|
|
1227
1364
|
const { internal } = store.getState();
|
|
1228
|
-
|
|
1365
|
+
const pointerId = getPointerId(event);
|
|
1366
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1367
|
+
if (pointerState?.captured.size) {
|
|
1229
1368
|
requestAnimationFrame(() => {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1369
|
+
const pointerState2 = internal.pointerMap.get(pointerId);
|
|
1370
|
+
if (pointerState2?.captured.size) {
|
|
1371
|
+
pointerState2.captured.clear();
|
|
1233
1372
|
}
|
|
1373
|
+
cancelPointer([], pointerId);
|
|
1234
1374
|
});
|
|
1235
1375
|
}
|
|
1236
1376
|
};
|
|
1237
1377
|
}
|
|
1238
1378
|
return function handleEvent(event) {
|
|
1239
1379
|
const state = store.getState();
|
|
1240
|
-
const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
|
|
1380
|
+
const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
|
|
1381
|
+
const pointerId = getPointerId(event);
|
|
1241
1382
|
internal.lastEvent.current = event;
|
|
1242
|
-
if (!
|
|
1383
|
+
if (!events.enabled) return;
|
|
1243
1384
|
const isPointerMove = name === "onPointerMove";
|
|
1244
1385
|
const isDragOver = name === "onDragOver";
|
|
1245
1386
|
const isDrop = name === "onDrop";
|
|
1246
1387
|
const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
|
|
1388
|
+
const isPointerDown = name === "onPointerDown";
|
|
1389
|
+
const isPointerUp = name === "onPointerUp";
|
|
1390
|
+
const isWheel = name === "onWheel";
|
|
1391
|
+
const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
|
|
1392
|
+
if (isPointerMove && canDeferRaycasts) {
|
|
1393
|
+
events.compute?.(event, state);
|
|
1394
|
+
internal.pointerDirty.set(pointerId, event);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
|
|
1398
|
+
events.compute?.(event, state);
|
|
1399
|
+
internal.pointerDirty.set(pointerId, event);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
|
|
1403
|
+
const deferredEvent = internal.pointerDirty.get(pointerId);
|
|
1404
|
+
internal.pointerDirty.delete(pointerId);
|
|
1405
|
+
processDeferredPointer(deferredEvent, pointerId);
|
|
1406
|
+
}
|
|
1247
1407
|
const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
|
|
1248
1408
|
const hits = intersect(event, filter);
|
|
1249
|
-
const delta = isClickEvent ? calculateDistance(event) : 0;
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1409
|
+
const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
|
|
1410
|
+
if (isPointerDown) {
|
|
1411
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1412
|
+
pointerState2.initialClick = [event.offsetX, event.offsetY];
|
|
1413
|
+
pointerState2.initialHits = hits.map((hit) => hit.eventObject);
|
|
1414
|
+
}
|
|
1415
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1416
|
+
const initialHits = pointerState?.initialHits ?? [];
|
|
1254
1417
|
if (isClickEvent && !hits.length) {
|
|
1255
1418
|
if (delta <= 2) {
|
|
1256
1419
|
pointerMissed(event, internal.interaction);
|
|
@@ -1265,7 +1428,9 @@ function createEvents(store) {
|
|
|
1265
1428
|
dropMissed(event, internal.interaction);
|
|
1266
1429
|
if (onDropMissed) onDropMissed(event);
|
|
1267
1430
|
}
|
|
1268
|
-
if (isPointerMove || isDragOver)
|
|
1431
|
+
if (isPointerMove || isDragOver) {
|
|
1432
|
+
cancelPointer(hits, pointerId);
|
|
1433
|
+
}
|
|
1269
1434
|
function onIntersect(data) {
|
|
1270
1435
|
const eventObject = data.eventObject;
|
|
1271
1436
|
const instance = eventObject.__r3f;
|
|
@@ -1274,9 +1439,10 @@ function createEvents(store) {
|
|
|
1274
1439
|
if (isPointerMove) {
|
|
1275
1440
|
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
|
|
1276
1441
|
const id = makeId(data);
|
|
1277
|
-
const
|
|
1442
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1443
|
+
const hoveredItem = pointerState2.hovered.get(id);
|
|
1278
1444
|
if (!hoveredItem) {
|
|
1279
|
-
|
|
1445
|
+
pointerState2.hovered.set(id, data);
|
|
1280
1446
|
handlers.onPointerOver?.(data);
|
|
1281
1447
|
handlers.onPointerEnter?.(data);
|
|
1282
1448
|
} else if (hoveredItem.stopped) {
|
|
@@ -1286,9 +1452,10 @@ function createEvents(store) {
|
|
|
1286
1452
|
handlers.onPointerMove?.(data);
|
|
1287
1453
|
} else if (isDragOver) {
|
|
1288
1454
|
const id = makeId(data);
|
|
1289
|
-
const
|
|
1455
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1456
|
+
const hoveredItem = pointerState2.hovered.get(id);
|
|
1290
1457
|
if (!hoveredItem) {
|
|
1291
|
-
|
|
1458
|
+
pointerState2.hovered.set(id, data);
|
|
1292
1459
|
handlers.onDragOverEnter?.(data);
|
|
1293
1460
|
} else if (hoveredItem.stopped) {
|
|
1294
1461
|
data.stopPropagation();
|
|
@@ -1299,18 +1466,18 @@ function createEvents(store) {
|
|
|
1299
1466
|
} else {
|
|
1300
1467
|
const handler = handlers[name];
|
|
1301
1468
|
if (handler) {
|
|
1302
|
-
if (!isClickEvent ||
|
|
1469
|
+
if (!isClickEvent || initialHits.includes(eventObject)) {
|
|
1303
1470
|
pointerMissed(
|
|
1304
1471
|
event,
|
|
1305
|
-
internal.interaction.filter((object) => !
|
|
1472
|
+
internal.interaction.filter((object) => !initialHits.includes(object))
|
|
1306
1473
|
);
|
|
1307
1474
|
handler(data);
|
|
1308
1475
|
}
|
|
1309
1476
|
} else {
|
|
1310
|
-
if (isClickEvent &&
|
|
1477
|
+
if (isClickEvent && initialHits.includes(eventObject)) {
|
|
1311
1478
|
pointerMissed(
|
|
1312
1479
|
event,
|
|
1313
|
-
internal.interaction.filter((object) => !
|
|
1480
|
+
internal.interaction.filter((object) => !initialHits.includes(object))
|
|
1314
1481
|
);
|
|
1315
1482
|
}
|
|
1316
1483
|
}
|
|
@@ -1319,7 +1486,15 @@ function createEvents(store) {
|
|
|
1319
1486
|
handleIntersects(hits, event, delta, onIntersect);
|
|
1320
1487
|
};
|
|
1321
1488
|
}
|
|
1322
|
-
|
|
1489
|
+
function flushDeferredPointers() {
|
|
1490
|
+
const { internal, events } = store.getState();
|
|
1491
|
+
if (!events.frameTimedRaycasts) return;
|
|
1492
|
+
for (const [pointerId, event] of internal.pointerDirty) {
|
|
1493
|
+
processDeferredPointer(event, pointerId);
|
|
1494
|
+
}
|
|
1495
|
+
internal.pointerDirty.clear();
|
|
1496
|
+
}
|
|
1497
|
+
return { handlePointer, flushDeferredPointers, processDeferredPointer };
|
|
1323
1498
|
}
|
|
1324
1499
|
const DOM_EVENTS = {
|
|
1325
1500
|
onClick: ["click", false],
|
|
@@ -1338,10 +1513,15 @@ const DOM_EVENTS = {
|
|
|
1338
1513
|
onLostPointerCapture: ["lostpointercapture", true]
|
|
1339
1514
|
};
|
|
1340
1515
|
function createPointerEvents(store) {
|
|
1341
|
-
const { handlePointer } = createEvents(store);
|
|
1516
|
+
const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
|
|
1517
|
+
let nextXRPointerId = XR_POINTER_ID_START;
|
|
1518
|
+
const xrPointers = /* @__PURE__ */ new Map();
|
|
1342
1519
|
return {
|
|
1343
1520
|
priority: 1,
|
|
1344
1521
|
enabled: true,
|
|
1522
|
+
frameTimedRaycasts: true,
|
|
1523
|
+
alwaysFireOnScroll: true,
|
|
1524
|
+
updateOnFrame: false,
|
|
1345
1525
|
compute(event, state) {
|
|
1346
1526
|
state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
|
|
1347
1527
|
state.raycaster.setFromCamera(state.pointer, state.camera);
|
|
@@ -1351,11 +1531,33 @@ function createPointerEvents(store) {
|
|
|
1351
1531
|
(acc, key) => ({ ...acc, [key]: handlePointer(key) }),
|
|
1352
1532
|
{}
|
|
1353
1533
|
),
|
|
1354
|
-
update: () => {
|
|
1534
|
+
update: (pointerId) => {
|
|
1535
|
+
const { events, internal } = store.getState();
|
|
1536
|
+
if (!events.handlers) return;
|
|
1537
|
+
if (pointerId !== void 0) {
|
|
1538
|
+
const event = internal.pointerDirty.get(pointerId);
|
|
1539
|
+
if (event) {
|
|
1540
|
+
internal.pointerDirty.delete(pointerId);
|
|
1541
|
+
processDeferredPointer(event, pointerId);
|
|
1542
|
+
} else if (internal.lastEvent?.current) {
|
|
1543
|
+
processDeferredPointer(internal.lastEvent.current, pointerId);
|
|
1544
|
+
}
|
|
1545
|
+
} else {
|
|
1546
|
+
flushDeferredPointers();
|
|
1547
|
+
if (internal.lastEvent?.current) {
|
|
1548
|
+
events.handlers.onPointerMove(internal.lastEvent.current);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
flush: () => {
|
|
1355
1553
|
const { events, internal } = store.getState();
|
|
1356
|
-
|
|
1554
|
+
flushDeferredPointers();
|
|
1555
|
+
if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
|
|
1556
|
+
events.handlers.onPointerMove(internal.lastEvent.current);
|
|
1557
|
+
}
|
|
1357
1558
|
},
|
|
1358
1559
|
connect: (target) => {
|
|
1560
|
+
if (!target) return;
|
|
1359
1561
|
const { set, events } = store.getState();
|
|
1360
1562
|
events.disconnect?.();
|
|
1361
1563
|
set((state) => ({ events: { ...state.events, connected: target } }));
|
|
@@ -1379,6 +1581,33 @@ function createPointerEvents(store) {
|
|
|
1379
1581
|
}
|
|
1380
1582
|
set((state) => ({ events: { ...state.events, connected: void 0 } }));
|
|
1381
1583
|
}
|
|
1584
|
+
},
|
|
1585
|
+
// PREVIEW (A8): the XR pointer registration API may change before stable.
|
|
1586
|
+
registerPointer: (config) => {
|
|
1587
|
+
const pointerId = nextXRPointerId++;
|
|
1588
|
+
xrPointers.set(pointerId, config);
|
|
1589
|
+
const { internal } = store.getState();
|
|
1590
|
+
getPointerState(internal, pointerId);
|
|
1591
|
+
return pointerId;
|
|
1592
|
+
},
|
|
1593
|
+
unregisterPointer: (pointerId) => {
|
|
1594
|
+
xrPointers.delete(pointerId);
|
|
1595
|
+
const { internal } = store.getState();
|
|
1596
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1597
|
+
if (pointerState) {
|
|
1598
|
+
for (const [, hoveredObj] of pointerState.hovered) {
|
|
1599
|
+
const eventObject = hoveredObj.eventObject;
|
|
1600
|
+
const instance = eventObject.__r3f;
|
|
1601
|
+
if (instance?.eventCount) {
|
|
1602
|
+
const handlers = instance.handlers;
|
|
1603
|
+
const data = { ...hoveredObj, intersections: [] };
|
|
1604
|
+
handlers.onPointerOut?.(data);
|
|
1605
|
+
handlers.onPointerLeave?.(data);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
internal.pointerMap.delete(pointerId);
|
|
1609
|
+
}
|
|
1610
|
+
internal.pointerDirty.delete(pointerId);
|
|
1382
1611
|
}
|
|
1383
1612
|
};
|
|
1384
1613
|
}
|
|
@@ -1440,1058 +1669,33 @@ function notifyAlpha({ message, link }) {
|
|
|
1440
1669
|
}
|
|
1441
1670
|
}
|
|
1442
1671
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const
|
|
1447
|
-
|
|
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;
|
|
1672
|
+
const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
|
|
1673
|
+
function isInternalRendererAccess(stack, callerDepth = 2) {
|
|
1674
|
+
if (!stack) return false;
|
|
1675
|
+
const caller = stack.split("\n")[callerDepth] ?? "";
|
|
1676
|
+
return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m));
|
|
1709
1677
|
}
|
|
1710
1678
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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 };
|
|
1679
|
+
const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
|
|
1680
|
+
const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
|
|
1681
|
+
const createStore = (invalidate, advance) => {
|
|
1682
|
+
const rootStore = createWithEqualityFn((set, get) => {
|
|
1683
|
+
const position = new Vector3();
|
|
1684
|
+
const defaultTarget = new Vector3();
|
|
1685
|
+
const tempTarget = new Vector3();
|
|
1686
|
+
function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
|
|
1687
|
+
const { width, height, top, left } = size;
|
|
1688
|
+
const aspect = width / height;
|
|
1689
|
+
if (target.isVector3) tempTarget.copy(target);
|
|
1690
|
+
else tempTarget.set(...target);
|
|
1691
|
+
const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
|
|
1692
|
+
if (isOrthographicCamera(camera)) {
|
|
1693
|
+
return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
|
|
1694
|
+
} else {
|
|
1695
|
+
const fov = camera.fov * Math.PI / 180;
|
|
1696
|
+
const h = 2 * Math.tan(fov / 2) * distance;
|
|
1697
|
+
const w = h * (width / height);
|
|
1698
|
+
return { width: w, height: h, top, left, factor: width / w, distance, aspect };
|
|
2495
1699
|
}
|
|
2496
1700
|
}
|
|
2497
1701
|
let performanceTimeout = void 0;
|
|
@@ -2509,7 +1713,14 @@ const createStore = (invalidate, advance) => {
|
|
|
2509
1713
|
frustum: new Frustum(),
|
|
2510
1714
|
autoUpdateFrustum: true,
|
|
2511
1715
|
raycaster: null,
|
|
2512
|
-
events: {
|
|
1716
|
+
events: {
|
|
1717
|
+
priority: 1,
|
|
1718
|
+
enabled: true,
|
|
1719
|
+
connected: false,
|
|
1720
|
+
frameTimedRaycasts: true,
|
|
1721
|
+
alwaysFireOnScroll: true,
|
|
1722
|
+
updateOnFrame: false
|
|
1723
|
+
},
|
|
2513
1724
|
scene: null,
|
|
2514
1725
|
rootScene: null,
|
|
2515
1726
|
xr: null,
|
|
@@ -2597,14 +1808,18 @@ const createStore = (invalidate, advance) => {
|
|
|
2597
1808
|
}),
|
|
2598
1809
|
setFrameloop: (frameloop = "always") => {
|
|
2599
1810
|
set(() => ({ frameloop }));
|
|
1811
|
+
getScheduler().frameloop = frameloop;
|
|
2600
1812
|
},
|
|
2601
1813
|
setError: (error) => set(() => ({ error })),
|
|
2602
1814
|
error: null,
|
|
2603
|
-
//* TSL State (managed via hooks: useUniforms, useNodes, useTextures,
|
|
1815
|
+
//* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
|
|
2604
1816
|
uniforms: {},
|
|
2605
1817
|
nodes: {},
|
|
1818
|
+
buffers: {},
|
|
1819
|
+
gpuStorage: {},
|
|
2606
1820
|
textures: /* @__PURE__ */ new Map(),
|
|
2607
|
-
|
|
1821
|
+
_textureRefs: /* @__PURE__ */ new Map(),
|
|
1822
|
+
renderPipeline: null,
|
|
2608
1823
|
passes: {},
|
|
2609
1824
|
_hmrVersion: 0,
|
|
2610
1825
|
_sizeImperative: false,
|
|
@@ -2613,12 +1828,16 @@ const createStore = (invalidate, advance) => {
|
|
|
2613
1828
|
internal: {
|
|
2614
1829
|
// Events
|
|
2615
1830
|
interaction: [],
|
|
2616
|
-
hovered: /* @__PURE__ */ new Map(),
|
|
2617
1831
|
subscribers: [],
|
|
1832
|
+
// Per-pointer state (new unified structure)
|
|
1833
|
+
pointerMap: /* @__PURE__ */ new Map(),
|
|
1834
|
+
pointerDirty: /* @__PURE__ */ new Map(),
|
|
1835
|
+
lastEvent: React.createRef(),
|
|
1836
|
+
// Deprecated but kept for backwards compatibility
|
|
1837
|
+
hovered: /* @__PURE__ */ new Map(),
|
|
2618
1838
|
initialClick: [0, 0],
|
|
2619
1839
|
initialHits: [],
|
|
2620
1840
|
capturedMap: /* @__PURE__ */ new Map(),
|
|
2621
|
-
lastEvent: React.createRef(),
|
|
2622
1841
|
// Visibility tracking (onFramed, onOccluded, onVisible)
|
|
2623
1842
|
visibilityRegistry: /* @__PURE__ */ new Map(),
|
|
2624
1843
|
// Occlusion system (WebGPU only)
|
|
@@ -2657,8 +1876,7 @@ const createStore = (invalidate, advance) => {
|
|
|
2657
1876
|
const currentState = rootStore.getState();
|
|
2658
1877
|
if (!currentState.isLegacy && currentState.internal.actualRenderer) {
|
|
2659
1878
|
const stack = new Error().stack || "";
|
|
2660
|
-
|
|
2661
|
-
if (!isInternalAccess) {
|
|
1879
|
+
if (!isInternalRendererAccess(stack)) {
|
|
2662
1880
|
const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
|
|
2663
1881
|
notifyDepreciated({
|
|
2664
1882
|
heading: "Accessing state.gl in WebGPU mode",
|
|
@@ -2706,14 +1924,16 @@ const createStore = (invalidate, advance) => {
|
|
|
2706
1924
|
oldSize = size;
|
|
2707
1925
|
oldDpr = viewport.dpr;
|
|
2708
1926
|
updateCamera(camera, size);
|
|
2709
|
-
if (canvasTarget) {
|
|
1927
|
+
if (internal.isSecondary && canvasTarget) {
|
|
2710
1928
|
if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
|
|
2711
|
-
|
|
2712
|
-
canvasTarget.setSize(size.width, size.height, updateStyle);
|
|
1929
|
+
canvasTarget.setSize(size.width, size.height, false);
|
|
2713
1930
|
} else {
|
|
2714
1931
|
if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
|
|
2715
|
-
|
|
2716
|
-
|
|
1932
|
+
actualRenderer.setSize(size.width, size.height, false);
|
|
1933
|
+
if (canvasTarget) {
|
|
1934
|
+
if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
|
|
1935
|
+
canvasTarget.setSize(size.width, size.height, false);
|
|
1936
|
+
}
|
|
2717
1937
|
}
|
|
2718
1938
|
}
|
|
2719
1939
|
if (camera !== oldCamera) {
|
|
@@ -2769,19 +1989,25 @@ function loadingFn(extensions, onProgress) {
|
|
|
2769
1989
|
);
|
|
2770
1990
|
};
|
|
2771
1991
|
}
|
|
2772
|
-
function useLoader(loader, input, extensions, onProgress) {
|
|
2773
|
-
const
|
|
1992
|
+
function useLoader(loader, input, extensions, onProgress, cacheKey) {
|
|
1993
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
1994
|
+
const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
|
|
2774
1995
|
const fn = loadingFn(extensions, onProgress);
|
|
2775
|
-
const results =
|
|
1996
|
+
const results = cacheKeys.map(
|
|
1997
|
+
(key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
|
|
1998
|
+
);
|
|
2776
1999
|
return Array.isArray(input) ? results : results[0];
|
|
2777
2000
|
}
|
|
2778
|
-
useLoader.preload = function(loader, input, extensions, onProgress) {
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2001
|
+
useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
|
|
2002
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
2003
|
+
const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
|
|
2004
|
+
cacheKeys.forEach(
|
|
2005
|
+
(key, index) => preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
|
|
2006
|
+
);
|
|
2781
2007
|
};
|
|
2782
|
-
useLoader.clear = function(loader, input) {
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2008
|
+
useLoader.clear = function(loader, input, cacheKey) {
|
|
2009
|
+
const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
|
|
2010
|
+
cacheKeys.forEach((key) => clear([loader, key]));
|
|
2785
2011
|
};
|
|
2786
2012
|
useLoader.loader = getLoader;
|
|
2787
2013
|
|
|
@@ -2857,20 +2083,14 @@ function useFrame(callback, priorityOrOptions) {
|
|
|
2857
2083
|
}
|
|
2858
2084
|
};
|
|
2859
2085
|
} else {
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
unregisterJob = registerOutside();
|
|
2869
|
-
});
|
|
2870
|
-
return () => {
|
|
2871
|
-
unsubReady();
|
|
2872
|
-
unregisterJob?.();
|
|
2873
|
-
};
|
|
2086
|
+
return scheduler.register(
|
|
2087
|
+
(state, delta) => {
|
|
2088
|
+
const frameState = state;
|
|
2089
|
+
if (!frameState.renderer) return;
|
|
2090
|
+
callbackRef.current?.(frameState, delta);
|
|
2091
|
+
},
|
|
2092
|
+
{ id, ...options }
|
|
2093
|
+
);
|
|
2874
2094
|
}
|
|
2875
2095
|
}, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
|
|
2876
2096
|
const isPaused = React.useSyncExternalStore(
|
|
@@ -2961,18 +2181,18 @@ function buildFromCache(input, textureCache) {
|
|
|
2961
2181
|
function useTexture(input, optionsOrOnLoad) {
|
|
2962
2182
|
const renderer = useThree((state) => state.internal.actualRenderer);
|
|
2963
2183
|
const store = useStore();
|
|
2964
|
-
const textureCache = useThree((state) => state.textures);
|
|
2965
2184
|
const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
|
|
2966
|
-
const { onLoad, cache =
|
|
2185
|
+
const { onLoad, cache = true } = options;
|
|
2967
2186
|
const onLoadRef = useRef(onLoad);
|
|
2968
2187
|
onLoadRef.current = onLoad;
|
|
2969
2188
|
const onLoadCalledForRef = useRef(null);
|
|
2970
2189
|
const urls = useMemo(() => getUrls(input), [input]);
|
|
2971
2190
|
const cachedResult = useMemo(() => {
|
|
2972
2191
|
if (!cache) return null;
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2192
|
+
const textures = store.getState().textures;
|
|
2193
|
+
if (!allUrlsCached(urls, textures)) return null;
|
|
2194
|
+
return buildFromCache(input, textures);
|
|
2195
|
+
}, [cache, urls, input, store]);
|
|
2976
2196
|
const loadedTextures = useLoader(
|
|
2977
2197
|
TextureLoader,
|
|
2978
2198
|
IsObject(input) ? Object.values(input) : input
|
|
@@ -3016,8 +2236,6 @@ function useTexture(input, optionsOrOnLoad) {
|
|
|
3016
2236
|
}, [input, loadedTextures, cachedResult]);
|
|
3017
2237
|
useEffect(() => {
|
|
3018
2238
|
if (!cache) return;
|
|
3019
|
-
if (cachedResult) return;
|
|
3020
|
-
const set = store.setState;
|
|
3021
2239
|
const urlTextureMap = [];
|
|
3022
2240
|
if (typeof input === "string") {
|
|
3023
2241
|
urlTextureMap.push([input, mappedTextures]);
|
|
@@ -3031,18 +2249,32 @@ function useTexture(input, optionsOrOnLoad) {
|
|
|
3031
2249
|
urlTextureMap.push([url, textureRecord[key]]);
|
|
3032
2250
|
}
|
|
3033
2251
|
}
|
|
3034
|
-
|
|
3035
|
-
const
|
|
3036
|
-
let
|
|
2252
|
+
store.setState((state) => {
|
|
2253
|
+
const refs = new Map(state._textureRefs);
|
|
2254
|
+
let textures = state.textures;
|
|
2255
|
+
let added = false;
|
|
3037
2256
|
for (const [url, texture] of urlTextureMap) {
|
|
3038
|
-
if (!
|
|
3039
|
-
|
|
3040
|
-
|
|
2257
|
+
if (!textures.has(url)) {
|
|
2258
|
+
if (!added) {
|
|
2259
|
+
textures = new Map(textures);
|
|
2260
|
+
added = true;
|
|
2261
|
+
}
|
|
2262
|
+
textures.set(url, texture);
|
|
3041
2263
|
}
|
|
2264
|
+
refs.set(url, (refs.get(url) ?? 0) + 1);
|
|
3042
2265
|
}
|
|
3043
|
-
return
|
|
2266
|
+
return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
|
|
2267
|
+
});
|
|
2268
|
+
return () => store.setState((state) => {
|
|
2269
|
+
const refs = new Map(state._textureRefs);
|
|
2270
|
+
for (const [url] of urlTextureMap) {
|
|
2271
|
+
const next = (refs.get(url) ?? 0) - 1;
|
|
2272
|
+
if (next <= 0) refs.delete(url);
|
|
2273
|
+
else refs.set(url, next);
|
|
2274
|
+
}
|
|
2275
|
+
return { _textureRefs: refs };
|
|
3044
2276
|
});
|
|
3045
|
-
}, [cache, input, mappedTextures, store
|
|
2277
|
+
}, [cache, input, mappedTextures, store]);
|
|
3046
2278
|
return mappedTextures;
|
|
3047
2279
|
}
|
|
3048
2280
|
useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
|
|
@@ -3058,96 +2290,63 @@ const Texture = ({
|
|
|
3058
2290
|
return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
|
|
3059
2291
|
};
|
|
3060
2292
|
|
|
3061
|
-
function
|
|
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() {
|
|
2293
|
+
function useTextures(selector) {
|
|
3069
2294
|
const store = useStore();
|
|
3070
|
-
|
|
3071
|
-
const set = store.setState;
|
|
2295
|
+
const registry = useMemo(() => {
|
|
3072
2296
|
const getState = store.getState;
|
|
3073
|
-
const
|
|
3074
|
-
|
|
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
|
-
};
|
|
2297
|
+
const setState = store.setState;
|
|
2298
|
+
const getOne = (key) => getState().textures.get(key);
|
|
3131
2299
|
return {
|
|
3132
|
-
|
|
3133
|
-
get textures() {
|
|
2300
|
+
get all() {
|
|
3134
2301
|
return getState().textures;
|
|
3135
2302
|
},
|
|
3136
|
-
|
|
3137
|
-
|
|
2303
|
+
get(input) {
|
|
2304
|
+
if (typeof input === "string") return getOne(input);
|
|
2305
|
+
if (Array.isArray(input)) return input.map(getOne);
|
|
2306
|
+
const out = {};
|
|
2307
|
+
for (const name in input) out[name] = getOne(input[name]);
|
|
2308
|
+
return out;
|
|
2309
|
+
},
|
|
3138
2310
|
has: (key) => getState().textures.has(key),
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
2311
|
+
add(keyOrRecord, texture) {
|
|
2312
|
+
setState((state) => {
|
|
2313
|
+
const textures = new Map(state.textures);
|
|
2314
|
+
if (typeof keyOrRecord === "string") {
|
|
2315
|
+
textures.set(keyOrRecord, texture);
|
|
2316
|
+
} else {
|
|
2317
|
+
for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
|
|
2318
|
+
}
|
|
2319
|
+
return { textures };
|
|
2320
|
+
});
|
|
2321
|
+
},
|
|
2322
|
+
dispose(key, options) {
|
|
2323
|
+
const state = getState();
|
|
2324
|
+
const refs = state._textureRefs.get(key) ?? 0;
|
|
2325
|
+
if (refs > 0 && !options?.force) {
|
|
2326
|
+
console.warn(
|
|
2327
|
+
`[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
|
|
2328
|
+
);
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
2331
|
+
state.textures.get(key)?.dispose();
|
|
2332
|
+
setState((s) => {
|
|
2333
|
+
const textures = new Map(s.textures);
|
|
2334
|
+
textures.delete(key);
|
|
2335
|
+
const nextRefs = new Map(s._textureRefs);
|
|
2336
|
+
nextRefs.delete(key);
|
|
2337
|
+
return { textures, _textureRefs: nextRefs };
|
|
2338
|
+
});
|
|
2339
|
+
return true;
|
|
2340
|
+
},
|
|
2341
|
+
disposeAll() {
|
|
2342
|
+
for (const texture of getState().textures.values()) texture.dispose();
|
|
2343
|
+
setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
|
|
2344
|
+
}
|
|
3149
2345
|
};
|
|
3150
2346
|
}, [store]);
|
|
2347
|
+
const subscribe = selector ? () => selector(registry) : (state) => state.textures;
|
|
2348
|
+
const selected = useThree(subscribe);
|
|
2349
|
+
return selector ? selected : registry;
|
|
3151
2350
|
}
|
|
3152
2351
|
|
|
3153
2352
|
function useRenderTarget(widthOrOptions, heightOrOptions, options) {
|
|
@@ -3228,7 +2427,7 @@ function advance(timestamp) {
|
|
|
3228
2427
|
getScheduler().step(timestamp);
|
|
3229
2428
|
}
|
|
3230
2429
|
|
|
3231
|
-
const version = "10.0.0-alpha.
|
|
2430
|
+
const version = "10.0.0-alpha.3";
|
|
3232
2431
|
const packageData = {
|
|
3233
2432
|
version: version};
|
|
3234
2433
|
|
|
@@ -14667,6 +13866,7 @@ function swapInstances() {
|
|
|
14667
13866
|
}
|
|
14668
13867
|
}
|
|
14669
13868
|
if (instance.isHidden) unhideInstance(instance);
|
|
13869
|
+
if (isObject3D(instance.object)) removeInteractivity(findInitialRoot(instance), instance.object);
|
|
14670
13870
|
if (instance.object.__r3f) delete instance.object.__r3f;
|
|
14671
13871
|
if (instance.type !== "primitive") disposeOnIdle(instance.object);
|
|
14672
13872
|
}
|
|
@@ -14679,6 +13879,8 @@ function swapInstances() {
|
|
|
14679
13879
|
instance.object.__r3f = instance;
|
|
14680
13880
|
setFiberRef(fiber, instance.object);
|
|
14681
13881
|
delete instance.appliedOnce;
|
|
13882
|
+
instance.eventCount = 0;
|
|
13883
|
+
instance.handlers = {};
|
|
14682
13884
|
applyProps(instance.object, instance.props);
|
|
14683
13885
|
if (instance.props.attach) {
|
|
14684
13886
|
attach(parent, instance);
|
|
@@ -14894,11 +14096,15 @@ const reconciler = /* @__PURE__ */ createReconciler({
|
|
|
14894
14096
|
cloneMutableTextInstance(textInstance) {
|
|
14895
14097
|
return textInstance;
|
|
14896
14098
|
},
|
|
14099
|
+
// React 19.2 added these for root-level view transitions. They only run for hosts that opt into
|
|
14100
|
+
// view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
|
|
14101
|
+
// into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
|
|
14102
|
+
// React never calls them. They stay as no-ops rather than throws: the host config is shared by
|
|
14103
|
+
// every root, and a throw here would turn a React-internal probe into a hard crash for users.
|
|
14104
|
+
// Guarded by a test asserting a full render + unmount never reaches them.
|
|
14897
14105
|
cloneRootViewTransitionContainer(_rootContainer) {
|
|
14898
|
-
throw new Error("Not implemented.");
|
|
14899
14106
|
},
|
|
14900
14107
|
removeRootViewTransitionClone(_rootContainer, _clone) {
|
|
14901
|
-
throw new Error("Not implemented.");
|
|
14902
14108
|
},
|
|
14903
14109
|
// https://github.com/facebook/react/pull/32465
|
|
14904
14110
|
createFragmentInstance: (_fiber) => null,
|
|
@@ -14983,6 +14189,7 @@ function createRoot(canvas) {
|
|
|
14983
14189
|
const lastConfiguredProps = {};
|
|
14984
14190
|
let configured = false;
|
|
14985
14191
|
let pending = null;
|
|
14192
|
+
let rendererSetup = null;
|
|
14986
14193
|
return {
|
|
14987
14194
|
async configure(props = {}) {
|
|
14988
14195
|
let resolve;
|
|
@@ -14998,7 +14205,6 @@ function createRoot(canvas) {
|
|
|
14998
14205
|
events,
|
|
14999
14206
|
onCreated: onCreatedCallback,
|
|
15000
14207
|
shadows = false,
|
|
15001
|
-
textureColorSpace = SRGBColorSpace,
|
|
15002
14208
|
orthographic = false,
|
|
15003
14209
|
frameloop = "always",
|
|
15004
14210
|
dpr = [1, 2],
|
|
@@ -15013,7 +14219,8 @@ function createRoot(canvas) {
|
|
|
15013
14219
|
_sizeProps,
|
|
15014
14220
|
forceEven
|
|
15015
14221
|
} = props;
|
|
15016
|
-
const
|
|
14222
|
+
const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || SRGBColorSpace;
|
|
14223
|
+
let state = store.getState();
|
|
15017
14224
|
const defaultGPUProps = {
|
|
15018
14225
|
canvas,
|
|
15019
14226
|
antialias: true
|
|
@@ -15027,46 +14234,64 @@ function createRoot(canvas) {
|
|
|
15027
14234
|
throw new Error("Cannot use both gl and renderer props at the same time");
|
|
15028
14235
|
}
|
|
15029
14236
|
let renderer = state.internal.actualRenderer;
|
|
15030
|
-
if (
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
14237
|
+
if (!state.internal.actualRenderer) {
|
|
14238
|
+
if (!rendererSetup) {
|
|
14239
|
+
rendererSetup = (async () => {
|
|
14240
|
+
if (primaryCanvas) {
|
|
14241
|
+
const primary = await waitForPrimary(primaryCanvas);
|
|
14242
|
+
renderer = primary.renderer;
|
|
14243
|
+
state.internal.actualRenderer = renderer;
|
|
14244
|
+
const canvasTarget = new CanvasTarget(canvas);
|
|
14245
|
+
primary.store.setState((prev) => ({
|
|
14246
|
+
internal: { ...prev.internal, isMultiCanvas: true }
|
|
14247
|
+
}));
|
|
14248
|
+
state.set((prev) => ({
|
|
14249
|
+
webGPUSupported: primary.store.getState().webGPUSupported,
|
|
14250
|
+
renderer,
|
|
14251
|
+
primaryStore: primary.store,
|
|
14252
|
+
internal: {
|
|
14253
|
+
...prev.internal,
|
|
14254
|
+
canvasTarget,
|
|
14255
|
+
isMultiCanvas: true,
|
|
14256
|
+
isSecondary: true,
|
|
14257
|
+
targetId: primaryCanvas
|
|
14258
|
+
}
|
|
14259
|
+
}));
|
|
14260
|
+
} else {
|
|
14261
|
+
renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
|
|
14262
|
+
if (!renderer.hasInitialized?.()) {
|
|
14263
|
+
const size2 = computeInitialSize(canvas, propsSize);
|
|
14264
|
+
if (size2.width > 0 && size2.height > 0) {
|
|
14265
|
+
const pixelRatio = calculateDpr(dpr);
|
|
14266
|
+
canvas.width = size2.width * pixelRatio;
|
|
14267
|
+
canvas.height = size2.height * pixelRatio;
|
|
14268
|
+
}
|
|
14269
|
+
await renderer.init();
|
|
14270
|
+
}
|
|
14271
|
+
const backend = renderer.backend;
|
|
14272
|
+
const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
|
|
14273
|
+
state.internal.actualRenderer = renderer;
|
|
14274
|
+
state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
|
|
14275
|
+
if (canvasId && !state.internal.isSecondary) {
|
|
14276
|
+
const canvasTarget = new CanvasTarget(canvas);
|
|
14277
|
+
const unregisterPrimary = registerPrimary(canvasId, renderer, store);
|
|
14278
|
+
state.set((prev) => ({
|
|
14279
|
+
internal: {
|
|
14280
|
+
...prev.internal,
|
|
14281
|
+
canvasTarget,
|
|
14282
|
+
unregisterPrimary
|
|
14283
|
+
}
|
|
14284
|
+
}));
|
|
14285
|
+
}
|
|
15067
14286
|
}
|
|
15068
|
-
}))
|
|
14287
|
+
})().catch((err) => {
|
|
14288
|
+
rendererSetup = null;
|
|
14289
|
+
throw err;
|
|
14290
|
+
});
|
|
15069
14291
|
}
|
|
14292
|
+
await rendererSetup;
|
|
14293
|
+
state = store.getState();
|
|
14294
|
+
renderer = state.internal.actualRenderer;
|
|
15070
14295
|
}
|
|
15071
14296
|
let raycaster = state.raycaster;
|
|
15072
14297
|
if (!raycaster) state.set({ raycaster: raycaster = new Raycaster() });
|
|
@@ -15159,7 +14384,7 @@ function createRoot(canvas) {
|
|
|
15159
14384
|
lastConfiguredProps.performance = performance;
|
|
15160
14385
|
}
|
|
15161
14386
|
if (!state.xr) {
|
|
15162
|
-
const handleXRFrame = (timestamp,
|
|
14387
|
+
const handleXRFrame = (timestamp, _frame) => {
|
|
15163
14388
|
const state2 = store.getState();
|
|
15164
14389
|
if (state2.frameloop === "never") return;
|
|
15165
14390
|
advance(timestamp);
|
|
@@ -15195,15 +14420,22 @@ function createRoot(canvas) {
|
|
|
15195
14420
|
const oldType = renderer.shadowMap.type;
|
|
15196
14421
|
renderer.shadowMap.enabled = !!shadows;
|
|
15197
14422
|
if (is.boo(shadows)) {
|
|
15198
|
-
renderer.shadowMap.type =
|
|
14423
|
+
renderer.shadowMap.type = PCFShadowMap;
|
|
15199
14424
|
} else if (is.str(shadows)) {
|
|
14425
|
+
if (shadows === "soft") {
|
|
14426
|
+
notifyDepreciated({
|
|
14427
|
+
heading: 'shadows="soft" is deprecated',
|
|
14428
|
+
body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
|
|
14429
|
+
link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
|
|
14430
|
+
});
|
|
14431
|
+
}
|
|
15200
14432
|
const types = {
|
|
15201
14433
|
basic: BasicShadowMap,
|
|
15202
14434
|
percentage: PCFShadowMap,
|
|
15203
|
-
soft:
|
|
14435
|
+
soft: PCFShadowMap,
|
|
15204
14436
|
variance: VSMShadowMap
|
|
15205
14437
|
};
|
|
15206
|
-
renderer.shadowMap.type = types[shadows] ??
|
|
14438
|
+
renderer.shadowMap.type = types[shadows] ?? PCFShadowMap;
|
|
15207
14439
|
} else if (is.obj(shadows)) {
|
|
15208
14440
|
Object.assign(renderer.shadowMap, shadows);
|
|
15209
14441
|
}
|
|
@@ -15219,13 +14451,24 @@ function createRoot(canvas) {
|
|
|
15219
14451
|
if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
|
|
15220
14452
|
lastConfiguredProps.textureColorSpace = textureColorSpace;
|
|
15221
14453
|
}
|
|
14454
|
+
const r3fProps = ["textureColorSpace"];
|
|
14455
|
+
const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
|
|
14456
|
+
const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
|
|
15222
14457
|
if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
|
|
15223
|
-
|
|
14458
|
+
const glProps = {};
|
|
14459
|
+
for (const key in glConfig) {
|
|
14460
|
+
if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
|
|
14461
|
+
}
|
|
14462
|
+
applyProps(renderer, glProps);
|
|
15224
14463
|
}
|
|
15225
14464
|
if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
|
|
15226
14465
|
const currentRenderer = state.renderer;
|
|
15227
14466
|
if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
|
|
15228
|
-
|
|
14467
|
+
const rendererProps = {};
|
|
14468
|
+
for (const key in rendererConfig) {
|
|
14469
|
+
if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
|
|
14470
|
+
}
|
|
14471
|
+
applyProps(currentRenderer, rendererProps);
|
|
15229
14472
|
}
|
|
15230
14473
|
}
|
|
15231
14474
|
const scheduler = getScheduler();
|
|
@@ -15251,6 +14494,18 @@ function createRoot(canvas) {
|
|
|
15251
14494
|
system: true
|
|
15252
14495
|
}
|
|
15253
14496
|
);
|
|
14497
|
+
const unregisterEventsFlush = scheduler.register(
|
|
14498
|
+
() => {
|
|
14499
|
+
const state2 = store.getState();
|
|
14500
|
+
state2.events.flush?.();
|
|
14501
|
+
},
|
|
14502
|
+
{
|
|
14503
|
+
id: `${newRootId}_events`,
|
|
14504
|
+
rootId: newRootId,
|
|
14505
|
+
phase: "input",
|
|
14506
|
+
system: true
|
|
14507
|
+
}
|
|
14508
|
+
);
|
|
15254
14509
|
const unregisterFrustum = scheduler.register(
|
|
15255
14510
|
() => {
|
|
15256
14511
|
const state2 = store.getState();
|
|
@@ -15261,7 +14516,7 @@ function createRoot(canvas) {
|
|
|
15261
14516
|
{
|
|
15262
14517
|
id: `${newRootId}_frustum`,
|
|
15263
14518
|
rootId: newRootId,
|
|
15264
|
-
|
|
14519
|
+
before: "render",
|
|
15265
14520
|
system: true
|
|
15266
14521
|
}
|
|
15267
14522
|
);
|
|
@@ -15273,7 +14528,7 @@ function createRoot(canvas) {
|
|
|
15273
14528
|
{
|
|
15274
14529
|
id: `${newRootId}_visibility`,
|
|
15275
14530
|
rootId: newRootId,
|
|
15276
|
-
|
|
14531
|
+
before: "render",
|
|
15277
14532
|
system: true,
|
|
15278
14533
|
after: `${newRootId}_frustum`
|
|
15279
14534
|
}
|
|
@@ -15285,7 +14540,7 @@ function createRoot(canvas) {
|
|
|
15285
14540
|
const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
|
|
15286
14541
|
if (userHandlesRender || state2.internal.priority) return;
|
|
15287
14542
|
try {
|
|
15288
|
-
if (state2.
|
|
14543
|
+
if (state2.renderPipeline?.render) state2.renderPipeline.render();
|
|
15289
14544
|
else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
|
|
15290
14545
|
} catch (error) {
|
|
15291
14546
|
state2.setError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -15310,6 +14565,7 @@ function createRoot(canvas) {
|
|
|
15310
14565
|
unregisterRoot: () => {
|
|
15311
14566
|
unregisterRoot();
|
|
15312
14567
|
unregisterCanvasTarget();
|
|
14568
|
+
unregisterEventsFlush();
|
|
15313
14569
|
unregisterFrustum();
|
|
15314
14570
|
unregisterVisibility();
|
|
15315
14571
|
unregisterRender();
|
|
@@ -15385,7 +14641,8 @@ function unmountComponentAtNode(canvas, callback) {
|
|
|
15385
14641
|
dispose(state.scene);
|
|
15386
14642
|
_roots.delete(canvas);
|
|
15387
14643
|
if (callback) callback(canvas);
|
|
15388
|
-
} catch {
|
|
14644
|
+
} catch (error) {
|
|
14645
|
+
console.warn("[R3F] Error while unmounting root; teardown may be incomplete:", error);
|
|
15389
14646
|
}
|
|
15390
14647
|
}, 500);
|
|
15391
14648
|
}
|
|
@@ -15448,6 +14705,7 @@ function PortalInner({ state = {}, children, container }) {
|
|
|
15448
14705
|
viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
|
|
15449
14706
|
if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
|
|
15450
14707
|
}
|
|
14708
|
+
const set = injectState.set;
|
|
15451
14709
|
return {
|
|
15452
14710
|
// The intersect consists of the previous root state
|
|
15453
14711
|
...rootState,
|
|
@@ -15466,7 +14724,7 @@ function PortalInner({ state = {}, children, container }) {
|
|
|
15466
14724
|
size: resolvedSize,
|
|
15467
14725
|
viewport: { ...rootState.viewport, ...viewport },
|
|
15468
14726
|
// Layers are allowed to override events
|
|
15469
|
-
setEvents: (events2) =>
|
|
14727
|
+
setEvents: (events2) => set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
|
|
15470
14728
|
// Container for child attachment - the portalScene (injected or container itself)
|
|
15471
14729
|
internal: { ...rootState.internal, ...injectState.internal, container: portalScene }
|
|
15472
14730
|
};
|
|
@@ -15475,9 +14733,13 @@ function PortalInner({ state = {}, children, container }) {
|
|
|
15475
14733
|
const store = createWithEqualityFn((set, get) => ({ ...rest, set, get }));
|
|
15476
14734
|
const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
|
|
15477
14735
|
onMutate(previousRoot.getState());
|
|
15478
|
-
previousRoot.subscribe(onMutate);
|
|
15479
14736
|
return store;
|
|
15480
14737
|
}, [previousRoot, container]);
|
|
14738
|
+
useIsomorphicLayoutEffect(() => {
|
|
14739
|
+
const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
|
|
14740
|
+
const unsubscribe = previousRoot.subscribe(onMutate);
|
|
14741
|
+
return unsubscribe;
|
|
14742
|
+
}, [previousRoot, usePortalStore]);
|
|
15481
14743
|
return (
|
|
15482
14744
|
// @ts-ignore, reconciler types are not maintained
|
|
15483
14745
|
/* @__PURE__ */ jsx(Fragment, { children: reconciler.createPortal(
|
|
@@ -15491,6 +14753,178 @@ function flushSync(fn) {
|
|
|
15491
14753
|
return reconciler.flushSyncFromReconciler(fn);
|
|
15492
14754
|
}
|
|
15493
14755
|
|
|
14756
|
+
function parseBackground(background) {
|
|
14757
|
+
if (!background) return null;
|
|
14758
|
+
if (typeof background === "object" && !background.isColor) {
|
|
14759
|
+
const { backgroundMap, envMap, files, preset, ...rest } = background;
|
|
14760
|
+
return {
|
|
14761
|
+
...rest,
|
|
14762
|
+
preset,
|
|
14763
|
+
files: envMap || files,
|
|
14764
|
+
backgroundFiles: backgroundMap,
|
|
14765
|
+
background: true
|
|
14766
|
+
};
|
|
14767
|
+
}
|
|
14768
|
+
if (typeof background === "number") {
|
|
14769
|
+
return { color: background, background: true };
|
|
14770
|
+
}
|
|
14771
|
+
if (typeof background === "string") {
|
|
14772
|
+
if (background in presetsObj) {
|
|
14773
|
+
return { preset: background, background: true };
|
|
14774
|
+
}
|
|
14775
|
+
if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
|
|
14776
|
+
return { files: background, background: true };
|
|
14777
|
+
}
|
|
14778
|
+
return { color: background, background: true };
|
|
14779
|
+
}
|
|
14780
|
+
if (background.isColor) {
|
|
14781
|
+
return { color: background, background: true };
|
|
14782
|
+
}
|
|
14783
|
+
return null;
|
|
14784
|
+
}
|
|
14785
|
+
|
|
14786
|
+
function parseRendererConfig(rendererProp) {
|
|
14787
|
+
const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
|
|
14788
|
+
if (isRendererConfig) {
|
|
14789
|
+
const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
|
|
14790
|
+
return {
|
|
14791
|
+
primaryCanvas: pc,
|
|
14792
|
+
scheduler: sc,
|
|
14793
|
+
renderer: Object.keys(rest).length > 0 ? rest : rendererProp
|
|
14794
|
+
};
|
|
14795
|
+
}
|
|
14796
|
+
return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
|
|
14797
|
+
}
|
|
14798
|
+
|
|
14799
|
+
const RESOURCE_KINDS = ["nodes", "uniforms", "buffers", "gpuStorage"];
|
|
14800
|
+
const ROOT_SCOPE = "";
|
|
14801
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
14802
|
+
function getKindRegistry(store, kind) {
|
|
14803
|
+
let registry = registries.get(store);
|
|
14804
|
+
if (!registry) {
|
|
14805
|
+
registry = {
|
|
14806
|
+
nodes: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
|
|
14807
|
+
uniforms: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
|
|
14808
|
+
buffers: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
|
|
14809
|
+
gpuStorage: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() }
|
|
14810
|
+
};
|
|
14811
|
+
registries.set(store, registry);
|
|
14812
|
+
}
|
|
14813
|
+
return registry[kind];
|
|
14814
|
+
}
|
|
14815
|
+
function resolvePrimary$1(store) {
|
|
14816
|
+
return store.getState().primaryStore ?? store;
|
|
14817
|
+
}
|
|
14818
|
+
function isScopeValid(store, kind, scopeKey) {
|
|
14819
|
+
return getKindRegistry(store, kind).valid.has(scopeKey);
|
|
14820
|
+
}
|
|
14821
|
+
function peekStaged(store, kind, scopeKey) {
|
|
14822
|
+
return getKindRegistry(store, kind).staged.get(scopeKey);
|
|
14823
|
+
}
|
|
14824
|
+
function stageEntries(store, kind, scopeKey, entries) {
|
|
14825
|
+
const { staged } = getKindRegistry(store, kind);
|
|
14826
|
+
const current = staged.get(scopeKey);
|
|
14827
|
+
if (current) Object.assign(current, entries);
|
|
14828
|
+
else staged.set(scopeKey, { ...entries });
|
|
14829
|
+
}
|
|
14830
|
+
function readCommittedScope(map, scopeKey, isLeaf) {
|
|
14831
|
+
if (scopeKey === ROOT_SCOPE) {
|
|
14832
|
+
const leaves = {};
|
|
14833
|
+
for (const [key, value] of Object.entries(map)) if (isLeaf(value)) leaves[key] = value;
|
|
14834
|
+
return leaves;
|
|
14835
|
+
}
|
|
14836
|
+
const scopeData = map[scopeKey];
|
|
14837
|
+
if (scopeData && typeof scopeData === "object" && !isLeaf(scopeData)) return scopeData;
|
|
14838
|
+
return {};
|
|
14839
|
+
}
|
|
14840
|
+
function withStagedOverlay(store, kind, committed) {
|
|
14841
|
+
const { staged } = getKindRegistry(store, kind);
|
|
14842
|
+
if (staged.size === 0) return committed;
|
|
14843
|
+
const view = { ...committed };
|
|
14844
|
+
for (const [scopeKey, entries] of staged) {
|
|
14845
|
+
if (scopeKey === ROOT_SCOPE) Object.assign(view, entries);
|
|
14846
|
+
else view[scopeKey] = { ...view[scopeKey], ...entries };
|
|
14847
|
+
}
|
|
14848
|
+
return view;
|
|
14849
|
+
}
|
|
14850
|
+
function flushStagedScope(store, kind, scopeKey, isLeaf) {
|
|
14851
|
+
const registry = getKindRegistry(store, kind);
|
|
14852
|
+
const staged = registry.staged.get(scopeKey);
|
|
14853
|
+
if (!staged) return;
|
|
14854
|
+
registry.staged.delete(scopeKey);
|
|
14855
|
+
const replace = !registry.valid.has(scopeKey);
|
|
14856
|
+
store.setState((state) => {
|
|
14857
|
+
const map = state[kind];
|
|
14858
|
+
if (scopeKey === ROOT_SCOPE) {
|
|
14859
|
+
if (!replace) return { [kind]: { ...map, ...staged } };
|
|
14860
|
+
const next = {};
|
|
14861
|
+
for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
|
|
14862
|
+
return { [kind]: { ...next, ...staged } };
|
|
14863
|
+
}
|
|
14864
|
+
const current = replace ? {} : map[scopeKey] ?? {};
|
|
14865
|
+
return { [kind]: { ...map, [scopeKey]: { ...current, ...staged } } };
|
|
14866
|
+
});
|
|
14867
|
+
registry.valid.add(scopeKey);
|
|
14868
|
+
}
|
|
14869
|
+
function invalidateResource(store, kind, scopeKey) {
|
|
14870
|
+
const registry = getKindRegistry(store, kind);
|
|
14871
|
+
if (scopeKey === void 0) {
|
|
14872
|
+
registry.valid.clear();
|
|
14873
|
+
registry.staged.clear();
|
|
14874
|
+
} else {
|
|
14875
|
+
registry.valid.delete(scopeKey);
|
|
14876
|
+
registry.staged.delete(scopeKey);
|
|
14877
|
+
}
|
|
14878
|
+
}
|
|
14879
|
+
function rebuildResource(store, kind, userScope) {
|
|
14880
|
+
const primary = resolvePrimary$1(store);
|
|
14881
|
+
invalidateResource(primary, kind, userScope === "root" ? ROOT_SCOPE : userScope);
|
|
14882
|
+
primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
|
|
14883
|
+
}
|
|
14884
|
+
function invalidateAllResources(store) {
|
|
14885
|
+
for (const kind of RESOURCE_KINDS) invalidateResource(store, kind);
|
|
14886
|
+
}
|
|
14887
|
+
function removeResourceEntries(store, kind, names, scope, isLeaf) {
|
|
14888
|
+
const staged = peekStaged(store, kind, scope ?? ROOT_SCOPE);
|
|
14889
|
+
if (staged) for (const name of names) delete staged[name];
|
|
14890
|
+
store.setState((state) => {
|
|
14891
|
+
const map = state[kind];
|
|
14892
|
+
if (scope) {
|
|
14893
|
+
const currentScope = { ...map[scope] };
|
|
14894
|
+
for (const name of names) delete currentScope[name];
|
|
14895
|
+
return { [kind]: { ...map, [scope]: currentScope } };
|
|
14896
|
+
}
|
|
14897
|
+
const next = { ...map };
|
|
14898
|
+
for (const name of names) if (isLeaf(next[name])) delete next[name];
|
|
14899
|
+
return { [kind]: next };
|
|
14900
|
+
});
|
|
14901
|
+
}
|
|
14902
|
+
function clearResourceEntries(store, kind, scope, isLeaf) {
|
|
14903
|
+
const registry = getKindRegistry(store, kind);
|
|
14904
|
+
if (scope && scope !== "root") registry.staged.delete(scope);
|
|
14905
|
+
else if (scope === "root") registry.staged.delete(ROOT_SCOPE);
|
|
14906
|
+
else registry.staged.clear();
|
|
14907
|
+
store.setState((state) => {
|
|
14908
|
+
const map = state[kind];
|
|
14909
|
+
if (scope && scope !== "root") {
|
|
14910
|
+
const { [scope]: _, ...rest } = map;
|
|
14911
|
+
return { [kind]: rest };
|
|
14912
|
+
}
|
|
14913
|
+
if (scope === "root") {
|
|
14914
|
+
const next = {};
|
|
14915
|
+
for (const [key, value] of Object.entries(map)) if (!isLeaf(value)) next[key] = value;
|
|
14916
|
+
return { [kind]: next };
|
|
14917
|
+
}
|
|
14918
|
+
return { [kind]: {} };
|
|
14919
|
+
});
|
|
14920
|
+
}
|
|
14921
|
+
|
|
14922
|
+
function clearHmrCaches(store) {
|
|
14923
|
+
const primary = store.getState().primaryStore ?? store;
|
|
14924
|
+
invalidateAllResources(primary);
|
|
14925
|
+
primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
|
|
14926
|
+
}
|
|
14927
|
+
|
|
15494
14928
|
function CanvasImpl({
|
|
15495
14929
|
ref,
|
|
15496
14930
|
children,
|
|
@@ -15511,6 +14945,8 @@ function CanvasImpl({
|
|
|
15511
14945
|
raycaster,
|
|
15512
14946
|
camera,
|
|
15513
14947
|
scene,
|
|
14948
|
+
autoUpdateFrustum,
|
|
14949
|
+
occlusion,
|
|
15514
14950
|
onPointerMissed,
|
|
15515
14951
|
onDragOverMissed,
|
|
15516
14952
|
onDropMissed,
|
|
@@ -15522,39 +14958,10 @@ function CanvasImpl({
|
|
|
15522
14958
|
forceEven,
|
|
15523
14959
|
...props
|
|
15524
14960
|
}) {
|
|
15525
|
-
const { primaryCanvas, scheduler,
|
|
15526
|
-
const renderer = Object.keys(rendererConfig).length > 0 ? rendererConfig : rendererProp;
|
|
14961
|
+
const { primaryCanvas, scheduler, renderer } = parseRendererConfig(rendererProp);
|
|
15527
14962
|
React.useMemo(() => extend(THREE), []);
|
|
15528
14963
|
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]);
|
|
14964
|
+
const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
|
|
15558
14965
|
const hasInitialSizeRef = React.useRef(false);
|
|
15559
14966
|
const measureConfig = React.useMemo(() => {
|
|
15560
14967
|
if (!hasInitialSizeRef.current) {
|
|
@@ -15596,6 +15003,7 @@ function CanvasImpl({
|
|
|
15596
15003
|
const handleDropMissed = useMutableCallback(onDropMissed);
|
|
15597
15004
|
const [block, setBlock] = React.useState(false);
|
|
15598
15005
|
const [error, setError] = React.useState(false);
|
|
15006
|
+
const [fallbackVisible, setFallbackVisible] = React.useState(false);
|
|
15599
15007
|
if (block) throw block;
|
|
15600
15008
|
if (error) throw error;
|
|
15601
15009
|
const root = React.useRef(null);
|
|
@@ -15603,6 +15011,7 @@ function CanvasImpl({
|
|
|
15603
15011
|
const unsubscribeErrorRef = React.useRef(null);
|
|
15604
15012
|
useIsomorphicLayoutEffect(() => {
|
|
15605
15013
|
effectActiveRef.current = true;
|
|
15014
|
+
if (fallbackVisible) return;
|
|
15606
15015
|
const canvas = canvasRef.current;
|
|
15607
15016
|
if (effectiveSize.width > 0 && effectiveSize.height > 0 && canvas) {
|
|
15608
15017
|
if (!root.current) {
|
|
@@ -15623,7 +15032,7 @@ function CanvasImpl({
|
|
|
15623
15032
|
}
|
|
15624
15033
|
async function run() {
|
|
15625
15034
|
if (!effectActiveRef.current || !root.current) return;
|
|
15626
|
-
await root.current.configure({
|
|
15035
|
+
const configured = await root.current.configure({
|
|
15627
15036
|
id,
|
|
15628
15037
|
primaryCanvas,
|
|
15629
15038
|
scheduler,
|
|
@@ -15638,6 +15047,8 @@ function CanvasImpl({
|
|
|
15638
15047
|
performance,
|
|
15639
15048
|
raycaster,
|
|
15640
15049
|
camera,
|
|
15050
|
+
autoUpdateFrustum,
|
|
15051
|
+
occlusion,
|
|
15641
15052
|
size: effectiveSize,
|
|
15642
15053
|
// Store size props for reset functionality
|
|
15643
15054
|
_sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
|
|
@@ -15662,8 +15073,20 @@ function CanvasImpl({
|
|
|
15662
15073
|
}
|
|
15663
15074
|
onCreated?.(state);
|
|
15664
15075
|
}
|
|
15665
|
-
})
|
|
15666
|
-
|
|
15076
|
+
}).then(
|
|
15077
|
+
() => true,
|
|
15078
|
+
// Renderer setup failed (e.g. no WebGL/WebGPU support). The `fallback` prop lives
|
|
15079
|
+
// inside <canvas>, which browsers don't display, so surface it as visible DOM
|
|
15080
|
+
// instead; with no fallback, rethrow to an external error boundary. (#3757)
|
|
15081
|
+
(setupError) => {
|
|
15082
|
+
if (effectActiveRef.current) {
|
|
15083
|
+
if (fallback != null) setFallbackVisible(true);
|
|
15084
|
+
else setError(setupError);
|
|
15085
|
+
}
|
|
15086
|
+
return false;
|
|
15087
|
+
}
|
|
15088
|
+
);
|
|
15089
|
+
if (!configured || !effectActiveRef.current || !root.current) return;
|
|
15667
15090
|
root.current.render(
|
|
15668
15091
|
/* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxs(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: [
|
|
15669
15092
|
backgroundProps && /* @__PURE__ */ jsx(Environment, { ...backgroundProps }),
|
|
@@ -15697,20 +15120,13 @@ function CanvasImpl({
|
|
|
15697
15120
|
const handleHMR = () => {
|
|
15698
15121
|
queueMicrotask(() => {
|
|
15699
15122
|
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
|
-
}
|
|
15123
|
+
if (rootEntry?.store) clearHmrCaches(rootEntry.store);
|
|
15707
15124
|
});
|
|
15708
15125
|
};
|
|
15709
15126
|
if (typeof import.meta !== "undefined" && import.meta.hot) {
|
|
15710
15127
|
const hot = import.meta.hot;
|
|
15711
15128
|
hot.on("vite:afterUpdate", handleHMR);
|
|
15712
|
-
return () => hot.
|
|
15713
|
-
});
|
|
15129
|
+
return () => hot.off?.("vite:afterUpdate", handleHMR);
|
|
15714
15130
|
}
|
|
15715
15131
|
if (typeof module !== "undefined" && module.hot) {
|
|
15716
15132
|
const hot = module.hot;
|
|
@@ -15733,7 +15149,20 @@ function CanvasImpl({
|
|
|
15733
15149
|
...style
|
|
15734
15150
|
},
|
|
15735
15151
|
...props,
|
|
15736
|
-
children:
|
|
15152
|
+
children: fallbackVisible ? (
|
|
15153
|
+
// Renderer setup failed: render the fallback as visible DOM. Inside <canvas> (below)
|
|
15154
|
+
// it exists in the tree but browsers never paint it, which is the whole bug (#3757).
|
|
15155
|
+
fallback
|
|
15156
|
+
) : /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
|
|
15157
|
+
"canvas",
|
|
15158
|
+
{
|
|
15159
|
+
ref: canvasRef,
|
|
15160
|
+
id,
|
|
15161
|
+
className: "r3f-canvas",
|
|
15162
|
+
style: { display: "block", width: "100%", height: "100%" },
|
|
15163
|
+
children: fallback
|
|
15164
|
+
}
|
|
15165
|
+
) })
|
|
15737
15166
|
}
|
|
15738
15167
|
);
|
|
15739
15168
|
}
|
|
@@ -15806,23 +15235,50 @@ let ScopedStore = _ScopedStore;
|
|
|
15806
15235
|
function createScopedStore(data) {
|
|
15807
15236
|
return new ScopedStore(data);
|
|
15808
15237
|
}
|
|
15809
|
-
function createLazyCreatorState(state) {
|
|
15238
|
+
function createLazyCreatorState(state, store) {
|
|
15810
15239
|
let _uniforms = null;
|
|
15811
15240
|
let _nodes = null;
|
|
15241
|
+
let _buffers = null;
|
|
15242
|
+
let _gpuStorage = null;
|
|
15243
|
+
const view = (kind) => store ? withStagedOverlay(store, kind, state[kind]) : state[kind];
|
|
15812
15244
|
return Object.create(state, {
|
|
15813
15245
|
uniforms: {
|
|
15814
15246
|
get() {
|
|
15815
|
-
return _uniforms ?? (_uniforms = createScopedStore(
|
|
15247
|
+
return _uniforms ?? (_uniforms = createScopedStore(view("uniforms")));
|
|
15816
15248
|
}
|
|
15817
15249
|
},
|
|
15818
15250
|
nodes: {
|
|
15819
15251
|
get() {
|
|
15820
|
-
return _nodes ?? (_nodes = createScopedStore(
|
|
15252
|
+
return _nodes ?? (_nodes = createScopedStore(view("nodes")));
|
|
15253
|
+
}
|
|
15254
|
+
},
|
|
15255
|
+
buffers: {
|
|
15256
|
+
get() {
|
|
15257
|
+
return _buffers ?? (_buffers = createScopedStore(view("buffers")));
|
|
15258
|
+
}
|
|
15259
|
+
},
|
|
15260
|
+
gpuStorage: {
|
|
15261
|
+
get() {
|
|
15262
|
+
return _gpuStorage ?? (_gpuStorage = createScopedStore(view("gpuStorage")));
|
|
15821
15263
|
}
|
|
15822
15264
|
}
|
|
15823
15265
|
});
|
|
15824
15266
|
}
|
|
15825
15267
|
|
|
15268
|
+
function resolvePrimary(local) {
|
|
15269
|
+
return local.getState().primaryStore ?? local;
|
|
15270
|
+
}
|
|
15271
|
+
function usePrimaryStore() {
|
|
15272
|
+
const local = useStore();
|
|
15273
|
+
const primary = local.getState().primaryStore;
|
|
15274
|
+
return useMemo(() => primary ?? local, [primary, local]);
|
|
15275
|
+
}
|
|
15276
|
+
function usePrimaryThree(selector = (state) => state, equalityFn) {
|
|
15277
|
+
const local = useStore();
|
|
15278
|
+
const primary = resolvePrimary(local);
|
|
15279
|
+
return primary(selector, equalityFn);
|
|
15280
|
+
}
|
|
15281
|
+
|
|
15826
15282
|
function addTexture(set, key, value) {
|
|
15827
15283
|
set((state) => {
|
|
15828
15284
|
const newMap = new Map(state.textures);
|
|
@@ -15901,6 +15357,10 @@ function vectorize(inObject) {
|
|
|
15901
15357
|
if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
|
|
15902
15358
|
if (obj.isMatrix3 || obj.isMatrix4) return inObject;
|
|
15903
15359
|
if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
|
|
15360
|
+
if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
|
|
15361
|
+
const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
|
|
15362
|
+
return new Color(obj.r * scale, obj.g * scale, obj.b * scale);
|
|
15363
|
+
}
|
|
15904
15364
|
if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
|
|
15905
15365
|
if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
|
|
15906
15366
|
return new Vector4(obj.x, obj.y, obj.z, obj.w);
|
|
@@ -15922,65 +15382,56 @@ function useCompareMemoize(value, deep) {
|
|
|
15922
15382
|
return ref.current;
|
|
15923
15383
|
}
|
|
15924
15384
|
|
|
15385
|
+
function useScopedResource(options) {
|
|
15386
|
+
const { store, kind, scope, isLeaf, create, prepare, reconcile, input } = options;
|
|
15387
|
+
const scopeKey = scope ?? ROOT_SCOPE;
|
|
15388
|
+
const version = store((s) => s._hmrVersion);
|
|
15389
|
+
const entries = useMemo(() => {
|
|
15390
|
+
const staged = peekStaged(store, kind, scopeKey);
|
|
15391
|
+
const reusable = isScopeValid(store, kind, scopeKey);
|
|
15392
|
+
const map = store.getState()[kind];
|
|
15393
|
+
const committed = reusable ? readCommittedScope(map, scopeKey, isLeaf) : void 0;
|
|
15394
|
+
const result = {};
|
|
15395
|
+
let fresh = null;
|
|
15396
|
+
for (const [name, candidate] of Object.entries(create())) {
|
|
15397
|
+
const existing = staged?.[name] ?? committed?.[name];
|
|
15398
|
+
if (existing !== void 0) {
|
|
15399
|
+
result[name] = existing;
|
|
15400
|
+
reconcile?.(existing, candidate);
|
|
15401
|
+
} else {
|
|
15402
|
+
const value = prepare ? prepare(name, candidate) : candidate;
|
|
15403
|
+
result[name] = value;
|
|
15404
|
+
(fresh ?? (fresh = {}))[name] = value;
|
|
15405
|
+
}
|
|
15406
|
+
}
|
|
15407
|
+
if (fresh) stageEntries(store, kind, scopeKey, fresh);
|
|
15408
|
+
return result;
|
|
15409
|
+
}, [store, kind, scopeKey, version, input]);
|
|
15410
|
+
useIsomorphicLayoutEffect(() => {
|
|
15411
|
+
flushStagedScope(store, kind, scopeKey, isLeaf);
|
|
15412
|
+
});
|
|
15413
|
+
return entries;
|
|
15414
|
+
}
|
|
15415
|
+
|
|
15925
15416
|
const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
|
|
15926
15417
|
function useUniforms(creatorOrScope, scope) {
|
|
15927
|
-
const store =
|
|
15418
|
+
const store = usePrimaryStore();
|
|
15928
15419
|
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
|
-
},
|
|
15420
|
+
(names, targetScope) => removeResourceEntries(store, "uniforms", Array.isArray(names) ? names : [names], targetScope, isUniformNode$1),
|
|
15942
15421
|
[store]
|
|
15943
15422
|
);
|
|
15944
15423
|
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
|
-
},
|
|
15424
|
+
(targetScope) => clearResourceEntries(store, "uniforms", targetScope, isUniformNode$1),
|
|
15961
15425
|
[store]
|
|
15962
15426
|
);
|
|
15963
15427
|
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
|
-
},
|
|
15428
|
+
(targetScope) => rebuildResource(store, "uniforms", targetScope),
|
|
15978
15429
|
[store]
|
|
15979
15430
|
);
|
|
15980
15431
|
const inputForMemoization = useMemo(() => {
|
|
15981
15432
|
let raw = creatorOrScope;
|
|
15982
15433
|
if (is.fun(creatorOrScope)) {
|
|
15983
|
-
const wrappedState = createLazyCreatorState(store.getState());
|
|
15434
|
+
const wrappedState = createLazyCreatorState(store.getState(), store);
|
|
15984
15435
|
raw = creatorOrScope(wrappedState);
|
|
15985
15436
|
}
|
|
15986
15437
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
@@ -15994,80 +15445,44 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
15994
15445
|
}, [creatorOrScope, store]);
|
|
15995
15446
|
const memoizedInput = useCompareMemoize(inputForMemoization);
|
|
15996
15447
|
const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
|
|
15997
|
-
const storeUniforms =
|
|
15998
|
-
const
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
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
|
-
}
|
|
15448
|
+
const storeUniforms = usePrimaryThree((s) => s.uniforms);
|
|
15449
|
+
const created = useScopedResource({
|
|
15450
|
+
store,
|
|
15451
|
+
kind: "uniforms",
|
|
15452
|
+
scope: isReader ? void 0 : scope,
|
|
15453
|
+
isLeaf: isUniformNode$1,
|
|
15454
|
+
input: memoizedInput,
|
|
15455
|
+
create: () => {
|
|
15456
|
+
if (isReader) return {};
|
|
15457
|
+
if (typeof memoizedInput !== "object" || memoizedInput === null) throw new Error("Invalid uniform input");
|
|
15458
|
+
return memoizedInput;
|
|
15459
|
+
},
|
|
15460
|
+
prepare: (name, candidate) => createUniform(name, candidate, scope),
|
|
15461
|
+
reconcile: (existing, candidate) => {
|
|
15462
|
+
const existingVal = existing.value;
|
|
15463
|
+
const newVal = vectorize(candidate);
|
|
15464
|
+
let equals = newVal === existingVal;
|
|
15465
|
+
if (!equals && hasEqualsMethod(existingVal) && hasEqualsMethod(newVal)) {
|
|
15466
|
+
if (isThreeVector(existingVal) && isThreeVector(newVal)) {
|
|
15467
|
+
equals = vectorEquals(existingVal, newVal);
|
|
15468
|
+
} else if (isSameThreeType(existingVal, newVal)) {
|
|
15469
|
+
equals = existingVal.equals(newVal);
|
|
16035
15470
|
}
|
|
16036
|
-
if (!equals) result[name].value = newVal;
|
|
16037
|
-
} else {
|
|
16038
|
-
result[name] = createUniform(name, node, scope);
|
|
16039
|
-
hasNewUniforms = true;
|
|
16040
|
-
}
|
|
16041
|
-
}
|
|
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
15471
|
}
|
|
15472
|
+
if (!equals) existing.value = newVal;
|
|
16053
15473
|
}
|
|
16054
|
-
|
|
16055
|
-
|
|
15474
|
+
});
|
|
15475
|
+
let uniforms = created;
|
|
15476
|
+
if (memoizedInput === void 0) {
|
|
15477
|
+
uniforms = storeUniforms;
|
|
15478
|
+
} else if (typeof memoizedInput === "string") {
|
|
15479
|
+
const scopeData = storeUniforms[memoizedInput];
|
|
15480
|
+
uniforms = scopeData && !isUniformNode$1(scopeData) ? scopeData : {};
|
|
15481
|
+
}
|
|
16056
15482
|
return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
|
|
16057
15483
|
}
|
|
16058
15484
|
function rebuildAllUniforms(store, scope) {
|
|
16059
|
-
store
|
|
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
|
-
});
|
|
15485
|
+
rebuildResource(store, "uniforms", scope);
|
|
16071
15486
|
}
|
|
16072
15487
|
function removeUniforms(set, names, scope) {
|
|
16073
15488
|
set((state) => {
|
|
@@ -16131,191 +15546,91 @@ function isSameThreeType(a, b) {
|
|
|
16131
15546
|
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
15547
|
}
|
|
16133
15548
|
|
|
15549
|
+
const uniformOf = uniform;
|
|
16134
15550
|
const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
|
|
16135
15551
|
const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
|
|
16136
15552
|
function useUniform(name, value) {
|
|
16137
15553
|
const store = useStore();
|
|
16138
|
-
const
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
|
|
16143
|
-
|
|
15554
|
+
const registered = useScopedResource({
|
|
15555
|
+
store,
|
|
15556
|
+
kind: "uniforms",
|
|
15557
|
+
scope: void 0,
|
|
15558
|
+
isLeaf: isUniformNode,
|
|
15559
|
+
input: name,
|
|
15560
|
+
create: () => value === void 0 ? {} : { [name]: value },
|
|
15561
|
+
prepare: createNamedUniform,
|
|
15562
|
+
reconcile: (existing2) => {
|
|
16144
15563
|
if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
|
|
16145
|
-
|
|
15564
|
+
existing2.value = typeof value === "string" ? new Color(value) : value;
|
|
16146
15565
|
}
|
|
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
|
-
}
|
|
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
15566
|
}
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
})
|
|
16181
|
-
|
|
16182
|
-
|
|
15567
|
+
});
|
|
15568
|
+
if (registered[name]) return registered[name];
|
|
15569
|
+
const existing = peekStaged(store, "uniforms", ROOT_SCOPE)?.[name] ?? store.getState().uniforms[name];
|
|
15570
|
+
if (existing && isUniformNode(existing)) return existing;
|
|
15571
|
+
throw new Error(
|
|
15572
|
+
`[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
|
|
15573
|
+
);
|
|
15574
|
+
}
|
|
15575
|
+
function createNamedUniform(name, value) {
|
|
15576
|
+
let node;
|
|
15577
|
+
if (isUniformNode(value)) {
|
|
15578
|
+
node = value;
|
|
15579
|
+
} else if (isTSLNode$1(value)) {
|
|
15580
|
+
node = uniformOf(value);
|
|
15581
|
+
} else if (typeof value === "string") {
|
|
15582
|
+
node = uniform(new Color(value));
|
|
15583
|
+
} else {
|
|
15584
|
+
node = uniformOf(value);
|
|
15585
|
+
}
|
|
15586
|
+
if (typeof node.setName === "function") {
|
|
15587
|
+
node.setName(name);
|
|
15588
|
+
}
|
|
15589
|
+
return node;
|
|
16183
15590
|
}
|
|
16184
15591
|
|
|
16185
15592
|
const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
|
|
16186
15593
|
function useNodes(creatorOrScope, scope) {
|
|
16187
|
-
const store =
|
|
15594
|
+
const store = usePrimaryStore();
|
|
16188
15595
|
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
|
-
},
|
|
15596
|
+
(names, targetScope) => removeResourceEntries(store, "nodes", Array.isArray(names) ? names : [names], targetScope, isTSLNode),
|
|
16202
15597
|
[store]
|
|
16203
15598
|
);
|
|
16204
15599
|
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
|
-
},
|
|
15600
|
+
(targetScope) => clearResourceEntries(store, "nodes", targetScope, isTSLNode),
|
|
16221
15601
|
[store]
|
|
16222
15602
|
);
|
|
16223
15603
|
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
|
-
},
|
|
15604
|
+
(targetScope) => rebuildResource(store, "nodes", targetScope),
|
|
16241
15605
|
[store]
|
|
16242
15606
|
);
|
|
16243
15607
|
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
16244
|
-
const storeNodes =
|
|
16245
|
-
const
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
return
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
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 } }));
|
|
15608
|
+
const storeNodes = usePrimaryThree((s) => s.nodes);
|
|
15609
|
+
const created = useScopedResource({
|
|
15610
|
+
store,
|
|
15611
|
+
kind: "nodes",
|
|
15612
|
+
scope: isReader ? void 0 : scope,
|
|
15613
|
+
isLeaf: isTSLNode,
|
|
15614
|
+
create: () => {
|
|
15615
|
+
if (isReader) return {};
|
|
15616
|
+
return creatorOrScope(createLazyCreatorState(store.getState(), store));
|
|
15617
|
+
},
|
|
15618
|
+
prepare: (name, node) => {
|
|
15619
|
+
node.setName?.(scope ? `${scope}.${name}` : name);
|
|
15620
|
+
return node;
|
|
16298
15621
|
}
|
|
16299
|
-
|
|
16300
|
-
|
|
15622
|
+
});
|
|
15623
|
+
let nodes = created;
|
|
15624
|
+
if (creatorOrScope === void 0) {
|
|
15625
|
+
nodes = storeNodes;
|
|
15626
|
+
} else if (typeof creatorOrScope === "string") {
|
|
15627
|
+
const scopeData = storeNodes[creatorOrScope];
|
|
15628
|
+
nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
|
|
15629
|
+
}
|
|
16301
15630
|
return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
|
|
16302
15631
|
}
|
|
16303
15632
|
function rebuildAllNodes(store, scope) {
|
|
16304
|
-
store
|
|
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
|
-
});
|
|
15633
|
+
rebuildResource(store, "nodes", scope);
|
|
16319
15634
|
}
|
|
16320
15635
|
function removeNodes(set, names, scope) {
|
|
16321
15636
|
set((state) => {
|
|
@@ -16345,18 +15660,172 @@ function clearRootNodes(set) {
|
|
|
16345
15660
|
});
|
|
16346
15661
|
}
|
|
16347
15662
|
function useLocalNodes(creator) {
|
|
16348
|
-
const store =
|
|
16349
|
-
const uniforms =
|
|
16350
|
-
const nodes =
|
|
16351
|
-
const textures =
|
|
16352
|
-
const hmrVersion =
|
|
15663
|
+
const store = usePrimaryStore();
|
|
15664
|
+
const uniforms = usePrimaryThree((s) => s.uniforms);
|
|
15665
|
+
const nodes = usePrimaryThree((s) => s.nodes);
|
|
15666
|
+
const textures = usePrimaryThree((s) => s.textures);
|
|
15667
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
16353
15668
|
return useMemo(() => {
|
|
16354
|
-
const wrappedState = createLazyCreatorState(store.getState());
|
|
15669
|
+
const wrappedState = createLazyCreatorState(store.getState(), store);
|
|
16355
15670
|
return creator(wrappedState);
|
|
16356
15671
|
}, [store, creator, uniforms, nodes, textures, hmrVersion]);
|
|
16357
15672
|
}
|
|
16358
15673
|
|
|
16359
|
-
|
|
15674
|
+
const isBufferLike = (value) => {
|
|
15675
|
+
if (value === null || typeof value !== "object") return false;
|
|
15676
|
+
if (ArrayBuffer.isView(value)) return true;
|
|
15677
|
+
if ("isBufferAttribute" in value) return true;
|
|
15678
|
+
if ("uuid" in value || "nodeType" in value) return true;
|
|
15679
|
+
return false;
|
|
15680
|
+
};
|
|
15681
|
+
const disposeBuffer = (buffer) => {
|
|
15682
|
+
if (buffer === null || typeof buffer !== "object") return;
|
|
15683
|
+
if ("dispose" in buffer && typeof buffer.dispose === "function") {
|
|
15684
|
+
buffer.dispose();
|
|
15685
|
+
}
|
|
15686
|
+
};
|
|
15687
|
+
function useBuffers(creatorOrScope, scope) {
|
|
15688
|
+
const store = usePrimaryStore();
|
|
15689
|
+
const removeBuffers = useCallback(
|
|
15690
|
+
(names, targetScope) => removeResourceEntries(store, "buffers", Array.isArray(names) ? names : [names], targetScope, isBufferLike),
|
|
15691
|
+
[store]
|
|
15692
|
+
);
|
|
15693
|
+
const clearBuffers = useCallback(
|
|
15694
|
+
(targetScope) => clearResourceEntries(store, "buffers", targetScope, isBufferLike),
|
|
15695
|
+
[store]
|
|
15696
|
+
);
|
|
15697
|
+
const rebuildBuffers = useCallback(
|
|
15698
|
+
(targetScope) => rebuildResource(store, "buffers", targetScope),
|
|
15699
|
+
[store]
|
|
15700
|
+
);
|
|
15701
|
+
const disposeBuffers = useCallback(
|
|
15702
|
+
(names, targetScope) => {
|
|
15703
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15704
|
+
const state = store.getState();
|
|
15705
|
+
for (const name of nameArray) {
|
|
15706
|
+
const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
|
|
15707
|
+
if (buffer && isBufferLike(buffer)) {
|
|
15708
|
+
disposeBuffer(buffer);
|
|
15709
|
+
}
|
|
15710
|
+
}
|
|
15711
|
+
removeBuffers(names, targetScope);
|
|
15712
|
+
},
|
|
15713
|
+
[store, removeBuffers]
|
|
15714
|
+
);
|
|
15715
|
+
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
15716
|
+
const storeBuffers = usePrimaryThree((s) => s.buffers);
|
|
15717
|
+
const created = useScopedResource({
|
|
15718
|
+
store,
|
|
15719
|
+
kind: "buffers",
|
|
15720
|
+
scope: isReader ? void 0 : scope,
|
|
15721
|
+
isLeaf: isBufferLike,
|
|
15722
|
+
create: () => {
|
|
15723
|
+
if (isReader) return {};
|
|
15724
|
+
return creatorOrScope(createLazyCreatorState(store.getState(), store));
|
|
15725
|
+
},
|
|
15726
|
+
prepare: (name, buffer) => {
|
|
15727
|
+
if ("setName" in buffer && typeof buffer.setName === "function") {
|
|
15728
|
+
buffer.setName(scope ? `${scope}.${name}` : name);
|
|
15729
|
+
}
|
|
15730
|
+
return buffer;
|
|
15731
|
+
}
|
|
15732
|
+
});
|
|
15733
|
+
let buffers = created;
|
|
15734
|
+
if (creatorOrScope === void 0) {
|
|
15735
|
+
buffers = storeBuffers;
|
|
15736
|
+
} else if (typeof creatorOrScope === "string") {
|
|
15737
|
+
const scopeData = storeBuffers[creatorOrScope];
|
|
15738
|
+
buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
|
|
15739
|
+
}
|
|
15740
|
+
return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
|
|
15741
|
+
}
|
|
15742
|
+
function rebuildAllBuffers(store, scope) {
|
|
15743
|
+
rebuildResource(store, "buffers", scope);
|
|
15744
|
+
}
|
|
15745
|
+
|
|
15746
|
+
const isStorageLike = (value) => {
|
|
15747
|
+
if (value === null || typeof value !== "object") return false;
|
|
15748
|
+
if ("isTexture" in value) return true;
|
|
15749
|
+
if ("isData3DTexture" in value) return true;
|
|
15750
|
+
if ("uuid" in value || "nodeType" in value) return true;
|
|
15751
|
+
return false;
|
|
15752
|
+
};
|
|
15753
|
+
const disposeStorage = (storage) => {
|
|
15754
|
+
if (storage === null || typeof storage !== "object") return;
|
|
15755
|
+
if ("dispose" in storage && typeof storage.dispose === "function") {
|
|
15756
|
+
storage.dispose();
|
|
15757
|
+
}
|
|
15758
|
+
};
|
|
15759
|
+
function useGPUStorage(creatorOrScope, scope) {
|
|
15760
|
+
const store = usePrimaryStore();
|
|
15761
|
+
const removeStorage = useCallback(
|
|
15762
|
+
(names, targetScope) => removeResourceEntries(store, "gpuStorage", Array.isArray(names) ? names : [names], targetScope, isStorageLike),
|
|
15763
|
+
[store]
|
|
15764
|
+
);
|
|
15765
|
+
const clearStorage = useCallback(
|
|
15766
|
+
(targetScope) => clearResourceEntries(store, "gpuStorage", targetScope, isStorageLike),
|
|
15767
|
+
[store]
|
|
15768
|
+
);
|
|
15769
|
+
const rebuildStorage = useCallback(
|
|
15770
|
+
(targetScope) => rebuildResource(store, "gpuStorage", targetScope),
|
|
15771
|
+
[store]
|
|
15772
|
+
);
|
|
15773
|
+
const disposeStorageFn = useCallback(
|
|
15774
|
+
(names, targetScope) => {
|
|
15775
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15776
|
+
const state = store.getState();
|
|
15777
|
+
for (const name of nameArray) {
|
|
15778
|
+
const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
|
|
15779
|
+
if (storage && isStorageLike(storage)) {
|
|
15780
|
+
disposeStorage(storage);
|
|
15781
|
+
}
|
|
15782
|
+
}
|
|
15783
|
+
removeStorage(names, targetScope);
|
|
15784
|
+
},
|
|
15785
|
+
[store, removeStorage]
|
|
15786
|
+
);
|
|
15787
|
+
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
15788
|
+
const storeStorage = usePrimaryThree((s) => s.gpuStorage);
|
|
15789
|
+
const created = useScopedResource({
|
|
15790
|
+
store,
|
|
15791
|
+
kind: "gpuStorage",
|
|
15792
|
+
scope: isReader ? void 0 : scope,
|
|
15793
|
+
isLeaf: isStorageLike,
|
|
15794
|
+
create: () => {
|
|
15795
|
+
if (isReader) return {};
|
|
15796
|
+
return creatorOrScope(createLazyCreatorState(store.getState(), store));
|
|
15797
|
+
},
|
|
15798
|
+
prepare: (name, storage) => {
|
|
15799
|
+
const label = scope ? `${scope}.${name}` : name;
|
|
15800
|
+
if ("setName" in storage && typeof storage.setName === "function") {
|
|
15801
|
+
storage.setName(label);
|
|
15802
|
+
}
|
|
15803
|
+
if ("name" in storage && typeof storage.name === "string") {
|
|
15804
|
+
storage.name = label;
|
|
15805
|
+
}
|
|
15806
|
+
return storage;
|
|
15807
|
+
}
|
|
15808
|
+
});
|
|
15809
|
+
let gpuStorage = created;
|
|
15810
|
+
if (creatorOrScope === void 0) {
|
|
15811
|
+
gpuStorage = storeStorage;
|
|
15812
|
+
} else if (typeof creatorOrScope === "string") {
|
|
15813
|
+
const scopeData = storeStorage[creatorOrScope];
|
|
15814
|
+
gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
|
|
15815
|
+
}
|
|
15816
|
+
return {
|
|
15817
|
+
...gpuStorage,
|
|
15818
|
+
removeStorage,
|
|
15819
|
+
clearStorage,
|
|
15820
|
+
rebuildStorage,
|
|
15821
|
+
disposeStorage: disposeStorageFn
|
|
15822
|
+
};
|
|
15823
|
+
}
|
|
15824
|
+
function rebuildAllStorage(store, scope) {
|
|
15825
|
+
rebuildResource(store, "gpuStorage", scope);
|
|
15826
|
+
}
|
|
15827
|
+
|
|
15828
|
+
function useRenderPipeline(mainCB, setupCB) {
|
|
16360
15829
|
const store = useStore();
|
|
16361
15830
|
const { scene, camera, renderer, isLegacy } = useThree();
|
|
16362
15831
|
const callbacksRanRef = useRef(false);
|
|
@@ -16375,7 +15844,7 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
16375
15844
|
}, [store]);
|
|
16376
15845
|
const reset = useCallback(() => {
|
|
16377
15846
|
store.setState({
|
|
16378
|
-
|
|
15847
|
+
renderPipeline: null,
|
|
16379
15848
|
passes: {}
|
|
16380
15849
|
});
|
|
16381
15850
|
callbacksRanRef.current = false;
|
|
@@ -16388,18 +15857,18 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
16388
15857
|
}, []);
|
|
16389
15858
|
useLayoutEffect(() => {
|
|
16390
15859
|
if (isLegacy) {
|
|
16391
|
-
throw new Error("
|
|
15860
|
+
throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
|
|
16392
15861
|
}
|
|
16393
15862
|
if (!renderer || !scene || !camera) return;
|
|
16394
15863
|
const state = store.getState();
|
|
16395
15864
|
const set = store.setState;
|
|
16396
15865
|
try {
|
|
16397
|
-
let
|
|
15866
|
+
let pipeline = state.renderPipeline;
|
|
16398
15867
|
let currentPasses = { ...state.passes };
|
|
16399
|
-
let
|
|
16400
|
-
if (!
|
|
16401
|
-
|
|
16402
|
-
|
|
15868
|
+
let justCreatedPipeline = false;
|
|
15869
|
+
if (!pipeline) {
|
|
15870
|
+
pipeline = new RenderPipeline(renderer);
|
|
15871
|
+
justCreatedPipeline = true;
|
|
16403
15872
|
}
|
|
16404
15873
|
const cacheValid = scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
|
|
16405
15874
|
let scenePass;
|
|
@@ -16410,9 +15879,9 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
16410
15879
|
scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
|
|
16411
15880
|
}
|
|
16412
15881
|
currentPasses.scenePass = scenePass;
|
|
16413
|
-
if (!
|
|
16414
|
-
set({
|
|
16415
|
-
const shouldRunCallbacks =
|
|
15882
|
+
if (!pipeline.outputNode || justCreatedPipeline) pipeline.outputNode = scenePass;
|
|
15883
|
+
set({ renderPipeline: pipeline, passes: currentPasses });
|
|
15884
|
+
const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
|
|
16416
15885
|
if (shouldRunCallbacks) {
|
|
16417
15886
|
if (setupCBRef.current) {
|
|
16418
15887
|
const freshState = store.getState();
|
|
@@ -16433,22 +15902,22 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
16433
15902
|
callbacksRanRef.current = true;
|
|
16434
15903
|
}
|
|
16435
15904
|
} catch (error) {
|
|
16436
|
-
console.
|
|
15905
|
+
console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
|
|
16437
15906
|
}
|
|
16438
15907
|
}, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
|
|
16439
15908
|
const passes = useThree((s) => s.passes);
|
|
16440
|
-
const
|
|
15909
|
+
const renderPipeline = useThree((s) => s.renderPipeline);
|
|
16441
15910
|
return {
|
|
16442
15911
|
passes,
|
|
16443
|
-
|
|
15912
|
+
renderPipeline,
|
|
16444
15913
|
clearPasses,
|
|
16445
15914
|
reset,
|
|
16446
15915
|
rebuild,
|
|
16447
|
-
// isReady indicates if
|
|
16448
|
-
isReady:
|
|
15916
|
+
// isReady indicates if RenderPipeline is configured and ready for rendering
|
|
15917
|
+
isReady: renderPipeline !== null
|
|
16449
15918
|
};
|
|
16450
15919
|
}
|
|
16451
15920
|
|
|
16452
15921
|
extend(THREE);
|
|
16453
15922
|
|
|
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,
|
|
15923
|
+
export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, 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 };
|