@react-three/fiber 10.0.0-alpha.2 → 10.0.0-canary.164c7be
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/index.cjs +1163 -1375
- package/dist/index.d.cts +1887 -1710
- package/dist/index.d.mts +1887 -1710
- package/dist/index.d.ts +1887 -1710
- package/dist/index.mjs +1124 -1354
- package/dist/legacy.cjs +1129 -1365
- package/dist/legacy.d.cts +1888 -1711
- package/dist/legacy.d.mts +1888 -1711
- package/dist/legacy.d.ts +1888 -1711
- package/dist/legacy.mjs +1090 -1344
- package/dist/webgpu/index.cjs +1720 -1486
- package/dist/webgpu/index.d.cts +2064 -1734
- package/dist/webgpu/index.d.mts +2064 -1734
- package/dist/webgpu/index.d.ts +2064 -1734
- package/dist/webgpu/index.mjs +1678 -1466
- package/package.json +4 -1
- package/readme.md +244 -318
package/dist/webgpu/index.mjs
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import * as webgpu from 'three/webgpu';
|
|
2
|
-
import { RenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1,
|
|
2
|
+
import { RenderTarget, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Scene, WebGLCubeRenderTarget, HalfFloatType, Color, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, Vector4, PostProcessing } from 'three/webgpu';
|
|
3
3
|
import { Inspector } from 'three/addons/inspector/Inspector.js';
|
|
4
|
-
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
-
import React__default, {
|
|
6
|
+
import React__default, { useLayoutEffect, useRef, useMemo, useEffect, useContext, useImperativeHandle, useCallback, useState } from 'react';
|
|
7
7
|
import useMeasure from 'react-use-measure';
|
|
8
8
|
import { useFiber, useContextBridge, traverseFiber, FiberProvider } from 'its-fine';
|
|
9
|
+
import { useThree as useThree$1, useLoader as useLoader$1, useFrame as useFrame$1, createPortal as createPortal$1, applyProps as applyProps$1, extend as extend$1 } from '@react-three/fiber';
|
|
10
|
+
import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
|
|
11
|
+
import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
|
|
12
|
+
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
|
|
13
|
+
import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
|
|
14
|
+
import { GainMapLoader } from '@monogrid/gainmap-js';
|
|
9
15
|
import Tb, { unstable_scheduleCallback, unstable_IdlePriority } from 'scheduler';
|
|
10
16
|
import { createWithEqualityFn } from 'zustand/traditional';
|
|
17
|
+
import { getScheduler } from '@pmndrs/scheduler';
|
|
18
|
+
export { Scheduler, getScheduler } from '@pmndrs/scheduler';
|
|
11
19
|
import { suspend, preload, clear } from 'suspend-react';
|
|
12
20
|
import { dequal } from 'dequal/lite';
|
|
13
21
|
import 'zustand/shallow';
|
|
@@ -46,9 +54,392 @@ const THREE = /*#__PURE__*/_mergeNamespaces({
|
|
|
46
54
|
WebGLRenderer: WebGLRenderer
|
|
47
55
|
}, [webgpu]);
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
const primaryRegistry = /* @__PURE__ */ new Map();
|
|
58
|
+
const pendingSubscribers = /* @__PURE__ */ new Map();
|
|
59
|
+
function registerPrimary(id, renderer, store) {
|
|
60
|
+
if (primaryRegistry.has(id)) {
|
|
61
|
+
console.warn(`Canvas with id="${id}" already registered. Overwriting.`);
|
|
62
|
+
}
|
|
63
|
+
const entry = { renderer, store };
|
|
64
|
+
primaryRegistry.set(id, entry);
|
|
65
|
+
const subscribers = pendingSubscribers.get(id);
|
|
66
|
+
if (subscribers) {
|
|
67
|
+
subscribers.forEach((callback) => callback(entry));
|
|
68
|
+
pendingSubscribers.delete(id);
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
const currentEntry = primaryRegistry.get(id);
|
|
72
|
+
if (currentEntry?.renderer === renderer) {
|
|
73
|
+
primaryRegistry.delete(id);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function getPrimary(id) {
|
|
78
|
+
return primaryRegistry.get(id);
|
|
79
|
+
}
|
|
80
|
+
function waitForPrimary(id, timeout = 5e3) {
|
|
81
|
+
const existing = primaryRegistry.get(id);
|
|
82
|
+
if (existing) {
|
|
83
|
+
return Promise.resolve(existing);
|
|
84
|
+
}
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const timeoutId = setTimeout(() => {
|
|
87
|
+
const subscribers = pendingSubscribers.get(id);
|
|
88
|
+
if (subscribers) {
|
|
89
|
+
const index = subscribers.indexOf(callback);
|
|
90
|
+
if (index !== -1) subscribers.splice(index, 1);
|
|
91
|
+
if (subscribers.length === 0) pendingSubscribers.delete(id);
|
|
92
|
+
}
|
|
93
|
+
reject(new Error(`Timeout waiting for canvas with id="${id}". Make sure a <Canvas id="${id}"> is mounted.`));
|
|
94
|
+
}, timeout);
|
|
95
|
+
const callback = (entry) => {
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
resolve(entry);
|
|
98
|
+
};
|
|
99
|
+
if (!pendingSubscribers.has(id)) {
|
|
100
|
+
pendingSubscribers.set(id, []);
|
|
101
|
+
}
|
|
102
|
+
pendingSubscribers.get(id).push(callback);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function hasPrimary(id) {
|
|
106
|
+
return primaryRegistry.has(id);
|
|
107
|
+
}
|
|
108
|
+
function unregisterPrimary(id) {
|
|
109
|
+
primaryRegistry.delete(id);
|
|
110
|
+
}
|
|
111
|
+
function getPrimaryIds() {
|
|
112
|
+
return Array.from(primaryRegistry.keys());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const presetsObj = {
|
|
116
|
+
apartment: "lebombo_1k.hdr",
|
|
117
|
+
city: "potsdamer_platz_1k.hdr",
|
|
118
|
+
dawn: "kiara_1_dawn_1k.hdr",
|
|
119
|
+
forest: "forest_slope_1k.hdr",
|
|
120
|
+
lobby: "st_fagans_interior_1k.hdr",
|
|
121
|
+
night: "dikhololo_night_1k.hdr",
|
|
122
|
+
park: "rooitou_park_1k.hdr",
|
|
123
|
+
studio: "studio_small_03_1k.hdr",
|
|
124
|
+
sunset: "venice_sunset_1k.hdr",
|
|
125
|
+
warehouse: "empty_warehouse_01_1k.hdr"
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const CUBEMAP_ROOT = "https://raw.githack.com/pmndrs/drei-assets/456060a26bbeb8fdf79326f224b6d99b8bcce736/hdri/";
|
|
129
|
+
const isArray = (arr) => Array.isArray(arr);
|
|
130
|
+
const defaultFiles = ["/px.png", "/nx.png", "/py.png", "/ny.png", "/pz.png", "/nz.png"];
|
|
131
|
+
function useEnvironment({
|
|
132
|
+
files = defaultFiles,
|
|
133
|
+
path = "",
|
|
134
|
+
preset = void 0,
|
|
135
|
+
colorSpace = void 0,
|
|
136
|
+
extensions
|
|
137
|
+
} = {}) {
|
|
138
|
+
if (preset) {
|
|
139
|
+
validatePreset(preset);
|
|
140
|
+
files = presetsObj[preset];
|
|
141
|
+
path = CUBEMAP_ROOT;
|
|
142
|
+
}
|
|
143
|
+
const multiFile = isArray(files);
|
|
144
|
+
const { extension, isCubemap } = getExtension(files);
|
|
145
|
+
const loader = getLoader$1(extension);
|
|
146
|
+
if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
|
|
147
|
+
const renderer = useThree$1((state) => state.renderer);
|
|
148
|
+
useLayoutEffect(() => {
|
|
149
|
+
if (extension !== "webp" && extension !== "jpg" && extension !== "jpeg") return;
|
|
150
|
+
function clearGainmapTexture() {
|
|
151
|
+
useLoader$1.clear(loader, multiFile ? [files] : files);
|
|
152
|
+
}
|
|
153
|
+
renderer.domElement.addEventListener("webglcontextlost", clearGainmapTexture, { once: true });
|
|
154
|
+
}, [extension, files, loader, multiFile, renderer.domElement]);
|
|
155
|
+
const loaderResult = useLoader$1(
|
|
156
|
+
loader,
|
|
157
|
+
multiFile ? [files] : files,
|
|
158
|
+
(loader2) => {
|
|
159
|
+
if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
|
|
160
|
+
loader2.setRenderer?.(renderer);
|
|
161
|
+
}
|
|
162
|
+
loader2.setPath?.(path);
|
|
163
|
+
if (extensions) extensions(loader2);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
let texture = multiFile ? (
|
|
167
|
+
// @ts-ignore
|
|
168
|
+
loaderResult[0]
|
|
169
|
+
) : loaderResult;
|
|
170
|
+
if (extension === "jpg" || extension === "jpeg" || extension === "webp") {
|
|
171
|
+
texture = texture.renderTarget?.texture;
|
|
172
|
+
}
|
|
173
|
+
texture.mapping = isCubemap ? CubeReflectionMapping : EquirectangularReflectionMapping;
|
|
174
|
+
texture.colorSpace = colorSpace ?? (isCubemap ? "srgb" : "srgb-linear");
|
|
175
|
+
return texture;
|
|
176
|
+
}
|
|
177
|
+
const preloadDefaultOptions = {
|
|
178
|
+
files: defaultFiles,
|
|
179
|
+
path: "",
|
|
180
|
+
preset: void 0,
|
|
181
|
+
extensions: void 0
|
|
182
|
+
};
|
|
183
|
+
useEnvironment.preload = (preloadOptions) => {
|
|
184
|
+
const options = { ...preloadDefaultOptions, ...preloadOptions };
|
|
185
|
+
let { files, path = "" } = options;
|
|
186
|
+
const { preset, extensions } = options;
|
|
187
|
+
if (preset) {
|
|
188
|
+
validatePreset(preset);
|
|
189
|
+
files = presetsObj[preset];
|
|
190
|
+
path = CUBEMAP_ROOT;
|
|
191
|
+
}
|
|
192
|
+
const { extension } = getExtension(files);
|
|
193
|
+
if (extension === "webp" || extension === "jpg" || extension === "jpeg") {
|
|
194
|
+
throw new Error("useEnvironment: Preloading gainmaps is not supported");
|
|
195
|
+
}
|
|
196
|
+
const loader = getLoader$1(extension);
|
|
197
|
+
if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
|
|
198
|
+
useLoader$1.preload(loader, isArray(files) ? [files] : files, (loader2) => {
|
|
199
|
+
loader2.setPath?.(path);
|
|
200
|
+
if (extensions) extensions(loader2);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const clearDefaultOptins = {
|
|
204
|
+
files: defaultFiles,
|
|
205
|
+
preset: void 0
|
|
206
|
+
};
|
|
207
|
+
useEnvironment.clear = (clearOptions) => {
|
|
208
|
+
const options = { ...clearDefaultOptins, ...clearOptions };
|
|
209
|
+
let { files } = options;
|
|
210
|
+
const { preset } = options;
|
|
211
|
+
if (preset) {
|
|
212
|
+
validatePreset(preset);
|
|
213
|
+
files = presetsObj[preset];
|
|
214
|
+
}
|
|
215
|
+
const { extension } = getExtension(files);
|
|
216
|
+
const loader = getLoader$1(extension);
|
|
217
|
+
if (!loader) throw new Error("useEnvironment: Unrecognized file extension: " + files);
|
|
218
|
+
useLoader$1.clear(loader, isArray(files) ? [files] : files);
|
|
219
|
+
};
|
|
220
|
+
function validatePreset(preset) {
|
|
221
|
+
if (!(preset in presetsObj)) throw new Error("Preset must be one of: " + Object.keys(presetsObj).join(", "));
|
|
222
|
+
}
|
|
223
|
+
function getExtension(files) {
|
|
224
|
+
const isCubemap = isArray(files) && files.length === 6;
|
|
225
|
+
const isGainmap = isArray(files) && files.length === 3 && files.some((file) => file.endsWith("json"));
|
|
226
|
+
const firstEntry = isArray(files) ? files[0] : files;
|
|
227
|
+
const extension = isCubemap ? "cube" : isGainmap ? "webp" : firstEntry.startsWith("data:application/exr") ? "exr" : firstEntry.startsWith("data:application/hdr") ? "hdr" : firstEntry.startsWith("data:image/jpeg") ? "jpg" : firstEntry.split(".").pop()?.split("?")?.shift()?.toLowerCase();
|
|
228
|
+
return { extension, isCubemap, isGainmap };
|
|
229
|
+
}
|
|
230
|
+
function getLoader$1(extension) {
|
|
231
|
+
const loader = extension === "cube" ? CubeTextureLoader : extension === "hdr" ? HDRLoader : extension === "exr" ? EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader : extension === "webp" ? GainMapLoader : null;
|
|
232
|
+
return loader;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const isRef$1 = (obj) => obj.current && obj.current.isScene;
|
|
236
|
+
const resolveScene = (scene) => isRef$1(scene) ? scene.current : scene;
|
|
237
|
+
function setEnvProps(background, scene, defaultScene, texture, sceneProps = {}) {
|
|
238
|
+
sceneProps = {
|
|
239
|
+
backgroundBlurriness: 0,
|
|
240
|
+
backgroundIntensity: 1,
|
|
241
|
+
backgroundRotation: [0, 0, 0],
|
|
242
|
+
environmentIntensity: 1,
|
|
243
|
+
environmentRotation: [0, 0, 0],
|
|
244
|
+
...sceneProps
|
|
245
|
+
};
|
|
246
|
+
const target = resolveScene(scene || defaultScene);
|
|
247
|
+
const oldbg = target.background;
|
|
248
|
+
const oldenv = target.environment;
|
|
249
|
+
const oldSceneProps = {
|
|
250
|
+
// @ts-ignore
|
|
251
|
+
backgroundBlurriness: target.backgroundBlurriness,
|
|
252
|
+
// @ts-ignore
|
|
253
|
+
backgroundIntensity: target.backgroundIntensity,
|
|
254
|
+
// @ts-ignore
|
|
255
|
+
backgroundRotation: target.backgroundRotation?.clone?.() ?? [0, 0, 0],
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
environmentIntensity: target.environmentIntensity,
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
environmentRotation: target.environmentRotation?.clone?.() ?? [0, 0, 0]
|
|
260
|
+
};
|
|
261
|
+
if (background !== "only") target.environment = texture;
|
|
262
|
+
if (background) target.background = texture;
|
|
263
|
+
applyProps$1(target, sceneProps);
|
|
264
|
+
return () => {
|
|
265
|
+
if (background !== "only") target.environment = oldenv;
|
|
266
|
+
if (background) target.background = oldbg;
|
|
267
|
+
applyProps$1(target, oldSceneProps);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function EnvironmentMap({ scene, background = false, map, ...config }) {
|
|
271
|
+
const defaultScene = useThree$1((state) => state.scene);
|
|
272
|
+
React.useLayoutEffect(() => {
|
|
273
|
+
if (map) return setEnvProps(background, scene, defaultScene, map, config);
|
|
274
|
+
});
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function EnvironmentCube({
|
|
278
|
+
background = false,
|
|
279
|
+
scene,
|
|
280
|
+
blur,
|
|
281
|
+
backgroundBlurriness,
|
|
282
|
+
backgroundIntensity,
|
|
283
|
+
backgroundRotation,
|
|
284
|
+
environmentIntensity,
|
|
285
|
+
environmentRotation,
|
|
286
|
+
...rest
|
|
287
|
+
}) {
|
|
288
|
+
const texture = useEnvironment(rest);
|
|
289
|
+
const defaultScene = useThree$1((state) => state.scene);
|
|
290
|
+
React.useLayoutEffect(() => {
|
|
291
|
+
return setEnvProps(background, scene, defaultScene, texture, {
|
|
292
|
+
backgroundBlurriness: blur ?? backgroundBlurriness,
|
|
293
|
+
backgroundIntensity,
|
|
294
|
+
backgroundRotation,
|
|
295
|
+
environmentIntensity,
|
|
296
|
+
environmentRotation
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
React.useEffect(() => {
|
|
300
|
+
return () => {
|
|
301
|
+
texture.dispose();
|
|
302
|
+
};
|
|
303
|
+
}, [texture]);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function EnvironmentPortal({
|
|
307
|
+
children,
|
|
308
|
+
near = 0.1,
|
|
309
|
+
far = 1e3,
|
|
310
|
+
resolution = 256,
|
|
311
|
+
frames = 1,
|
|
312
|
+
map,
|
|
313
|
+
background = false,
|
|
314
|
+
blur,
|
|
315
|
+
backgroundBlurriness,
|
|
316
|
+
backgroundIntensity,
|
|
317
|
+
backgroundRotation,
|
|
318
|
+
environmentIntensity,
|
|
319
|
+
environmentRotation,
|
|
320
|
+
scene,
|
|
321
|
+
files,
|
|
322
|
+
path,
|
|
323
|
+
preset = void 0,
|
|
324
|
+
extensions
|
|
325
|
+
}) {
|
|
326
|
+
const gl = useThree$1((state) => state.gl);
|
|
327
|
+
const defaultScene = useThree$1((state) => state.scene);
|
|
328
|
+
const camera = React.useRef(null);
|
|
329
|
+
const [virtualScene] = React.useState(() => new Scene());
|
|
330
|
+
const fbo = React.useMemo(() => {
|
|
331
|
+
const fbo2 = new WebGLCubeRenderTarget(resolution);
|
|
332
|
+
fbo2.texture.type = HalfFloatType;
|
|
333
|
+
return fbo2;
|
|
334
|
+
}, [resolution]);
|
|
335
|
+
React.useEffect(() => {
|
|
336
|
+
return () => {
|
|
337
|
+
fbo.dispose();
|
|
338
|
+
};
|
|
339
|
+
}, [fbo]);
|
|
340
|
+
React.useLayoutEffect(() => {
|
|
341
|
+
if (frames === 1) {
|
|
342
|
+
const autoClear = gl.autoClear;
|
|
343
|
+
gl.autoClear = true;
|
|
344
|
+
camera.current.update(gl, virtualScene);
|
|
345
|
+
gl.autoClear = autoClear;
|
|
346
|
+
}
|
|
347
|
+
return setEnvProps(background, scene, defaultScene, fbo.texture, {
|
|
348
|
+
backgroundBlurriness: blur ?? backgroundBlurriness,
|
|
349
|
+
backgroundIntensity,
|
|
350
|
+
backgroundRotation,
|
|
351
|
+
environmentIntensity,
|
|
352
|
+
environmentRotation
|
|
353
|
+
});
|
|
354
|
+
}, [
|
|
355
|
+
children,
|
|
356
|
+
virtualScene,
|
|
357
|
+
fbo.texture,
|
|
358
|
+
scene,
|
|
359
|
+
defaultScene,
|
|
360
|
+
background,
|
|
361
|
+
frames,
|
|
362
|
+
gl,
|
|
363
|
+
blur,
|
|
364
|
+
backgroundBlurriness,
|
|
365
|
+
backgroundIntensity,
|
|
366
|
+
backgroundRotation,
|
|
367
|
+
environmentIntensity,
|
|
368
|
+
environmentRotation
|
|
369
|
+
]);
|
|
370
|
+
let count = 1;
|
|
371
|
+
useFrame$1(() => {
|
|
372
|
+
if (frames === Infinity || count < frames) {
|
|
373
|
+
const autoClear = gl.autoClear;
|
|
374
|
+
gl.autoClear = true;
|
|
375
|
+
camera.current.update(gl, virtualScene);
|
|
376
|
+
gl.autoClear = autoClear;
|
|
377
|
+
count++;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return /* @__PURE__ */ jsx(Fragment, { children: createPortal$1(
|
|
381
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
382
|
+
children,
|
|
383
|
+
/* @__PURE__ */ jsx("cubeCamera", { ref: camera, args: [near, far, fbo] }),
|
|
384
|
+
files || preset ? /* @__PURE__ */ jsx(EnvironmentCube, { background: true, files, preset, path, extensions }) : map ? /* @__PURE__ */ jsx(EnvironmentMap, { background: true, map, extensions }) : null
|
|
385
|
+
] }),
|
|
386
|
+
virtualScene
|
|
387
|
+
) });
|
|
388
|
+
}
|
|
389
|
+
function EnvironmentGround(props) {
|
|
390
|
+
const textureDefault = useEnvironment(props);
|
|
391
|
+
const texture = props.map || textureDefault;
|
|
392
|
+
React.useMemo(() => extend$1({ GroundProjectedEnvImpl: GroundedSkybox }), []);
|
|
393
|
+
React.useEffect(() => {
|
|
394
|
+
return () => {
|
|
395
|
+
textureDefault.dispose();
|
|
396
|
+
};
|
|
397
|
+
}, [textureDefault]);
|
|
398
|
+
const height = props.ground?.height ?? 15;
|
|
399
|
+
const radius = props.ground?.radius ?? 60;
|
|
400
|
+
const scale = props.ground?.scale ?? 1e3;
|
|
401
|
+
const args = React.useMemo(
|
|
402
|
+
() => [texture, height, radius],
|
|
403
|
+
[texture, height, radius]
|
|
404
|
+
);
|
|
405
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
406
|
+
/* @__PURE__ */ jsx(EnvironmentMap, { ...props, map: texture }),
|
|
407
|
+
/* @__PURE__ */ jsx("groundProjectedEnvImpl", { args, scale })
|
|
408
|
+
] });
|
|
409
|
+
}
|
|
410
|
+
function EnvironmentColor({ color, scene }) {
|
|
411
|
+
const defaultScene = useThree$1((state) => state.scene);
|
|
412
|
+
React.useLayoutEffect(() => {
|
|
413
|
+
if (color === void 0) return;
|
|
414
|
+
const target = resolveScene(scene || defaultScene);
|
|
415
|
+
const oldBg = target.background;
|
|
416
|
+
target.background = new Color(color);
|
|
417
|
+
return () => {
|
|
418
|
+
target.background = oldBg;
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
function EnvironmentDualSource(props) {
|
|
424
|
+
const { backgroundFiles, ...envProps } = props;
|
|
425
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
426
|
+
/* @__PURE__ */ jsx(EnvironmentCube, { ...envProps, background: false }),
|
|
427
|
+
/* @__PURE__ */ jsx(EnvironmentCube, { ...props, files: backgroundFiles, background: "only" })
|
|
428
|
+
] });
|
|
429
|
+
}
|
|
430
|
+
function Environment(props) {
|
|
431
|
+
if (props.color && !props.files && !props.preset && !props.map) {
|
|
432
|
+
return /* @__PURE__ */ jsx(EnvironmentColor, { ...props });
|
|
433
|
+
}
|
|
434
|
+
if (props.backgroundFiles && props.backgroundFiles !== props.files) {
|
|
435
|
+
return /* @__PURE__ */ jsx(EnvironmentDualSource, { ...props });
|
|
436
|
+
}
|
|
437
|
+
return props.ground ? /* @__PURE__ */ jsx(EnvironmentGround, { ...props }) : props.map ? /* @__PURE__ */ jsx(EnvironmentMap, { ...props }) : props.children ? /* @__PURE__ */ jsx(EnvironmentPortal, { ...props }) : /* @__PURE__ */ jsx(EnvironmentCube, { ...props });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
var __defProp$1 = Object.defineProperty;
|
|
441
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
442
|
+
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
52
443
|
const act = React["act"];
|
|
53
444
|
const useIsomorphicLayoutEffect = /* @__PURE__ */ (() => typeof window !== "undefined" && (window.document?.createElement || window.navigator?.product === "ReactNative"))() ? React.useLayoutEffect : React.useEffect;
|
|
54
445
|
function useMutableCallback(fn) {
|
|
@@ -80,7 +471,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
|
|
|
80
471
|
return _a = class extends React.Component {
|
|
81
472
|
constructor() {
|
|
82
473
|
super(...arguments);
|
|
83
|
-
__publicField$
|
|
474
|
+
__publicField$1(this, "state", { error: false });
|
|
84
475
|
}
|
|
85
476
|
componentDidCatch(err) {
|
|
86
477
|
this.props.set(err);
|
|
@@ -88,7 +479,7 @@ const ErrorBoundary = /* @__PURE__ */ (() => {
|
|
|
88
479
|
render() {
|
|
89
480
|
return this.state.error ? null : this.props.children;
|
|
90
481
|
}
|
|
91
|
-
}, __publicField$
|
|
482
|
+
}, __publicField$1(_a, "getDerivedStateFromError", () => ({ error: true })), _a;
|
|
92
483
|
})();
|
|
93
484
|
|
|
94
485
|
const is = {
|
|
@@ -225,7 +616,8 @@ function prepare(target, root, type, props) {
|
|
|
225
616
|
object,
|
|
226
617
|
eventCount: 0,
|
|
227
618
|
handlers: {},
|
|
228
|
-
isHidden: false
|
|
619
|
+
isHidden: false,
|
|
620
|
+
deferredRefs: []
|
|
229
621
|
};
|
|
230
622
|
if (object) object.__r3f = instance;
|
|
231
623
|
}
|
|
@@ -274,7 +666,7 @@ function createOcclusionObserverNode(store, uniform) {
|
|
|
274
666
|
let occlusionSetupPromise = null;
|
|
275
667
|
function enableOcclusion(store) {
|
|
276
668
|
const state = store.getState();
|
|
277
|
-
const { internal, renderer
|
|
669
|
+
const { internal, renderer } = state;
|
|
278
670
|
if (internal.occlusionEnabled || occlusionSetupPromise) return;
|
|
279
671
|
const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
|
|
280
672
|
if (!hasOcclusionSupport) {
|
|
@@ -437,6 +829,22 @@ function hasVisibilityHandlers(handlers) {
|
|
|
437
829
|
return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
|
|
438
830
|
}
|
|
439
831
|
|
|
832
|
+
const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
|
|
833
|
+
function fromRef(ref) {
|
|
834
|
+
return { [FROM_REF]: ref };
|
|
835
|
+
}
|
|
836
|
+
function isFromRef(value) {
|
|
837
|
+
return value !== null && typeof value === "object" && FROM_REF in value;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const ONCE = Symbol.for("@react-three/fiber.once");
|
|
841
|
+
function once(...args) {
|
|
842
|
+
return { [ONCE]: args.length ? args : true };
|
|
843
|
+
}
|
|
844
|
+
function isOnce(value) {
|
|
845
|
+
return value !== null && typeof value === "object" && ONCE in value;
|
|
846
|
+
}
|
|
847
|
+
|
|
440
848
|
const RESERVED_PROPS = [
|
|
441
849
|
"children",
|
|
442
850
|
"key",
|
|
@@ -507,7 +915,7 @@ function getMemoizedPrototype(root) {
|
|
|
507
915
|
ctor = new root.constructor();
|
|
508
916
|
MEMOIZED_PROTOTYPES.set(root.constructor, ctor);
|
|
509
917
|
}
|
|
510
|
-
} catch
|
|
918
|
+
} catch {
|
|
511
919
|
}
|
|
512
920
|
return ctor;
|
|
513
921
|
}
|
|
@@ -553,6 +961,25 @@ function applyProps(object, props) {
|
|
|
553
961
|
continue;
|
|
554
962
|
}
|
|
555
963
|
if (value === void 0) continue;
|
|
964
|
+
if (isFromRef(value)) {
|
|
965
|
+
instance?.deferredRefs?.push({ prop, ref: value[FROM_REF] });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (isOnce(value)) {
|
|
969
|
+
if (instance?.appliedOnce?.has(prop)) continue;
|
|
970
|
+
if (instance) {
|
|
971
|
+
instance.appliedOnce ?? (instance.appliedOnce = /* @__PURE__ */ new Set());
|
|
972
|
+
instance.appliedOnce.add(prop);
|
|
973
|
+
}
|
|
974
|
+
const { root: targetRoot, key: targetKey } = resolve(object, prop);
|
|
975
|
+
const args = value[ONCE];
|
|
976
|
+
if (typeof targetRoot[targetKey] === "function") {
|
|
977
|
+
targetRoot[targetKey](...args === true ? [] : args);
|
|
978
|
+
} else if (args !== true && args.length > 0) {
|
|
979
|
+
targetRoot[targetKey] = args[0];
|
|
980
|
+
}
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
556
983
|
let { root, key, target } = resolve(object, prop);
|
|
557
984
|
if (target === void 0 && (typeof root !== "object" || root === null)) {
|
|
558
985
|
throw Error(`R3F: Cannot set "${prop}". Ensure it is an object before setting "${key}".`);
|
|
@@ -575,7 +1002,10 @@ function applyProps(object, props) {
|
|
|
575
1002
|
else target.set(value);
|
|
576
1003
|
} else {
|
|
577
1004
|
root[key] = value;
|
|
578
|
-
if (
|
|
1005
|
+
if (key.endsWith("Node") && root.isMaterial) {
|
|
1006
|
+
root.needsUpdate = true;
|
|
1007
|
+
}
|
|
1008
|
+
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
|
|
579
1009
|
root[key].format === RGBAFormat && root[key].type === UnsignedByteType) {
|
|
580
1010
|
root[key].colorSpace = rootState.textureColorSpace;
|
|
581
1011
|
}
|
|
@@ -608,38 +1038,60 @@ function applyProps(object, props) {
|
|
|
608
1038
|
return object;
|
|
609
1039
|
}
|
|
610
1040
|
|
|
1041
|
+
const DEFAULT_POINTER_ID = 0;
|
|
1042
|
+
const XR_POINTER_ID_START = 1e3;
|
|
1043
|
+
function getPointerState(internal, pointerId) {
|
|
1044
|
+
let state = internal.pointerMap.get(pointerId);
|
|
1045
|
+
if (!state) {
|
|
1046
|
+
state = {
|
|
1047
|
+
hovered: /* @__PURE__ */ new Map(),
|
|
1048
|
+
captured: /* @__PURE__ */ new Map(),
|
|
1049
|
+
initialClick: [0, 0],
|
|
1050
|
+
initialHits: []
|
|
1051
|
+
};
|
|
1052
|
+
internal.pointerMap.set(pointerId, state);
|
|
1053
|
+
}
|
|
1054
|
+
return state;
|
|
1055
|
+
}
|
|
1056
|
+
function getPointerId(event) {
|
|
1057
|
+
return "pointerId" in event ? event.pointerId : DEFAULT_POINTER_ID;
|
|
1058
|
+
}
|
|
611
1059
|
function makeId(event) {
|
|
612
1060
|
return (event.eventObject || event.object).uuid + "/" + event.index + event.instanceId;
|
|
613
1061
|
}
|
|
614
|
-
function releaseInternalPointerCapture(
|
|
615
|
-
const
|
|
1062
|
+
function releaseInternalPointerCapture(internal, obj, pointerId) {
|
|
1063
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1064
|
+
if (!pointerState) return;
|
|
1065
|
+
const captureData = pointerState.captured.get(obj);
|
|
616
1066
|
if (captureData) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
capturedMap.delete(pointerId);
|
|
620
|
-
captureData.target.releasePointerCapture(pointerId);
|
|
621
|
-
}
|
|
1067
|
+
pointerState.captured.delete(obj);
|
|
1068
|
+
captureData.target.releasePointerCapture(pointerId);
|
|
622
1069
|
}
|
|
623
1070
|
}
|
|
624
1071
|
function removeInteractivity(store, object) {
|
|
625
1072
|
const { internal } = store.getState();
|
|
626
1073
|
internal.interaction = internal.interaction.filter((o) => o !== object);
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1074
|
+
for (const [pointerId, pointerState] of internal.pointerMap) {
|
|
1075
|
+
pointerState.initialHits = pointerState.initialHits.filter((o) => o !== object);
|
|
1076
|
+
pointerState.hovered.forEach((value, key) => {
|
|
1077
|
+
if (value.eventObject === object || value.object === object) {
|
|
1078
|
+
pointerState.hovered.delete(key);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
if (pointerState.captured.has(object)) {
|
|
1082
|
+
releaseInternalPointerCapture(internal, object, pointerId);
|
|
631
1083
|
}
|
|
632
|
-
}
|
|
633
|
-
internal.capturedMap.forEach((captures, pointerId) => {
|
|
634
|
-
releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
|
|
635
|
-
});
|
|
1084
|
+
}
|
|
636
1085
|
unregisterVisibility(store, object);
|
|
637
1086
|
}
|
|
638
1087
|
function createEvents(store) {
|
|
639
|
-
function calculateDistance(event) {
|
|
1088
|
+
function calculateDistance(event, pointerId) {
|
|
640
1089
|
const { internal } = store.getState();
|
|
641
|
-
const
|
|
642
|
-
|
|
1090
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1091
|
+
if (!pointerState) return 0;
|
|
1092
|
+
const [initialX, initialY] = pointerState.initialClick;
|
|
1093
|
+
const dx = event.offsetX - initialX;
|
|
1094
|
+
const dy = event.offsetY - initialY;
|
|
643
1095
|
return Math.round(Math.sqrt(dx * dx + dy * dy));
|
|
644
1096
|
}
|
|
645
1097
|
function filterPointerEvents(objects) {
|
|
@@ -675,6 +1127,15 @@ function createEvents(store) {
|
|
|
675
1127
|
return state2.raycaster.camera ? state2.raycaster.intersectObject(obj, true) : [];
|
|
676
1128
|
}
|
|
677
1129
|
let hits = eventsObjects.flatMap(handleRaycast).sort((a, b) => {
|
|
1130
|
+
const aInteractivePriority = a.object.userData?.interactivePriority;
|
|
1131
|
+
const bInteractivePriority = b.object.userData?.interactivePriority;
|
|
1132
|
+
if (aInteractivePriority !== void 0 || bInteractivePriority !== void 0) {
|
|
1133
|
+
if (aInteractivePriority !== void 0 && bInteractivePriority === void 0) return -1;
|
|
1134
|
+
if (bInteractivePriority !== void 0 && aInteractivePriority === void 0) return 1;
|
|
1135
|
+
if (aInteractivePriority !== bInteractivePriority) {
|
|
1136
|
+
return (bInteractivePriority ?? 0) - (aInteractivePriority ?? 0);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
678
1139
|
const aState = getRootState(a.object);
|
|
679
1140
|
const bState = getRootState(b.object);
|
|
680
1141
|
const aPriority = aState?.events?.priority ?? 1;
|
|
@@ -696,9 +1157,13 @@ function createEvents(store) {
|
|
|
696
1157
|
eventObject = eventObject.parent;
|
|
697
1158
|
}
|
|
698
1159
|
}
|
|
699
|
-
if ("pointerId" in event
|
|
700
|
-
|
|
701
|
-
|
|
1160
|
+
if ("pointerId" in event) {
|
|
1161
|
+
const pointerId = event.pointerId;
|
|
1162
|
+
const pointerState = state.internal.pointerMap.get(pointerId);
|
|
1163
|
+
if (pointerState?.captured.size) {
|
|
1164
|
+
for (const captureData of pointerState.captured.values()) {
|
|
1165
|
+
if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
|
|
1166
|
+
}
|
|
702
1167
|
}
|
|
703
1168
|
}
|
|
704
1169
|
return intersections;
|
|
@@ -711,27 +1176,25 @@ function createEvents(store) {
|
|
|
711
1176
|
if (state) {
|
|
712
1177
|
const { raycaster, pointer, camera, internal } = state;
|
|
713
1178
|
const unprojectedPoint = new Vector3(pointer.x, pointer.y, 0).unproject(camera);
|
|
714
|
-
const hasPointerCapture = (id) =>
|
|
1179
|
+
const hasPointerCapture = (id) => {
|
|
1180
|
+
const pointerState = internal.pointerMap.get(id);
|
|
1181
|
+
return pointerState?.captured.has(hit.eventObject) ?? false;
|
|
1182
|
+
};
|
|
715
1183
|
const setPointerCapture = (id) => {
|
|
716
1184
|
const captureData = { intersection: hit, target: event.target };
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
} else {
|
|
720
|
-
internal.capturedMap.set(id, /* @__PURE__ */ new Map([[hit.eventObject, captureData]]));
|
|
721
|
-
}
|
|
1185
|
+
const pointerState = getPointerState(internal, id);
|
|
1186
|
+
pointerState.captured.set(hit.eventObject, captureData);
|
|
722
1187
|
event.target.setPointerCapture(id);
|
|
723
1188
|
};
|
|
724
1189
|
const releasePointerCapture = (id) => {
|
|
725
|
-
|
|
726
|
-
if (captures) {
|
|
727
|
-
releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
|
|
728
|
-
}
|
|
1190
|
+
releaseInternalPointerCapture(internal, hit.eventObject, id);
|
|
729
1191
|
};
|
|
730
1192
|
const extractEventProps = {};
|
|
731
1193
|
for (const prop in event) {
|
|
732
1194
|
const property = event[prop];
|
|
733
1195
|
if (typeof property !== "function") extractEventProps[prop] = property;
|
|
734
1196
|
}
|
|
1197
|
+
const eventPointerId = "pointerId" in event ? event.pointerId : void 0;
|
|
735
1198
|
const raycastEvent = {
|
|
736
1199
|
...hit,
|
|
737
1200
|
...extractEventProps,
|
|
@@ -742,18 +1205,19 @@ function createEvents(store) {
|
|
|
742
1205
|
unprojectedPoint,
|
|
743
1206
|
ray: raycaster.ray,
|
|
744
1207
|
camera,
|
|
1208
|
+
pointerId: eventPointerId,
|
|
745
1209
|
// Hijack stopPropagation, which just sets a flag
|
|
746
1210
|
stopPropagation() {
|
|
747
|
-
const
|
|
1211
|
+
const pointerState = eventPointerId !== void 0 ? internal.pointerMap.get(eventPointerId) : void 0;
|
|
748
1212
|
if (
|
|
749
1213
|
// ...if this pointer hasn't been captured
|
|
750
|
-
!
|
|
751
|
-
|
|
1214
|
+
!pointerState?.captured.size || // ... or if the hit object is capturing the pointer
|
|
1215
|
+
pointerState.captured.has(hit.eventObject)
|
|
752
1216
|
) {
|
|
753
1217
|
raycastEvent.stopped = localState.stopped = true;
|
|
754
|
-
if (
|
|
1218
|
+
if (pointerState?.hovered.size && Array.from(pointerState.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
|
|
755
1219
|
const higher = intersections.slice(0, intersections.indexOf(hit));
|
|
756
|
-
cancelPointer([...higher, hit]);
|
|
1220
|
+
cancelPointer([...higher, hit], eventPointerId);
|
|
757
1221
|
}
|
|
758
1222
|
}
|
|
759
1223
|
},
|
|
@@ -769,15 +1233,18 @@ function createEvents(store) {
|
|
|
769
1233
|
}
|
|
770
1234
|
return intersections;
|
|
771
1235
|
}
|
|
772
|
-
function cancelPointer(intersections) {
|
|
1236
|
+
function cancelPointer(intersections, pointerId) {
|
|
773
1237
|
const { internal } = store.getState();
|
|
774
|
-
|
|
1238
|
+
const pid = pointerId ?? DEFAULT_POINTER_ID;
|
|
1239
|
+
const pointerState = internal.pointerMap.get(pid);
|
|
1240
|
+
if (!pointerState) return;
|
|
1241
|
+
for (const [hoveredId, hoveredObj] of pointerState.hovered) {
|
|
775
1242
|
if (!intersections.length || !intersections.find(
|
|
776
1243
|
(hit) => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId
|
|
777
1244
|
)) {
|
|
778
1245
|
const eventObject = hoveredObj.eventObject;
|
|
779
1246
|
const instance = eventObject.__r3f;
|
|
780
|
-
|
|
1247
|
+
pointerState.hovered.delete(hoveredId);
|
|
781
1248
|
if (instance?.eventCount) {
|
|
782
1249
|
const handlers = instance.handlers;
|
|
783
1250
|
const data = { ...hoveredObj, intersections };
|
|
@@ -806,41 +1273,118 @@ function createEvents(store) {
|
|
|
806
1273
|
instance?.handlers.onDropMissed?.(event);
|
|
807
1274
|
}
|
|
808
1275
|
}
|
|
1276
|
+
function cleanupPointer(pointerId) {
|
|
1277
|
+
const { internal } = store.getState();
|
|
1278
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1279
|
+
if (pointerState) {
|
|
1280
|
+
for (const [, hoveredObj] of pointerState.hovered) {
|
|
1281
|
+
const eventObject = hoveredObj.eventObject;
|
|
1282
|
+
const instance = eventObject.__r3f;
|
|
1283
|
+
if (instance?.eventCount) {
|
|
1284
|
+
const handlers = instance.handlers;
|
|
1285
|
+
const data = { ...hoveredObj, intersections: [] };
|
|
1286
|
+
handlers.onPointerOut?.(data);
|
|
1287
|
+
handlers.onPointerLeave?.(data);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
internal.pointerMap.delete(pointerId);
|
|
1291
|
+
}
|
|
1292
|
+
internal.pointerDirty.delete(pointerId);
|
|
1293
|
+
}
|
|
1294
|
+
function processDeferredPointer(event, pointerId) {
|
|
1295
|
+
const state = store.getState();
|
|
1296
|
+
const { internal } = state;
|
|
1297
|
+
if (!state.events.enabled) return;
|
|
1298
|
+
const filter = filterPointerEvents;
|
|
1299
|
+
const hits = intersect(event, filter);
|
|
1300
|
+
cancelPointer(hits, pointerId);
|
|
1301
|
+
function onIntersect(data) {
|
|
1302
|
+
const eventObject = data.eventObject;
|
|
1303
|
+
const instance = eventObject.__r3f;
|
|
1304
|
+
if (!instance?.eventCount) return;
|
|
1305
|
+
const handlers = instance.handlers;
|
|
1306
|
+
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
|
|
1307
|
+
const id = makeId(data);
|
|
1308
|
+
const pointerState = getPointerState(internal, pointerId);
|
|
1309
|
+
const hoveredItem = pointerState.hovered.get(id);
|
|
1310
|
+
if (!hoveredItem) {
|
|
1311
|
+
pointerState.hovered.set(id, data);
|
|
1312
|
+
handlers.onPointerOver?.(data);
|
|
1313
|
+
handlers.onPointerEnter?.(data);
|
|
1314
|
+
} else if (hoveredItem.stopped) {
|
|
1315
|
+
data.stopPropagation();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
handlers.onPointerMove?.(data);
|
|
1319
|
+
}
|
|
1320
|
+
handleIntersects(hits, event, 0, onIntersect);
|
|
1321
|
+
}
|
|
809
1322
|
function handlePointer(name) {
|
|
810
1323
|
switch (name) {
|
|
811
1324
|
case "onPointerLeave":
|
|
812
|
-
case "onPointerCancel":
|
|
813
1325
|
case "onDragLeave":
|
|
814
1326
|
return () => cancelPointer([]);
|
|
1327
|
+
// Global cancel of these events
|
|
1328
|
+
case "onPointerCancel":
|
|
1329
|
+
return (event) => {
|
|
1330
|
+
const pointerId = getPointerId(event);
|
|
1331
|
+
cleanupPointer(pointerId);
|
|
1332
|
+
};
|
|
815
1333
|
case "onLostPointerCapture":
|
|
816
1334
|
return (event) => {
|
|
817
1335
|
const { internal } = store.getState();
|
|
818
|
-
|
|
1336
|
+
const pointerId = getPointerId(event);
|
|
1337
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1338
|
+
if (pointerState?.captured.size) {
|
|
819
1339
|
requestAnimationFrame(() => {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1340
|
+
const pointerState2 = internal.pointerMap.get(pointerId);
|
|
1341
|
+
if (pointerState2?.captured.size) {
|
|
1342
|
+
pointerState2.captured.clear();
|
|
823
1343
|
}
|
|
1344
|
+
cancelPointer([], pointerId);
|
|
824
1345
|
});
|
|
825
1346
|
}
|
|
826
1347
|
};
|
|
827
1348
|
}
|
|
828
1349
|
return function handleEvent(event) {
|
|
829
1350
|
const state = store.getState();
|
|
830
|
-
const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = state;
|
|
1351
|
+
const { onPointerMissed, onDragOverMissed, onDropMissed, internal, events } = state;
|
|
1352
|
+
const pointerId = getPointerId(event);
|
|
831
1353
|
internal.lastEvent.current = event;
|
|
832
|
-
if (!
|
|
1354
|
+
if (!events.enabled) return;
|
|
833
1355
|
const isPointerMove = name === "onPointerMove";
|
|
834
1356
|
const isDragOver = name === "onDragOver";
|
|
835
1357
|
const isDrop = name === "onDrop";
|
|
836
1358
|
const isClickEvent = name === "onClick" || name === "onContextMenu" || name === "onDoubleClick";
|
|
1359
|
+
const isPointerDown = name === "onPointerDown";
|
|
1360
|
+
const isPointerUp = name === "onPointerUp";
|
|
1361
|
+
const isWheel = name === "onWheel";
|
|
1362
|
+
const canDeferRaycasts = events.frameTimedRaycasts && state.frameloop === "always";
|
|
1363
|
+
if (isPointerMove && canDeferRaycasts) {
|
|
1364
|
+
events.compute?.(event, state);
|
|
1365
|
+
internal.pointerDirty.set(pointerId, event);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (isWheel && canDeferRaycasts && !events.alwaysFireOnScroll) {
|
|
1369
|
+
events.compute?.(event, state);
|
|
1370
|
+
internal.pointerDirty.set(pointerId, event);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if ((isClickEvent || isPointerDown || isPointerUp) && internal.pointerDirty.has(pointerId)) {
|
|
1374
|
+
const deferredEvent = internal.pointerDirty.get(pointerId);
|
|
1375
|
+
internal.pointerDirty.delete(pointerId);
|
|
1376
|
+
processDeferredPointer(deferredEvent, pointerId);
|
|
1377
|
+
}
|
|
837
1378
|
const filter = isPointerMove || isDragOver || isDrop ? filterPointerEvents : void 0;
|
|
838
1379
|
const hits = intersect(event, filter);
|
|
839
|
-
const delta = isClickEvent ? calculateDistance(event) : 0;
|
|
840
|
-
if (
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1380
|
+
const delta = isClickEvent ? calculateDistance(event, pointerId) : 0;
|
|
1381
|
+
if (isPointerDown) {
|
|
1382
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1383
|
+
pointerState2.initialClick = [event.offsetX, event.offsetY];
|
|
1384
|
+
pointerState2.initialHits = hits.map((hit) => hit.eventObject);
|
|
1385
|
+
}
|
|
1386
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1387
|
+
const initialHits = pointerState?.initialHits ?? [];
|
|
844
1388
|
if (isClickEvent && !hits.length) {
|
|
845
1389
|
if (delta <= 2) {
|
|
846
1390
|
pointerMissed(event, internal.interaction);
|
|
@@ -855,7 +1399,9 @@ function createEvents(store) {
|
|
|
855
1399
|
dropMissed(event, internal.interaction);
|
|
856
1400
|
if (onDropMissed) onDropMissed(event);
|
|
857
1401
|
}
|
|
858
|
-
if (isPointerMove || isDragOver)
|
|
1402
|
+
if (isPointerMove || isDragOver) {
|
|
1403
|
+
cancelPointer(hits, pointerId);
|
|
1404
|
+
}
|
|
859
1405
|
function onIntersect(data) {
|
|
860
1406
|
const eventObject = data.eventObject;
|
|
861
1407
|
const instance = eventObject.__r3f;
|
|
@@ -864,9 +1410,10 @@ function createEvents(store) {
|
|
|
864
1410
|
if (isPointerMove) {
|
|
865
1411
|
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) {
|
|
866
1412
|
const id = makeId(data);
|
|
867
|
-
const
|
|
1413
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1414
|
+
const hoveredItem = pointerState2.hovered.get(id);
|
|
868
1415
|
if (!hoveredItem) {
|
|
869
|
-
|
|
1416
|
+
pointerState2.hovered.set(id, data);
|
|
870
1417
|
handlers.onPointerOver?.(data);
|
|
871
1418
|
handlers.onPointerEnter?.(data);
|
|
872
1419
|
} else if (hoveredItem.stopped) {
|
|
@@ -876,9 +1423,10 @@ function createEvents(store) {
|
|
|
876
1423
|
handlers.onPointerMove?.(data);
|
|
877
1424
|
} else if (isDragOver) {
|
|
878
1425
|
const id = makeId(data);
|
|
879
|
-
const
|
|
1426
|
+
const pointerState2 = getPointerState(internal, pointerId);
|
|
1427
|
+
const hoveredItem = pointerState2.hovered.get(id);
|
|
880
1428
|
if (!hoveredItem) {
|
|
881
|
-
|
|
1429
|
+
pointerState2.hovered.set(id, data);
|
|
882
1430
|
handlers.onDragOverEnter?.(data);
|
|
883
1431
|
} else if (hoveredItem.stopped) {
|
|
884
1432
|
data.stopPropagation();
|
|
@@ -889,18 +1437,18 @@ function createEvents(store) {
|
|
|
889
1437
|
} else {
|
|
890
1438
|
const handler = handlers[name];
|
|
891
1439
|
if (handler) {
|
|
892
|
-
if (!isClickEvent ||
|
|
1440
|
+
if (!isClickEvent || initialHits.includes(eventObject)) {
|
|
893
1441
|
pointerMissed(
|
|
894
1442
|
event,
|
|
895
|
-
internal.interaction.filter((object) => !
|
|
1443
|
+
internal.interaction.filter((object) => !initialHits.includes(object))
|
|
896
1444
|
);
|
|
897
1445
|
handler(data);
|
|
898
1446
|
}
|
|
899
1447
|
} else {
|
|
900
|
-
if (isClickEvent &&
|
|
1448
|
+
if (isClickEvent && initialHits.includes(eventObject)) {
|
|
901
1449
|
pointerMissed(
|
|
902
1450
|
event,
|
|
903
|
-
internal.interaction.filter((object) => !
|
|
1451
|
+
internal.interaction.filter((object) => !initialHits.includes(object))
|
|
904
1452
|
);
|
|
905
1453
|
}
|
|
906
1454
|
}
|
|
@@ -909,7 +1457,15 @@ function createEvents(store) {
|
|
|
909
1457
|
handleIntersects(hits, event, delta, onIntersect);
|
|
910
1458
|
};
|
|
911
1459
|
}
|
|
912
|
-
|
|
1460
|
+
function flushDeferredPointers() {
|
|
1461
|
+
const { internal, events } = store.getState();
|
|
1462
|
+
if (!events.frameTimedRaycasts) return;
|
|
1463
|
+
for (const [pointerId, event] of internal.pointerDirty) {
|
|
1464
|
+
processDeferredPointer(event, pointerId);
|
|
1465
|
+
}
|
|
1466
|
+
internal.pointerDirty.clear();
|
|
1467
|
+
}
|
|
1468
|
+
return { handlePointer, flushDeferredPointers, processDeferredPointer };
|
|
913
1469
|
}
|
|
914
1470
|
const DOM_EVENTS = {
|
|
915
1471
|
onClick: ["click", false],
|
|
@@ -928,11 +1484,16 @@ const DOM_EVENTS = {
|
|
|
928
1484
|
onLostPointerCapture: ["lostpointercapture", true]
|
|
929
1485
|
};
|
|
930
1486
|
function createPointerEvents(store) {
|
|
931
|
-
const { handlePointer } = createEvents(store);
|
|
1487
|
+
const { handlePointer, flushDeferredPointers, processDeferredPointer } = createEvents(store);
|
|
1488
|
+
let nextXRPointerId = XR_POINTER_ID_START;
|
|
1489
|
+
const xrPointers = /* @__PURE__ */ new Map();
|
|
932
1490
|
return {
|
|
933
1491
|
priority: 1,
|
|
934
1492
|
enabled: true,
|
|
935
|
-
|
|
1493
|
+
frameTimedRaycasts: true,
|
|
1494
|
+
alwaysFireOnScroll: true,
|
|
1495
|
+
updateOnFrame: false,
|
|
1496
|
+
compute(event, state) {
|
|
936
1497
|
state.pointer.set(event.offsetX / state.size.width * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
|
|
937
1498
|
state.raycaster.setFromCamera(state.pointer, state.camera);
|
|
938
1499
|
},
|
|
@@ -941,11 +1502,33 @@ function createPointerEvents(store) {
|
|
|
941
1502
|
(acc, key) => ({ ...acc, [key]: handlePointer(key) }),
|
|
942
1503
|
{}
|
|
943
1504
|
),
|
|
944
|
-
update: () => {
|
|
1505
|
+
update: (pointerId) => {
|
|
945
1506
|
const { events, internal } = store.getState();
|
|
946
|
-
if (
|
|
1507
|
+
if (!events.handlers) return;
|
|
1508
|
+
if (pointerId !== void 0) {
|
|
1509
|
+
const event = internal.pointerDirty.get(pointerId);
|
|
1510
|
+
if (event) {
|
|
1511
|
+
internal.pointerDirty.delete(pointerId);
|
|
1512
|
+
processDeferredPointer(event, pointerId);
|
|
1513
|
+
} else if (internal.lastEvent?.current) {
|
|
1514
|
+
processDeferredPointer(internal.lastEvent.current, pointerId);
|
|
1515
|
+
}
|
|
1516
|
+
} else {
|
|
1517
|
+
flushDeferredPointers();
|
|
1518
|
+
if (internal.lastEvent?.current) {
|
|
1519
|
+
events.handlers.onPointerMove(internal.lastEvent.current);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
flush: () => {
|
|
1524
|
+
const { events, internal } = store.getState();
|
|
1525
|
+
flushDeferredPointers();
|
|
1526
|
+
if (events.updateOnFrame && internal.lastEvent?.current && events.handlers) {
|
|
1527
|
+
events.handlers.onPointerMove(internal.lastEvent.current);
|
|
1528
|
+
}
|
|
947
1529
|
},
|
|
948
1530
|
connect: (target) => {
|
|
1531
|
+
if (!target) return;
|
|
949
1532
|
const { set, events } = store.getState();
|
|
950
1533
|
events.disconnect?.();
|
|
951
1534
|
set((state) => ({ events: { ...state.events, connected: target } }));
|
|
@@ -969,6 +1552,32 @@ function createPointerEvents(store) {
|
|
|
969
1552
|
}
|
|
970
1553
|
set((state) => ({ events: { ...state.events, connected: void 0 } }));
|
|
971
1554
|
}
|
|
1555
|
+
},
|
|
1556
|
+
registerPointer: (config) => {
|
|
1557
|
+
const pointerId = nextXRPointerId++;
|
|
1558
|
+
xrPointers.set(pointerId, config);
|
|
1559
|
+
const { internal } = store.getState();
|
|
1560
|
+
getPointerState(internal, pointerId);
|
|
1561
|
+
return pointerId;
|
|
1562
|
+
},
|
|
1563
|
+
unregisterPointer: (pointerId) => {
|
|
1564
|
+
xrPointers.delete(pointerId);
|
|
1565
|
+
const { internal } = store.getState();
|
|
1566
|
+
const pointerState = internal.pointerMap.get(pointerId);
|
|
1567
|
+
if (pointerState) {
|
|
1568
|
+
for (const [, hoveredObj] of pointerState.hovered) {
|
|
1569
|
+
const eventObject = hoveredObj.eventObject;
|
|
1570
|
+
const instance = eventObject.__r3f;
|
|
1571
|
+
if (instance?.eventCount) {
|
|
1572
|
+
const handlers = instance.handlers;
|
|
1573
|
+
const data = { ...hoveredObj, intersections: [] };
|
|
1574
|
+
handlers.onPointerOut?.(data);
|
|
1575
|
+
handlers.onPointerLeave?.(data);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
internal.pointerMap.delete(pointerId);
|
|
1579
|
+
}
|
|
1580
|
+
internal.pointerDirty.delete(pointerId);
|
|
972
1581
|
}
|
|
973
1582
|
};
|
|
974
1583
|
}
|
|
@@ -1059,23 +1668,29 @@ const createStore = (invalidate, advance) => {
|
|
|
1059
1668
|
set,
|
|
1060
1669
|
get,
|
|
1061
1670
|
// Mock objects that have to be configured
|
|
1671
|
+
// primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
|
|
1672
|
+
primaryStore: null,
|
|
1062
1673
|
gl: null,
|
|
1063
1674
|
renderer: null,
|
|
1064
1675
|
camera: null,
|
|
1065
1676
|
frustum: new Frustum(),
|
|
1066
1677
|
autoUpdateFrustum: true,
|
|
1067
1678
|
raycaster: null,
|
|
1068
|
-
events: {
|
|
1679
|
+
events: {
|
|
1680
|
+
priority: 1,
|
|
1681
|
+
enabled: true,
|
|
1682
|
+
connected: false,
|
|
1683
|
+
frameTimedRaycasts: true,
|
|
1684
|
+
alwaysFireOnScroll: true,
|
|
1685
|
+
updateOnFrame: false
|
|
1686
|
+
},
|
|
1069
1687
|
scene: null,
|
|
1070
1688
|
rootScene: null,
|
|
1071
1689
|
xr: null,
|
|
1072
1690
|
inspector: null,
|
|
1073
1691
|
invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
|
|
1074
1692
|
advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
|
|
1075
|
-
|
|
1076
|
-
linear: false,
|
|
1077
|
-
flat: false,
|
|
1078
|
-
textureColorSpace: "srgb",
|
|
1693
|
+
textureColorSpace: SRGBColorSpace,
|
|
1079
1694
|
isLegacy: false,
|
|
1080
1695
|
webGPUSupported: false,
|
|
1081
1696
|
isNative: false,
|
|
@@ -1133,6 +1748,7 @@ const createStore = (invalidate, advance) => {
|
|
|
1133
1748
|
size: newSize,
|
|
1134
1749
|
viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
|
|
1135
1750
|
}));
|
|
1751
|
+
getScheduler().invalidate();
|
|
1136
1752
|
}
|
|
1137
1753
|
}
|
|
1138
1754
|
return;
|
|
@@ -1147,6 +1763,7 @@ const createStore = (invalidate, advance) => {
|
|
|
1147
1763
|
viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
|
|
1148
1764
|
_sizeImperative: true
|
|
1149
1765
|
}));
|
|
1766
|
+
getScheduler().invalidate();
|
|
1150
1767
|
},
|
|
1151
1768
|
setDpr: (dpr) => set((state2) => {
|
|
1152
1769
|
const resolved = calculateDpr(dpr);
|
|
@@ -1157,11 +1774,14 @@ const createStore = (invalidate, advance) => {
|
|
|
1157
1774
|
},
|
|
1158
1775
|
setError: (error) => set(() => ({ error })),
|
|
1159
1776
|
error: null,
|
|
1160
|
-
//* TSL State (managed via hooks: useUniforms, useNodes, useTextures,
|
|
1777
|
+
//* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
|
|
1161
1778
|
uniforms: {},
|
|
1162
1779
|
nodes: {},
|
|
1780
|
+
buffers: {},
|
|
1781
|
+
gpuStorage: {},
|
|
1163
1782
|
textures: /* @__PURE__ */ new Map(),
|
|
1164
|
-
|
|
1783
|
+
_textureRefs: /* @__PURE__ */ new Map(),
|
|
1784
|
+
renderPipeline: null,
|
|
1165
1785
|
passes: {},
|
|
1166
1786
|
_hmrVersion: 0,
|
|
1167
1787
|
_sizeImperative: false,
|
|
@@ -1170,12 +1790,16 @@ const createStore = (invalidate, advance) => {
|
|
|
1170
1790
|
internal: {
|
|
1171
1791
|
// Events
|
|
1172
1792
|
interaction: [],
|
|
1173
|
-
hovered: /* @__PURE__ */ new Map(),
|
|
1174
1793
|
subscribers: [],
|
|
1794
|
+
// Per-pointer state (new unified structure)
|
|
1795
|
+
pointerMap: /* @__PURE__ */ new Map(),
|
|
1796
|
+
pointerDirty: /* @__PURE__ */ new Map(),
|
|
1797
|
+
lastEvent: React.createRef(),
|
|
1798
|
+
// Deprecated but kept for backwards compatibility
|
|
1799
|
+
hovered: /* @__PURE__ */ new Map(),
|
|
1175
1800
|
initialClick: [0, 0],
|
|
1176
1801
|
initialHits: [],
|
|
1177
1802
|
capturedMap: /* @__PURE__ */ new Map(),
|
|
1178
|
-
lastEvent: React.createRef(),
|
|
1179
1803
|
// Visibility tracking (onFramed, onOccluded, onVisible)
|
|
1180
1804
|
visibilityRegistry: /* @__PURE__ */ new Map(),
|
|
1181
1805
|
// Occlusion system (WebGPU only)
|
|
@@ -1258,13 +1882,22 @@ const createStore = (invalidate, advance) => {
|
|
|
1258
1882
|
rootStore.subscribe(() => {
|
|
1259
1883
|
const { camera, size, viewport, set, internal } = rootStore.getState();
|
|
1260
1884
|
const actualRenderer = internal.actualRenderer;
|
|
1885
|
+
const canvasTarget = internal.canvasTarget;
|
|
1261
1886
|
if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
|
|
1262
1887
|
oldSize = size;
|
|
1263
1888
|
oldDpr = viewport.dpr;
|
|
1264
1889
|
updateCamera(camera, size);
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1890
|
+
if (internal.isSecondary && canvasTarget) {
|
|
1891
|
+
if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
|
|
1892
|
+
canvasTarget.setSize(size.width, size.height, false);
|
|
1893
|
+
} else {
|
|
1894
|
+
if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
|
|
1895
|
+
actualRenderer.setSize(size.width, size.height, false);
|
|
1896
|
+
if (canvasTarget) {
|
|
1897
|
+
if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
|
|
1898
|
+
canvasTarget.setSize(size.width, size.height, false);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1268
1901
|
}
|
|
1269
1902
|
if (camera !== oldCamera) {
|
|
1270
1903
|
oldCamera = camera;
|
|
@@ -1335,1124 +1968,86 @@ useLoader.clear = function(loader, input) {
|
|
|
1335
1968
|
};
|
|
1336
1969
|
useLoader.loader = getLoader;
|
|
1337
1970
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1971
|
+
function useFrame(callback, priorityOrOptions) {
|
|
1972
|
+
const store = React.useContext(context);
|
|
1973
|
+
const isInsideCanvas = store !== null;
|
|
1974
|
+
const scheduler = getScheduler();
|
|
1975
|
+
const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
|
|
1976
|
+
id: priorityOrOptions.id,
|
|
1977
|
+
phase: priorityOrOptions.phase,
|
|
1978
|
+
priority: priorityOrOptions.priority,
|
|
1979
|
+
fps: priorityOrOptions.fps,
|
|
1980
|
+
drop: priorityOrOptions.drop,
|
|
1981
|
+
enabled: priorityOrOptions.enabled,
|
|
1982
|
+
before: priorityOrOptions.before,
|
|
1983
|
+
after: priorityOrOptions.after
|
|
1984
|
+
}) : "";
|
|
1985
|
+
const options = React.useMemo(() => {
|
|
1986
|
+
return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
|
|
1987
|
+
}, [optionsKey]);
|
|
1988
|
+
const reactId = React.useId();
|
|
1989
|
+
const id = options.id ?? reactId;
|
|
1990
|
+
const callbackRef = useMutableCallback(callback);
|
|
1991
|
+
const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
|
|
1992
|
+
useIsomorphicLayoutEffect(() => {
|
|
1993
|
+
if (!callback) return;
|
|
1994
|
+
if (isInsideCanvas) {
|
|
1995
|
+
const state = store.getState();
|
|
1996
|
+
const rootId = state.internal.rootId;
|
|
1997
|
+
if (isLegacyPriority) {
|
|
1998
|
+
state.internal.priority++;
|
|
1999
|
+
let parentRoot = state.previousRoot;
|
|
2000
|
+
while (parentRoot) {
|
|
2001
|
+
const parentState = parentRoot.getState();
|
|
2002
|
+
if (parentState?.internal) parentState.internal.priority++;
|
|
2003
|
+
parentRoot = parentState?.previousRoot;
|
|
2004
|
+
}
|
|
2005
|
+
notifyDepreciated({
|
|
2006
|
+
heading: "useFrame with numeric priority is deprecated",
|
|
2007
|
+
body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
|
|
2008
|
+
link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
const wrappedCallback = (frameState, delta) => {
|
|
2012
|
+
const localState = store.getState();
|
|
2013
|
+
const mergedState = {
|
|
2014
|
+
...localState,
|
|
2015
|
+
time: frameState.time,
|
|
2016
|
+
delta: frameState.delta,
|
|
2017
|
+
elapsed: frameState.elapsed,
|
|
2018
|
+
frame: frameState.frame
|
|
2019
|
+
};
|
|
2020
|
+
callbackRef.current?.(mergedState, delta);
|
|
2021
|
+
};
|
|
2022
|
+
const unregister = scheduler.register(wrappedCallback, {
|
|
2023
|
+
id,
|
|
2024
|
+
rootId,
|
|
2025
|
+
...options
|
|
2026
|
+
});
|
|
2027
|
+
return () => {
|
|
2028
|
+
unregister();
|
|
2029
|
+
if (isLegacyPriority) {
|
|
2030
|
+
const currentState = store.getState();
|
|
2031
|
+
if (currentState.internal) {
|
|
2032
|
+
currentState.internal.priority--;
|
|
2033
|
+
let parentRoot = currentState.previousRoot;
|
|
2034
|
+
while (parentRoot) {
|
|
2035
|
+
const parentState = parentRoot.getState();
|
|
2036
|
+
if (parentState?.internal) parentState.internal.priority--;
|
|
2037
|
+
parentRoot = parentState?.previousRoot;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
1378
2042
|
} else {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
* Get ordered list of phase names
|
|
1388
|
-
*/
|
|
1389
|
-
getOrderedPhases() {
|
|
1390
|
-
if (this.orderedNamesCache === null) this.orderedNamesCache = this.phases.map((p) => p.name);
|
|
1391
|
-
return this.orderedNamesCache;
|
|
1392
|
-
}
|
|
1393
|
-
/**
|
|
1394
|
-
* Check if a phase exists
|
|
1395
|
-
*/
|
|
1396
|
-
hasPhase(name) {
|
|
1397
|
-
return this.phaseMap.has(name);
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Get the index of a phase (-1 if not found)
|
|
1401
|
-
*/
|
|
1402
|
-
getPhaseIndex(name) {
|
|
1403
|
-
if (!name) return -1;
|
|
1404
|
-
return this.phases.findIndex((p) => p.name === name);
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Ensure a phase exists, creating an auto-generated one if needed.
|
|
1408
|
-
* Used for resolving before/after constraints.
|
|
1409
|
-
*
|
|
1410
|
-
* @param name - The phase name to ensure exists
|
|
1411
|
-
* @returns The phase name (may be auto-generated like 'before:render')
|
|
1412
|
-
*/
|
|
1413
|
-
ensurePhase(name) {
|
|
1414
|
-
if (this.phaseMap.has(name)) return name;
|
|
1415
|
-
const node = { name, isAutoGenerated: true };
|
|
1416
|
-
this.phases.push(node);
|
|
1417
|
-
this.phaseMap.set(name, node);
|
|
1418
|
-
this.invalidateCache();
|
|
1419
|
-
return name;
|
|
1420
|
-
}
|
|
1421
|
-
/**
|
|
1422
|
-
* Resolve where a job with before/after constraints should go.
|
|
1423
|
-
* Creates auto-generated phases if needed.
|
|
1424
|
-
*
|
|
1425
|
-
* @param before - Phase(s) to run before
|
|
1426
|
-
* @param after - Phase(s) to run after
|
|
1427
|
-
* @returns The resolved phase name
|
|
1428
|
-
*/
|
|
1429
|
-
resolveConstraintPhase(before, after) {
|
|
1430
|
-
const beforeArr = before ? Array.isArray(before) ? before : [before] : [];
|
|
1431
|
-
const afterArr = after ? Array.isArray(after) ? after : [after] : [];
|
|
1432
|
-
if (beforeArr.length > 0) {
|
|
1433
|
-
return this.ensureAutoPhase(beforeArr[0], "before", 0);
|
|
1434
|
-
}
|
|
1435
|
-
if (afterArr.length > 0) {
|
|
1436
|
-
return this.ensureAutoPhase(afterArr[0], "after", 1);
|
|
1437
|
-
}
|
|
1438
|
-
return "update";
|
|
1439
|
-
}
|
|
1440
|
-
/**
|
|
1441
|
-
* Ensure an auto-generated phase exists relative to a target phase.
|
|
1442
|
-
* Creates the phase if it doesn't exist, inserting it at the correct position.
|
|
1443
|
-
*
|
|
1444
|
-
* @param target - The target phase name to position relative to
|
|
1445
|
-
* @param prefix - Prefix for auto-generated phase name ('before' or 'after')
|
|
1446
|
-
* @param offset - Insertion offset (0 for before, 1 for after)
|
|
1447
|
-
* @returns The auto-generated phase name
|
|
1448
|
-
*/
|
|
1449
|
-
ensureAutoPhase(target, prefix, offset) {
|
|
1450
|
-
const autoName = `${prefix}:${target}`;
|
|
1451
|
-
if (this.phaseMap.has(autoName)) return autoName;
|
|
1452
|
-
const node = { name: autoName, isAutoGenerated: true };
|
|
1453
|
-
const targetIndex = this.getPhaseIndex(target);
|
|
1454
|
-
if (targetIndex !== -1) this.phases.splice(targetIndex + offset, 0, node);
|
|
1455
|
-
else this.phases.push(node);
|
|
1456
|
-
this.phaseMap.set(autoName, node);
|
|
1457
|
-
this.invalidateCache();
|
|
1458
|
-
return autoName;
|
|
1459
|
-
}
|
|
1460
|
-
// Internal --------------------------------
|
|
1461
|
-
invalidateCache() {
|
|
1462
|
-
this.orderedNamesCache = null;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function rebuildSortedJobs(jobs, phaseGraph) {
|
|
1467
|
-
const orderedPhases = phaseGraph.getOrderedPhases();
|
|
1468
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
1469
|
-
for (const phase of orderedPhases) {
|
|
1470
|
-
buckets.set(phase, []);
|
|
1471
|
-
}
|
|
1472
|
-
for (const job of jobs.values()) {
|
|
1473
|
-
if (!job.enabled) continue;
|
|
1474
|
-
let bucket = buckets.get(job.phase);
|
|
1475
|
-
if (!bucket) {
|
|
1476
|
-
bucket = [];
|
|
1477
|
-
buckets.set(job.phase, bucket);
|
|
1478
|
-
}
|
|
1479
|
-
bucket.push(job);
|
|
1480
|
-
}
|
|
1481
|
-
const sortedBuckets = [];
|
|
1482
|
-
for (const phase of orderedPhases) {
|
|
1483
|
-
const bucket = buckets.get(phase);
|
|
1484
|
-
if (!bucket || bucket.length === 0) continue;
|
|
1485
|
-
bucket.sort((a, b) => {
|
|
1486
|
-
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
1487
|
-
return a.index - b.index;
|
|
1488
|
-
});
|
|
1489
|
-
sortedBuckets.push(hasCrossJobConstraints(bucket) ? topologicalSort(bucket) : bucket);
|
|
1490
|
-
}
|
|
1491
|
-
for (const [phase, bucket] of buckets) {
|
|
1492
|
-
if (!orderedPhases.includes(phase) && bucket.length > 0) {
|
|
1493
|
-
bucket.sort((a, b) => {
|
|
1494
|
-
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
1495
|
-
return a.index - b.index;
|
|
1496
|
-
});
|
|
1497
|
-
sortedBuckets.push(bucket);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
return sortedBuckets.flat();
|
|
1501
|
-
}
|
|
1502
|
-
function hasCrossJobConstraints(bucket) {
|
|
1503
|
-
const jobIds = new Set(bucket.map((j) => j.id));
|
|
1504
|
-
for (const job of bucket) {
|
|
1505
|
-
for (const ref of job.before) {
|
|
1506
|
-
if (jobIds.has(ref)) return true;
|
|
1507
|
-
}
|
|
1508
|
-
for (const ref of job.after) {
|
|
1509
|
-
if (jobIds.has(ref)) return true;
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
return false;
|
|
1513
|
-
}
|
|
1514
|
-
function topologicalSort(jobs) {
|
|
1515
|
-
const n = jobs.length;
|
|
1516
|
-
if (n <= 1) return jobs;
|
|
1517
|
-
const jobMap = /* @__PURE__ */ new Map();
|
|
1518
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
1519
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
1520
|
-
for (const job of jobs) {
|
|
1521
|
-
jobMap.set(job.id, job);
|
|
1522
|
-
inDegree.set(job.id, 0);
|
|
1523
|
-
adjacency.set(job.id, []);
|
|
1524
|
-
}
|
|
1525
|
-
for (const job of jobs) {
|
|
1526
|
-
for (const ref of job.before) {
|
|
1527
|
-
if (jobMap.has(ref)) {
|
|
1528
|
-
adjacency.get(job.id).push(ref);
|
|
1529
|
-
inDegree.set(ref, inDegree.get(ref) + 1);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
for (const ref of job.after) {
|
|
1533
|
-
if (jobMap.has(ref)) {
|
|
1534
|
-
adjacency.get(ref).push(job.id);
|
|
1535
|
-
inDegree.set(job.id, inDegree.get(job.id) + 1);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
const queue = [];
|
|
1540
|
-
for (const job of jobs) {
|
|
1541
|
-
if (inDegree.get(job.id) === 0) {
|
|
1542
|
-
queue.push(job);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
queue.sort((a, b) => {
|
|
1546
|
-
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
1547
|
-
return a.index - b.index;
|
|
1548
|
-
});
|
|
1549
|
-
const result = [];
|
|
1550
|
-
while (queue.length > 0) {
|
|
1551
|
-
const job = queue.shift();
|
|
1552
|
-
result.push(job);
|
|
1553
|
-
const neighbors = adjacency.get(job.id) || [];
|
|
1554
|
-
for (const neighborId of neighbors) {
|
|
1555
|
-
const newDegree = inDegree.get(neighborId) - 1;
|
|
1556
|
-
inDegree.set(neighborId, newDegree);
|
|
1557
|
-
if (newDegree === 0) {
|
|
1558
|
-
const neighbor = jobMap.get(neighborId);
|
|
1559
|
-
insertSorted(queue, neighbor);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
if (result.length !== n) {
|
|
1564
|
-
console.warn("[useFrame] Circular dependency detected in job constraints");
|
|
1565
|
-
const resultIds = new Set(result.map((j) => j.id));
|
|
1566
|
-
for (const job of jobs) {
|
|
1567
|
-
if (!resultIds.has(job.id)) result.push(job);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return result;
|
|
1571
|
-
}
|
|
1572
|
-
function insertSorted(arr, job) {
|
|
1573
|
-
let i = 0;
|
|
1574
|
-
while (i < arr.length) {
|
|
1575
|
-
const cmp = arr[i];
|
|
1576
|
-
if (job.priority > cmp.priority || job.priority === cmp.priority && job.index < cmp.index) {
|
|
1577
|
-
break;
|
|
1578
|
-
}
|
|
1579
|
-
i++;
|
|
1580
|
-
}
|
|
1581
|
-
arr.splice(i, 0, job);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
function shouldRun(job, now) {
|
|
1585
|
-
if (!job.enabled) return false;
|
|
1586
|
-
if (!job.fps) return true;
|
|
1587
|
-
const minInterval = 1e3 / job.fps;
|
|
1588
|
-
const lastRun = job.lastRun ?? 0;
|
|
1589
|
-
const elapsed = now - lastRun;
|
|
1590
|
-
if (elapsed < minInterval) return false;
|
|
1591
|
-
if (job.drop) {
|
|
1592
|
-
job.lastRun = now;
|
|
1593
|
-
} else {
|
|
1594
|
-
const steps = Math.floor(elapsed / minInterval);
|
|
1595
|
-
job.lastRun = lastRun + steps * minInterval;
|
|
1596
|
-
if (job.lastRun < now - minInterval) {
|
|
1597
|
-
job.lastRun = now;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
return true;
|
|
1601
|
-
}
|
|
1602
|
-
function resetJobTiming(job) {
|
|
1603
|
-
job.lastRun = void 0;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
var __defProp$1 = Object.defineProperty;
|
|
1607
|
-
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1608
|
-
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1609
|
-
const hmrData = (() => {
|
|
1610
|
-
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return void 0;
|
|
1611
|
-
if (typeof import_meta_hot !== "undefined") return import_meta_hot;
|
|
1612
|
-
try {
|
|
1613
|
-
return (0, eval)("import.meta.hot");
|
|
1614
|
-
} catch {
|
|
1615
|
-
return void 0;
|
|
1616
|
-
}
|
|
1617
|
-
})();
|
|
1618
|
-
const _Scheduler = class _Scheduler {
|
|
1619
|
-
//* Constructor ================================
|
|
1620
|
-
constructor() {
|
|
1621
|
-
//* Critical State ================================
|
|
1622
|
-
__publicField$1(this, "roots", /* @__PURE__ */ new Map());
|
|
1623
|
-
__publicField$1(this, "phaseGraph");
|
|
1624
|
-
__publicField$1(this, "loopState", {
|
|
1625
|
-
running: false,
|
|
1626
|
-
rafHandle: null,
|
|
1627
|
-
lastTime: null,
|
|
1628
|
-
// null = uninitialized, 0+ = valid timestamp
|
|
1629
|
-
frameCount: 0,
|
|
1630
|
-
elapsedTime: 0,
|
|
1631
|
-
createdAt: performance.now()
|
|
1632
|
-
});
|
|
1633
|
-
__publicField$1(this, "stoppedTime", 0);
|
|
1634
|
-
//* Private State ================================
|
|
1635
|
-
__publicField$1(this, "nextRootIndex", 0);
|
|
1636
|
-
__publicField$1(this, "globalBeforeJobs", /* @__PURE__ */ new Map());
|
|
1637
|
-
__publicField$1(this, "globalAfterJobs", /* @__PURE__ */ new Map());
|
|
1638
|
-
__publicField$1(this, "nextGlobalIndex", 0);
|
|
1639
|
-
__publicField$1(this, "idleCallbacks", /* @__PURE__ */ new Set());
|
|
1640
|
-
__publicField$1(this, "nextJobIndex", 0);
|
|
1641
|
-
__publicField$1(this, "jobStateListeners", /* @__PURE__ */ new Map());
|
|
1642
|
-
__publicField$1(this, "pendingFrames", 0);
|
|
1643
|
-
__publicField$1(this, "_frameloop", "always");
|
|
1644
|
-
//* Independent Mode & Error Handling State ================================
|
|
1645
|
-
__publicField$1(this, "_independent", false);
|
|
1646
|
-
__publicField$1(this, "errorHandler", null);
|
|
1647
|
-
__publicField$1(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
|
|
1648
|
-
//* Core Loop Execution Methods ================================
|
|
1649
|
-
/**
|
|
1650
|
-
* Main RAF loop callback.
|
|
1651
|
-
* Executes frame, handles demand mode, and schedules next frame.
|
|
1652
|
-
* @param {number} timestamp - RAF timestamp in milliseconds
|
|
1653
|
-
* @returns {void}
|
|
1654
|
-
* @private
|
|
1655
|
-
*/
|
|
1656
|
-
__publicField$1(this, "loop", (timestamp) => {
|
|
1657
|
-
if (!this.loopState.running) return;
|
|
1658
|
-
this.executeFrame(timestamp);
|
|
1659
|
-
if (this._frameloop === "demand") {
|
|
1660
|
-
this.pendingFrames = Math.max(0, this.pendingFrames - 1);
|
|
1661
|
-
if (this.pendingFrames === 0) {
|
|
1662
|
-
this.notifyIdle(timestamp);
|
|
1663
|
-
return this.stop();
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
this.loopState.rafHandle = requestAnimationFrame(this.loop);
|
|
1667
|
-
});
|
|
1668
|
-
this.phaseGraph = new PhaseGraph();
|
|
1669
|
-
}
|
|
1670
|
-
static get instance() {
|
|
1671
|
-
return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
|
|
1672
|
-
}
|
|
1673
|
-
static set instance(value) {
|
|
1674
|
-
globalThis[_Scheduler.INSTANCE_KEY] = value;
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* Get the global scheduler instance (creates if doesn't exist).
|
|
1678
|
-
* Uses HMR data to preserve instance across hot reloads.
|
|
1679
|
-
* @returns {Scheduler} The singleton scheduler instance
|
|
1680
|
-
*/
|
|
1681
|
-
static get() {
|
|
1682
|
-
if (!_Scheduler.instance && hmrData?.data?.scheduler) {
|
|
1683
|
-
_Scheduler.instance = hmrData.data.scheduler;
|
|
1684
|
-
}
|
|
1685
|
-
if (!_Scheduler.instance) {
|
|
1686
|
-
_Scheduler.instance = new _Scheduler();
|
|
1687
|
-
if (hmrData?.data) {
|
|
1688
|
-
hmrData.data.scheduler = _Scheduler.instance;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
return _Scheduler.instance;
|
|
1692
|
-
}
|
|
1693
|
-
/**
|
|
1694
|
-
* Reset the singleton instance. Stops the loop and clears all state.
|
|
1695
|
-
* Primarily used for testing to ensure clean state between tests.
|
|
1696
|
-
* @returns {void}
|
|
1697
|
-
*/
|
|
1698
|
-
static reset() {
|
|
1699
|
-
if (_Scheduler.instance) {
|
|
1700
|
-
_Scheduler.instance.stop();
|
|
1701
|
-
_Scheduler.instance = null;
|
|
1702
|
-
}
|
|
1703
|
-
if (hmrData?.data) {
|
|
1704
|
-
hmrData.data.scheduler = null;
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
//* Getters & Setters ================================
|
|
1708
|
-
get phases() {
|
|
1709
|
-
return this.phaseGraph.getOrderedPhases();
|
|
1710
|
-
}
|
|
1711
|
-
get frameloop() {
|
|
1712
|
-
return this._frameloop;
|
|
1713
|
-
}
|
|
1714
|
-
set frameloop(mode) {
|
|
1715
|
-
if (this._frameloop === mode) return;
|
|
1716
|
-
const wasAlways = this._frameloop === "always";
|
|
1717
|
-
this._frameloop = mode;
|
|
1718
|
-
if (mode === "always" && !this.loopState.running && this.roots.size > 0) this.start();
|
|
1719
|
-
else if (mode !== "always" && wasAlways) this.stop();
|
|
1720
|
-
}
|
|
1721
|
-
get isRunning() {
|
|
1722
|
-
return this.loopState.running;
|
|
1723
|
-
}
|
|
1724
|
-
get isReady() {
|
|
1725
|
-
return this.roots.size > 0;
|
|
1726
|
-
}
|
|
1727
|
-
get independent() {
|
|
1728
|
-
return this._independent;
|
|
1729
|
-
}
|
|
1730
|
-
set independent(value) {
|
|
1731
|
-
this._independent = value;
|
|
1732
|
-
if (value) this.ensureDefaultRoot();
|
|
1733
|
-
}
|
|
1734
|
-
//* Root Management Methods ================================
|
|
1735
|
-
/**
|
|
1736
|
-
* Register a root (Canvas) with the scheduler.
|
|
1737
|
-
* The first root to register starts the RAF loop (if frameloop='always').
|
|
1738
|
-
* @param {string} id - Unique identifier for this root
|
|
1739
|
-
* @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
|
|
1740
|
-
* @returns {() => void} Unsubscribe function to remove this root
|
|
1741
|
-
*/
|
|
1742
|
-
registerRoot(id, options = {}) {
|
|
1743
|
-
if (this.roots.has(id)) {
|
|
1744
|
-
console.warn(`[Scheduler] Root "${id}" already registered`);
|
|
1745
|
-
return () => this.unregisterRoot(id);
|
|
1746
|
-
}
|
|
1747
|
-
const entry = {
|
|
1748
|
-
id,
|
|
1749
|
-
getState: options.getState ?? (() => ({})),
|
|
1750
|
-
jobs: /* @__PURE__ */ new Map(),
|
|
1751
|
-
sortedJobs: [],
|
|
1752
|
-
needsRebuild: false
|
|
1753
|
-
};
|
|
1754
|
-
if (options.onError) {
|
|
1755
|
-
this.errorHandler = options.onError;
|
|
1756
|
-
}
|
|
1757
|
-
this.roots.set(id, entry);
|
|
1758
|
-
if (this.roots.size === 1) {
|
|
1759
|
-
this.notifyRootReady();
|
|
1760
|
-
if (this._frameloop === "always") this.start();
|
|
1761
|
-
}
|
|
1762
|
-
return () => this.unregisterRoot(id);
|
|
1763
|
-
}
|
|
1764
|
-
/**
|
|
1765
|
-
* Unregister a root from the scheduler.
|
|
1766
|
-
* Cleans up all job state listeners for this root's jobs.
|
|
1767
|
-
* The last root to unregister stops the RAF loop.
|
|
1768
|
-
* @param {string} id - The root ID to unregister
|
|
1769
|
-
* @returns {void}
|
|
1770
|
-
*/
|
|
1771
|
-
unregisterRoot(id) {
|
|
1772
|
-
const root = this.roots.get(id);
|
|
1773
|
-
if (!root) return;
|
|
1774
|
-
for (const jobId of root.jobs.keys()) {
|
|
1775
|
-
this.jobStateListeners.delete(jobId);
|
|
1776
|
-
}
|
|
1777
|
-
this.roots.delete(id);
|
|
1778
|
-
if (this.roots.size === 0) {
|
|
1779
|
-
this.stop();
|
|
1780
|
-
this.errorHandler = null;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
/**
|
|
1784
|
-
* Subscribe to be notified when a root becomes available.
|
|
1785
|
-
* Fires immediately if a root already exists.
|
|
1786
|
-
* @param {() => void} callback - Function called when first root registers
|
|
1787
|
-
* @returns {() => void} Unsubscribe function
|
|
1788
|
-
*/
|
|
1789
|
-
onRootReady(callback) {
|
|
1790
|
-
if (this.roots.size > 0) {
|
|
1791
|
-
callback();
|
|
1792
|
-
return () => {
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
this.rootReadyCallbacks.add(callback);
|
|
1796
|
-
return () => this.rootReadyCallbacks.delete(callback);
|
|
1797
|
-
}
|
|
1798
|
-
/**
|
|
1799
|
-
* Notify all registered root-ready callbacks.
|
|
1800
|
-
* Called when the first root registers.
|
|
1801
|
-
* @returns {void}
|
|
1802
|
-
* @private
|
|
1803
|
-
*/
|
|
1804
|
-
notifyRootReady() {
|
|
1805
|
-
for (const cb of this.rootReadyCallbacks) {
|
|
1806
|
-
try {
|
|
1807
|
-
cb();
|
|
1808
|
-
} catch (error) {
|
|
1809
|
-
console.error("[Scheduler] Error in root-ready callback:", error);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
this.rootReadyCallbacks.clear();
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Ensure a default root exists for independent mode.
|
|
1816
|
-
* Creates a minimal root with no state provider.
|
|
1817
|
-
* @returns {void}
|
|
1818
|
-
* @private
|
|
1819
|
-
*/
|
|
1820
|
-
ensureDefaultRoot() {
|
|
1821
|
-
if (!this.roots.has("__default__")) {
|
|
1822
|
-
this.registerRoot("__default__");
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
/**
|
|
1826
|
-
* Trigger error handling for job errors.
|
|
1827
|
-
* Uses the bound error handler if available, otherwise logs to console.
|
|
1828
|
-
* @param {Error} error - The error to handle
|
|
1829
|
-
* @returns {void}
|
|
1830
|
-
*/
|
|
1831
|
-
triggerError(error) {
|
|
1832
|
-
if (this.errorHandler) this.errorHandler(error);
|
|
1833
|
-
else console.error("[Scheduler]", error);
|
|
1834
|
-
}
|
|
1835
|
-
//* Phase Management Methods ================================
|
|
1836
|
-
/**
|
|
1837
|
-
* Add a named phase to the scheduler's execution order.
|
|
1838
|
-
* Marks all roots for rebuild to incorporate the new phase.
|
|
1839
|
-
* @param {string} name - The phase name (e.g., 'physics', 'postprocess')
|
|
1840
|
-
* @param {AddPhaseOptions} [options] - Positioning options (before/after other phases)
|
|
1841
|
-
* @returns {void}
|
|
1842
|
-
* @example
|
|
1843
|
-
* scheduler.addPhase('physics', { before: 'update' });
|
|
1844
|
-
* scheduler.addPhase('postprocess', { after: 'render' });
|
|
1845
|
-
*/
|
|
1846
|
-
addPhase(name, options) {
|
|
1847
|
-
this.phaseGraph.addPhase(name, options);
|
|
1848
|
-
for (const root of this.roots.values()) {
|
|
1849
|
-
root.needsRebuild = true;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
/**
|
|
1853
|
-
* Check if a phase exists in the scheduler.
|
|
1854
|
-
* @param {string} name - The phase name to check
|
|
1855
|
-
* @returns {boolean} True if the phase exists
|
|
1856
|
-
*/
|
|
1857
|
-
hasPhase(name) {
|
|
1858
|
-
return this.phaseGraph.hasPhase(name);
|
|
1859
|
-
}
|
|
1860
|
-
//* Global Job Registration Methods (Deprecated APIs) ================================
|
|
1861
|
-
/**
|
|
1862
|
-
* Register a global job that runs once per frame (not per-root).
|
|
1863
|
-
* Used internally by deprecated addEffect/addAfterEffect APIs.
|
|
1864
|
-
* @param {'before' | 'after'} phase - When to run: 'before' all roots or 'after' all roots
|
|
1865
|
-
* @param {string} id - Unique identifier for this global job
|
|
1866
|
-
* @param {(timestamp: number) => void} callback - Function called each frame with RAF timestamp
|
|
1867
|
-
* @returns {() => void} Unsubscribe function to remove this global job
|
|
1868
|
-
* @deprecated Use useFrame with phases instead
|
|
1869
|
-
*/
|
|
1870
|
-
registerGlobal(phase, id, callback) {
|
|
1871
|
-
const job = { id, callback };
|
|
1872
|
-
if (phase === "before") {
|
|
1873
|
-
this.globalBeforeJobs.set(id, job);
|
|
1874
|
-
} else {
|
|
1875
|
-
this.globalAfterJobs.set(id, job);
|
|
1876
|
-
}
|
|
1877
|
-
return () => {
|
|
1878
|
-
if (phase === "before") this.globalBeforeJobs.delete(id);
|
|
1879
|
-
else this.globalAfterJobs.delete(id);
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
//* Idle Callback Methods (Deprecated API) ================================
|
|
1883
|
-
/**
|
|
1884
|
-
* Register an idle callback that fires when the loop stops.
|
|
1885
|
-
* Used internally by deprecated addTail API.
|
|
1886
|
-
* @param {(timestamp: number) => void} callback - Function called when loop becomes idle
|
|
1887
|
-
* @returns {() => void} Unsubscribe function to remove this idle callback
|
|
1888
|
-
* @deprecated Use demand mode with invalidate() instead
|
|
1889
|
-
*/
|
|
1890
|
-
onIdle(callback) {
|
|
1891
|
-
this.idleCallbacks.add(callback);
|
|
1892
|
-
return () => this.idleCallbacks.delete(callback);
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Notify all registered idle callbacks.
|
|
1896
|
-
* Called when the loop stops in demand mode.
|
|
1897
|
-
* @param {number} timestamp - The RAF timestamp when idle occurred
|
|
1898
|
-
* @returns {void}
|
|
1899
|
-
* @private
|
|
1900
|
-
*/
|
|
1901
|
-
notifyIdle(timestamp) {
|
|
1902
|
-
for (const cb of this.idleCallbacks) {
|
|
1903
|
-
try {
|
|
1904
|
-
cb(timestamp);
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
console.error("[Scheduler] Error in idle callback:", error);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
//* Job Registration & Management Methods ================================
|
|
1911
|
-
/**
|
|
1912
|
-
* Register a job (frame callback) with a specific root.
|
|
1913
|
-
* This is the core registration method used by useFrame internally.
|
|
1914
|
-
* @param {FrameNextCallback} callback - The function to call each frame
|
|
1915
|
-
* @param {JobOptions & { rootId?: string; system?: boolean }} [options] - Job configuration
|
|
1916
|
-
* @param {string} [options.rootId] - Target root ID (defaults to first registered root)
|
|
1917
|
-
* @param {string} [options.id] - Unique job ID (auto-generated if not provided)
|
|
1918
|
-
* @param {string} [options.phase] - Execution phase (defaults to 'update')
|
|
1919
|
-
* @param {number} [options.priority] - Priority within phase (higher = earlier, default 0)
|
|
1920
|
-
* @param {number} [options.fps] - FPS throttle limit
|
|
1921
|
-
* @param {boolean} [options.drop] - Drop frames when behind (default true)
|
|
1922
|
-
* @param {boolean} [options.enabled] - Whether job is active (default true)
|
|
1923
|
-
* @param {boolean} [options.system] - Internal flag for system jobs (not user-facing)
|
|
1924
|
-
* @returns {() => void} Unsubscribe function to remove this job
|
|
1925
|
-
*/
|
|
1926
|
-
register(callback, options = {}) {
|
|
1927
|
-
const rootId = options.rootId;
|
|
1928
|
-
const root = rootId ? this.roots.get(rootId) : this.roots.values().next().value;
|
|
1929
|
-
if (!root) {
|
|
1930
|
-
console.warn("[Scheduler] No root registered. Is this inside a Canvas?");
|
|
1931
|
-
return () => {
|
|
1932
|
-
};
|
|
1933
|
-
}
|
|
1934
|
-
const id = options.id ?? this.generateJobId();
|
|
1935
|
-
let phase = options.phase ?? "update";
|
|
1936
|
-
if (!options.phase && (options.before || options.after)) {
|
|
1937
|
-
phase = this.phaseGraph.resolveConstraintPhase(options.before, options.after);
|
|
1938
|
-
}
|
|
1939
|
-
const before = this.normalizeConstraints(options.before);
|
|
1940
|
-
const after = this.normalizeConstraints(options.after);
|
|
1941
|
-
const job = {
|
|
1942
|
-
id,
|
|
1943
|
-
callback,
|
|
1944
|
-
phase,
|
|
1945
|
-
before,
|
|
1946
|
-
after,
|
|
1947
|
-
priority: options.priority ?? 0,
|
|
1948
|
-
index: this.nextJobIndex++,
|
|
1949
|
-
fps: options.fps,
|
|
1950
|
-
drop: options.drop ?? true,
|
|
1951
|
-
enabled: options.enabled ?? true,
|
|
1952
|
-
system: options.system ?? false
|
|
1953
|
-
};
|
|
1954
|
-
if (root.jobs.has(id)) {
|
|
1955
|
-
console.warn(`[useFrame] Job with id "${id}" already exists, replacing`);
|
|
1956
|
-
}
|
|
1957
|
-
root.jobs.set(id, job);
|
|
1958
|
-
root.needsRebuild = true;
|
|
1959
|
-
return () => this.unregister(id, root.id);
|
|
1960
|
-
}
|
|
1961
|
-
/**
|
|
1962
|
-
* Unregister a job by its ID.
|
|
1963
|
-
* Searches all roots if rootId is not provided.
|
|
1964
|
-
* @param {string} id - The job ID to unregister
|
|
1965
|
-
* @param {string} [rootId] - Optional root ID to search (searches all if not provided)
|
|
1966
|
-
* @returns {void}
|
|
1967
|
-
*/
|
|
1968
|
-
unregister(id, rootId) {
|
|
1969
|
-
const root = rootId ? this.roots.get(rootId) : Array.from(this.roots.values()).find((r) => r.jobs.has(id));
|
|
1970
|
-
if (root?.jobs.delete(id)) {
|
|
1971
|
-
root.needsRebuild = true;
|
|
1972
|
-
this.jobStateListeners.delete(id);
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Update a job's options dynamically.
|
|
1977
|
-
* Searches all roots to find the job by ID.
|
|
1978
|
-
* Phase/constraint changes trigger a rebuild of the sorted job list.
|
|
1979
|
-
* @param {string} id - The job ID to update
|
|
1980
|
-
* @param {Partial<JobOptions>} options - The options to update
|
|
1981
|
-
* @returns {void}
|
|
1982
|
-
*/
|
|
1983
|
-
updateJob(id, options) {
|
|
1984
|
-
let job;
|
|
1985
|
-
let root;
|
|
1986
|
-
for (const r of this.roots.values()) {
|
|
1987
|
-
job = r.jobs.get(id);
|
|
1988
|
-
if (job) {
|
|
1989
|
-
root = r;
|
|
1990
|
-
break;
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
if (!job || !root) return;
|
|
1994
|
-
if (options.priority !== void 0) job.priority = options.priority;
|
|
1995
|
-
if (options.fps !== void 0) job.fps = options.fps;
|
|
1996
|
-
if (options.drop !== void 0) job.drop = options.drop;
|
|
1997
|
-
if (options.enabled !== void 0) {
|
|
1998
|
-
const wasEnabled = job.enabled;
|
|
1999
|
-
job.enabled = options.enabled;
|
|
2000
|
-
if (!wasEnabled && job.enabled) resetJobTiming(job);
|
|
2001
|
-
if (wasEnabled !== job.enabled) root.needsRebuild = true;
|
|
2002
|
-
}
|
|
2003
|
-
if (options.phase !== void 0 || options.before !== void 0 || options.after !== void 0) {
|
|
2004
|
-
if (options.phase) job.phase = options.phase;
|
|
2005
|
-
if (options.before !== void 0) job.before = this.normalizeConstraints(options.before);
|
|
2006
|
-
if (options.after !== void 0) job.after = this.normalizeConstraints(options.after);
|
|
2007
|
-
root.needsRebuild = true;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
//* Job State Management Methods ================================
|
|
2011
|
-
/**
|
|
2012
|
-
* Check if a job is currently paused (disabled).
|
|
2013
|
-
* @param {string} id - The job ID to check
|
|
2014
|
-
* @returns {boolean} True if the job exists and is paused
|
|
2015
|
-
*/
|
|
2016
|
-
isJobPaused(id) {
|
|
2017
|
-
for (const root of this.roots.values()) {
|
|
2018
|
-
const job = root.jobs.get(id);
|
|
2019
|
-
if (job) return !job.enabled;
|
|
2020
|
-
}
|
|
2021
|
-
return false;
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Subscribe to state changes for a specific job.
|
|
2025
|
-
* Listener is called when job is paused or resumed.
|
|
2026
|
-
* @param {string} id - The job ID to subscribe to
|
|
2027
|
-
* @param {() => void} listener - Callback invoked on state changes
|
|
2028
|
-
* @returns {() => void} Unsubscribe function
|
|
2029
|
-
*/
|
|
2030
|
-
subscribeJobState(id, listener) {
|
|
2031
|
-
if (!this.jobStateListeners.has(id)) {
|
|
2032
|
-
this.jobStateListeners.set(id, /* @__PURE__ */ new Set());
|
|
2033
|
-
}
|
|
2034
|
-
this.jobStateListeners.get(id).add(listener);
|
|
2035
|
-
return () => {
|
|
2036
|
-
this.jobStateListeners.get(id)?.delete(listener);
|
|
2037
|
-
if (this.jobStateListeners.get(id)?.size === 0) {
|
|
2038
|
-
this.jobStateListeners.delete(id);
|
|
2039
|
-
}
|
|
2040
|
-
};
|
|
2041
|
-
}
|
|
2042
|
-
/**
|
|
2043
|
-
* Notify all listeners that a job's state has changed.
|
|
2044
|
-
* @param {string} id - The job ID that changed
|
|
2045
|
-
* @returns {void}
|
|
2046
|
-
* @private
|
|
2047
|
-
*/
|
|
2048
|
-
notifyJobStateChange(id) {
|
|
2049
|
-
this.jobStateListeners.get(id)?.forEach((listener) => listener());
|
|
2050
|
-
}
|
|
2051
|
-
/**
|
|
2052
|
-
* Pause a job by ID (sets enabled=false).
|
|
2053
|
-
* Notifies any subscribed state listeners.
|
|
2054
|
-
* @param {string} id - The job ID to pause
|
|
2055
|
-
* @returns {void}
|
|
2056
|
-
*/
|
|
2057
|
-
pauseJob(id) {
|
|
2058
|
-
this.updateJob(id, { enabled: false });
|
|
2059
|
-
this.notifyJobStateChange(id);
|
|
2060
|
-
}
|
|
2061
|
-
/**
|
|
2062
|
-
* Resume a paused job by ID (sets enabled=true).
|
|
2063
|
-
* Resets job timing to prevent frame accumulation.
|
|
2064
|
-
* Notifies any subscribed state listeners.
|
|
2065
|
-
* @param {string} id - The job ID to resume
|
|
2066
|
-
* @returns {void}
|
|
2067
|
-
*/
|
|
2068
|
-
resumeJob(id) {
|
|
2069
|
-
this.updateJob(id, { enabled: true });
|
|
2070
|
-
this.notifyJobStateChange(id);
|
|
2071
|
-
}
|
|
2072
|
-
//* Frame Loop Control Methods ================================
|
|
2073
|
-
/**
|
|
2074
|
-
* Start the requestAnimationFrame loop.
|
|
2075
|
-
* Resets timing state (elapsedTime, frameCount) on start.
|
|
2076
|
-
* No-op if already running.
|
|
2077
|
-
* @returns {void}
|
|
2078
|
-
*/
|
|
2079
|
-
start() {
|
|
2080
|
-
if (this.loopState.running) return;
|
|
2081
|
-
const { elapsedTime, createdAt } = this.loopState;
|
|
2082
|
-
let adjustedCreated = 0;
|
|
2083
|
-
if (this.stoppedTime > 0) {
|
|
2084
|
-
adjustedCreated = createdAt - (performance.now() - this.stoppedTime);
|
|
2085
|
-
this.stoppedTime = 0;
|
|
2086
|
-
}
|
|
2087
|
-
Object.assign(this.loopState, {
|
|
2088
|
-
running: true,
|
|
2089
|
-
elapsedTime: elapsedTime ?? 0,
|
|
2090
|
-
lastTime: performance.now(),
|
|
2091
|
-
createdAt: adjustedCreated > 0 ? adjustedCreated : performance.now(),
|
|
2092
|
-
frameCount: 0,
|
|
2093
|
-
rafHandle: requestAnimationFrame(this.loop)
|
|
2094
|
-
});
|
|
2095
|
-
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Stop the requestAnimationFrame loop.
|
|
2098
|
-
* Cancels any pending RAF callback.
|
|
2099
|
-
* No-op if not running.
|
|
2100
|
-
* @returns {void}
|
|
2101
|
-
*/
|
|
2102
|
-
stop() {
|
|
2103
|
-
if (!this.loopState.running) return;
|
|
2104
|
-
this.loopState.running = false;
|
|
2105
|
-
if (this.loopState.rafHandle !== null) {
|
|
2106
|
-
cancelAnimationFrame(this.loopState.rafHandle);
|
|
2107
|
-
this.loopState.rafHandle = null;
|
|
2108
|
-
}
|
|
2109
|
-
this.stoppedTime = performance.now();
|
|
2110
|
-
}
|
|
2111
|
-
/**
|
|
2112
|
-
* Request frames to be rendered in demand mode.
|
|
2113
|
-
* Accumulates pending frames (capped at 60) and starts the loop if not running.
|
|
2114
|
-
* No-op if frameloop is not 'demand'.
|
|
2115
|
-
* @param {number} [frames=1] - Number of frames to request
|
|
2116
|
-
* @param {boolean} [stackFrames=false] - Whether to add frames to existing pending count
|
|
2117
|
-
* - `false` (default): Sets pending frames to the specified value (replaces existing count)
|
|
2118
|
-
* - `true`: Adds frames to existing pending count (useful for accumulating invalidations)
|
|
2119
|
-
* @returns {void}
|
|
2120
|
-
* @example
|
|
2121
|
-
* // Request a single frame render
|
|
2122
|
-
* scheduler.invalidate();
|
|
2123
|
-
*
|
|
2124
|
-
* @example
|
|
2125
|
-
* // Request 5 frames (e.g., for animations)
|
|
2126
|
-
* scheduler.invalidate(5);
|
|
2127
|
-
*
|
|
2128
|
-
* @example
|
|
2129
|
-
* // Set pending frames to exactly 3 (don't stack with existing)
|
|
2130
|
-
* scheduler.invalidate(3, false);
|
|
2131
|
-
*
|
|
2132
|
-
* @example
|
|
2133
|
-
* // Add 2 more frames to existing pending count
|
|
2134
|
-
* scheduler.invalidate(2, true);
|
|
2135
|
-
*/
|
|
2136
|
-
invalidate(frames = 1, stackFrames = false) {
|
|
2137
|
-
if (this._frameloop !== "demand") return;
|
|
2138
|
-
const baseFrames = stackFrames ? this.pendingFrames : 0;
|
|
2139
|
-
this.pendingFrames = Math.min(60, baseFrames + frames);
|
|
2140
|
-
if (!this.loopState.running && this.pendingFrames > 0) this.start();
|
|
2141
|
-
}
|
|
2142
|
-
/**
|
|
2143
|
-
* Reset timing state for deterministic testing.
|
|
2144
|
-
* Preserves jobs and roots but resets lastTime, frameCount, elapsedTime, etc.
|
|
2145
|
-
* @returns {void}
|
|
2146
|
-
*/
|
|
2147
|
-
resetTiming() {
|
|
2148
|
-
this.loopState.lastTime = null;
|
|
2149
|
-
this.loopState.frameCount = 0;
|
|
2150
|
-
this.loopState.elapsedTime = 0;
|
|
2151
|
-
this.loopState.createdAt = performance.now();
|
|
2152
|
-
}
|
|
2153
|
-
//* Manual Stepping Methods ================================
|
|
2154
|
-
/**
|
|
2155
|
-
* Manually execute a single frame for all roots.
|
|
2156
|
-
* Useful for frameloop='never' mode or testing scenarios.
|
|
2157
|
-
* @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
|
|
2158
|
-
* @returns {void}
|
|
2159
|
-
* @example
|
|
2160
|
-
* // Manual control mode
|
|
2161
|
-
* scheduler.frameloop = 'never';
|
|
2162
|
-
* scheduler.step(); // Execute one frame
|
|
2163
|
-
*/
|
|
2164
|
-
step(timestamp) {
|
|
2165
|
-
const now = timestamp ?? performance.now();
|
|
2166
|
-
this.executeFrame(now);
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Manually execute a single job by its ID.
|
|
2170
|
-
* Useful for testing individual job callbacks in isolation.
|
|
2171
|
-
* @param {string} id - The job ID to step
|
|
2172
|
-
* @param {number} [timestamp] - Optional timestamp (defaults to performance.now())
|
|
2173
|
-
* @returns {void}
|
|
2174
|
-
*/
|
|
2175
|
-
stepJob(id, timestamp) {
|
|
2176
|
-
let job;
|
|
2177
|
-
let root;
|
|
2178
|
-
for (const r of this.roots.values()) {
|
|
2179
|
-
job = r.jobs.get(id);
|
|
2180
|
-
if (job) {
|
|
2181
|
-
root = r;
|
|
2182
|
-
break;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
if (!job || !root) {
|
|
2186
|
-
console.warn(`[Scheduler] Job "${id}" not found`);
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
const now = timestamp ?? performance.now();
|
|
2190
|
-
const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
|
|
2191
|
-
const delta = deltaMs / 1e3;
|
|
2192
|
-
const elapsed = now - this.loopState.createdAt;
|
|
2193
|
-
const providedState = root.getState?.() ?? {};
|
|
2194
|
-
const frameState = {
|
|
2195
|
-
...providedState,
|
|
2196
|
-
time: now,
|
|
2197
|
-
delta,
|
|
2198
|
-
elapsed,
|
|
2199
|
-
frame: this.loopState.frameCount
|
|
2200
|
-
};
|
|
2201
|
-
try {
|
|
2202
|
-
job.callback(frameState, delta);
|
|
2203
|
-
} catch (error) {
|
|
2204
|
-
console.error(`[Scheduler] Error in job "${job.id}":`, error);
|
|
2205
|
-
this.triggerError(error instanceof Error ? error : new Error(String(error)));
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
/**
|
|
2209
|
-
* Execute a single frame across all roots.
|
|
2210
|
-
* Order: globalBefore → each root's jobs → globalAfter
|
|
2211
|
-
* @param {number} timestamp - RAF timestamp in milliseconds
|
|
2212
|
-
* @returns {void}
|
|
2213
|
-
* @private
|
|
2214
|
-
*/
|
|
2215
|
-
executeFrame(timestamp) {
|
|
2216
|
-
const deltaMs = this.loopState.lastTime !== null ? timestamp - this.loopState.lastTime : 0;
|
|
2217
|
-
const delta = deltaMs / 1e3;
|
|
2218
|
-
this.loopState.lastTime = timestamp;
|
|
2219
|
-
this.loopState.frameCount++;
|
|
2220
|
-
this.loopState.elapsedTime += deltaMs;
|
|
2221
|
-
this.runGlobalJobs(this.globalBeforeJobs, timestamp);
|
|
2222
|
-
for (const root of this.roots.values()) {
|
|
2223
|
-
this.tickRoot(root, timestamp, delta);
|
|
2224
|
-
}
|
|
2225
|
-
this.runGlobalJobs(this.globalAfterJobs, timestamp);
|
|
2226
|
-
}
|
|
2227
|
-
/**
|
|
2228
|
-
* Run all global jobs from a job map.
|
|
2229
|
-
* Catches and logs errors without stopping execution.
|
|
2230
|
-
* @param {Map<string, GlobalJob>} jobs - The global jobs map to execute
|
|
2231
|
-
* @param {number} timestamp - RAF timestamp in milliseconds
|
|
2232
|
-
* @returns {void}
|
|
2233
|
-
* @private
|
|
2234
|
-
*/
|
|
2235
|
-
runGlobalJobs(jobs, timestamp) {
|
|
2236
|
-
for (const job of jobs.values()) {
|
|
2237
|
-
try {
|
|
2238
|
-
job.callback(timestamp);
|
|
2239
|
-
} catch (error) {
|
|
2240
|
-
console.error(`[Scheduler] Error in global job "${job.id}":`, error);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
/**
|
|
2245
|
-
* Execute all jobs for a single root in sorted order.
|
|
2246
|
-
* Rebuilds sorted job list if needed, then dispatches each job.
|
|
2247
|
-
* Errors are caught and propagated via triggerError.
|
|
2248
|
-
* @param {RootEntry} root - The root entry to tick
|
|
2249
|
-
* @param {number} timestamp - RAF timestamp in milliseconds
|
|
2250
|
-
* @param {number} delta - Time since last frame in seconds
|
|
2251
|
-
* @returns {void}
|
|
2252
|
-
* @private
|
|
2253
|
-
*/
|
|
2254
|
-
tickRoot(root, timestamp, delta) {
|
|
2255
|
-
if (root.needsRebuild) {
|
|
2256
|
-
root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
|
|
2257
|
-
root.needsRebuild = false;
|
|
2258
|
-
}
|
|
2259
|
-
const providedState = root.getState?.() ?? {};
|
|
2260
|
-
const frameState = {
|
|
2261
|
-
...providedState,
|
|
2262
|
-
time: timestamp,
|
|
2263
|
-
delta,
|
|
2264
|
-
elapsed: this.loopState.elapsedTime / 1e3,
|
|
2265
|
-
// Convert ms to seconds
|
|
2266
|
-
frame: this.loopState.frameCount
|
|
2267
|
-
};
|
|
2268
|
-
for (const job of root.sortedJobs) {
|
|
2269
|
-
if (!shouldRun(job, timestamp)) continue;
|
|
2270
|
-
try {
|
|
2271
|
-
job.callback(frameState, delta);
|
|
2272
|
-
} catch (error) {
|
|
2273
|
-
console.error(`[Scheduler] Error in job "${job.id}":`, error);
|
|
2274
|
-
this.triggerError(error instanceof Error ? error : new Error(String(error)));
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
//* Debug & Inspection Methods ================================
|
|
2279
|
-
/**
|
|
2280
|
-
* Get the total number of registered jobs across all roots.
|
|
2281
|
-
* Includes both per-root jobs and global before/after jobs.
|
|
2282
|
-
* @returns {number} Total job count
|
|
2283
|
-
*/
|
|
2284
|
-
getJobCount() {
|
|
2285
|
-
let count = 0;
|
|
2286
|
-
for (const root of this.roots.values()) {
|
|
2287
|
-
count += root.jobs.size;
|
|
2288
|
-
}
|
|
2289
|
-
return count + this.globalBeforeJobs.size + this.globalAfterJobs.size;
|
|
2290
|
-
}
|
|
2291
|
-
/**
|
|
2292
|
-
* Get all registered job IDs across all roots.
|
|
2293
|
-
* Includes both per-root jobs and global before/after jobs.
|
|
2294
|
-
* @returns {string[]} Array of all job IDs
|
|
2295
|
-
*/
|
|
2296
|
-
getJobIds() {
|
|
2297
|
-
const ids = [];
|
|
2298
|
-
for (const root of this.roots.values()) {
|
|
2299
|
-
ids.push(...root.jobs.keys());
|
|
2300
|
-
}
|
|
2301
|
-
ids.push(...this.globalBeforeJobs.keys());
|
|
2302
|
-
ids.push(...this.globalAfterJobs.keys());
|
|
2303
|
-
return ids;
|
|
2304
|
-
}
|
|
2305
|
-
/**
|
|
2306
|
-
* Get the number of registered roots (Canvas instances).
|
|
2307
|
-
* @returns {number} Number of registered roots
|
|
2308
|
-
*/
|
|
2309
|
-
getRootCount() {
|
|
2310
|
-
return this.roots.size;
|
|
2311
|
-
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Check if any user (non-system) jobs are registered in a specific phase.
|
|
2314
|
-
* Used by the default render job to know if a user has taken over rendering.
|
|
2315
|
-
*
|
|
2316
|
-
* @param phase The phase to check
|
|
2317
|
-
* @param rootId Optional root ID to check (checks all roots if not provided)
|
|
2318
|
-
* @returns true if any user jobs exist in the phase
|
|
2319
|
-
*/
|
|
2320
|
-
hasUserJobsInPhase(phase, rootId) {
|
|
2321
|
-
const rootsToCheck = rootId ? [this.roots.get(rootId)].filter(Boolean) : Array.from(this.roots.values());
|
|
2322
|
-
return rootsToCheck.some((root) => {
|
|
2323
|
-
if (!root) return false;
|
|
2324
|
-
for (const job of root.jobs.values()) {
|
|
2325
|
-
if (job.phase === phase && !job.system && job.enabled) return true;
|
|
2326
|
-
}
|
|
2327
|
-
return false;
|
|
2328
|
-
});
|
|
2329
|
-
}
|
|
2330
|
-
//* Utility Methods ================================
|
|
2331
|
-
/**
|
|
2332
|
-
* Generate a unique root ID for automatic root registration.
|
|
2333
|
-
* @returns {string} A unique root ID in the format 'root_N'
|
|
2334
|
-
*/
|
|
2335
|
-
generateRootId() {
|
|
2336
|
-
return `root_${this.nextRootIndex++}`;
|
|
2337
|
-
}
|
|
2338
|
-
/**
|
|
2339
|
-
* Generate a unique job ID.
|
|
2340
|
-
* @returns {string} A unique job ID in the format 'job_N'
|
|
2341
|
-
* @private
|
|
2342
|
-
*/
|
|
2343
|
-
generateJobId() {
|
|
2344
|
-
return `job_${this.nextJobIndex}`;
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Normalize before/after constraints to a Set.
|
|
2348
|
-
* Handles undefined, single string, or array inputs.
|
|
2349
|
-
* @param {string | string[] | undefined} value - The constraint value(s)
|
|
2350
|
-
* @returns {Set<string>} Normalized Set of constraint strings
|
|
2351
|
-
* @private
|
|
2352
|
-
*/
|
|
2353
|
-
normalizeConstraints(value) {
|
|
2354
|
-
if (!value) return /* @__PURE__ */ new Set();
|
|
2355
|
-
if (Array.isArray(value)) return new Set(value);
|
|
2356
|
-
return /* @__PURE__ */ new Set([value]);
|
|
2357
|
-
}
|
|
2358
|
-
};
|
|
2359
|
-
//* Static State & Methods (Singleton Usage) ================================
|
|
2360
|
-
//* Cross-Bundle Singleton Key ==============================
|
|
2361
|
-
// Use Symbol.for() to ensure scheduler is shared across bundle boundaries
|
|
2362
|
-
// This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
|
|
2363
|
-
__publicField$1(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
|
|
2364
|
-
let Scheduler = _Scheduler;
|
|
2365
|
-
const getScheduler = () => Scheduler.get();
|
|
2366
|
-
if (hmrData) {
|
|
2367
|
-
hmrData.accept?.();
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
function useFrame(callback, priorityOrOptions) {
|
|
2371
|
-
const store = React.useContext(context);
|
|
2372
|
-
const isInsideCanvas = store !== null;
|
|
2373
|
-
const scheduler = getScheduler();
|
|
2374
|
-
const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
|
|
2375
|
-
id: priorityOrOptions.id,
|
|
2376
|
-
phase: priorityOrOptions.phase,
|
|
2377
|
-
priority: priorityOrOptions.priority,
|
|
2378
|
-
fps: priorityOrOptions.fps,
|
|
2379
|
-
drop: priorityOrOptions.drop,
|
|
2380
|
-
enabled: priorityOrOptions.enabled,
|
|
2381
|
-
before: priorityOrOptions.before,
|
|
2382
|
-
after: priorityOrOptions.after
|
|
2383
|
-
}) : "";
|
|
2384
|
-
const options = React.useMemo(() => {
|
|
2385
|
-
return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
|
|
2386
|
-
}, [optionsKey]);
|
|
2387
|
-
const reactId = React.useId();
|
|
2388
|
-
const id = options.id ?? reactId;
|
|
2389
|
-
const callbackRef = useMutableCallback(callback);
|
|
2390
|
-
const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
|
|
2391
|
-
useIsomorphicLayoutEffect(() => {
|
|
2392
|
-
if (!callback) return;
|
|
2393
|
-
if (isInsideCanvas) {
|
|
2394
|
-
const state = store.getState();
|
|
2395
|
-
const rootId = state.internal.rootId;
|
|
2396
|
-
if (isLegacyPriority) {
|
|
2397
|
-
state.internal.priority++;
|
|
2398
|
-
let parentRoot = state.previousRoot;
|
|
2399
|
-
while (parentRoot) {
|
|
2400
|
-
const parentState = parentRoot.getState();
|
|
2401
|
-
if (parentState?.internal) parentState.internal.priority++;
|
|
2402
|
-
parentRoot = parentState?.previousRoot;
|
|
2403
|
-
}
|
|
2404
|
-
notifyDepreciated({
|
|
2405
|
-
heading: "useFrame with numeric priority is deprecated",
|
|
2406
|
-
body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
|
|
2407
|
-
link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2410
|
-
const wrappedCallback = (frameState, delta) => {
|
|
2411
|
-
const localState = store.getState();
|
|
2412
|
-
const mergedState = {
|
|
2413
|
-
...localState,
|
|
2414
|
-
time: frameState.time,
|
|
2415
|
-
delta: frameState.delta,
|
|
2416
|
-
elapsed: frameState.elapsed,
|
|
2417
|
-
frame: frameState.frame
|
|
2418
|
-
};
|
|
2419
|
-
callbackRef.current?.(mergedState, delta);
|
|
2420
|
-
};
|
|
2421
|
-
const unregister = scheduler.register(wrappedCallback, {
|
|
2422
|
-
id,
|
|
2423
|
-
rootId,
|
|
2424
|
-
...options
|
|
2425
|
-
});
|
|
2426
|
-
return () => {
|
|
2427
|
-
unregister();
|
|
2428
|
-
if (isLegacyPriority) {
|
|
2429
|
-
const currentState = store.getState();
|
|
2430
|
-
if (currentState.internal) {
|
|
2431
|
-
currentState.internal.priority--;
|
|
2432
|
-
let parentRoot = currentState.previousRoot;
|
|
2433
|
-
while (parentRoot) {
|
|
2434
|
-
const parentState = parentRoot.getState();
|
|
2435
|
-
if (parentState?.internal) parentState.internal.priority--;
|
|
2436
|
-
parentRoot = parentState?.previousRoot;
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
};
|
|
2441
|
-
} else {
|
|
2442
|
-
const registerOutside = () => {
|
|
2443
|
-
return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
|
|
2444
|
-
};
|
|
2445
|
-
if (scheduler.independent || scheduler.isReady) {
|
|
2446
|
-
return registerOutside();
|
|
2447
|
-
}
|
|
2448
|
-
let unregisterJob = null;
|
|
2449
|
-
const unsubReady = scheduler.onRootReady(() => {
|
|
2450
|
-
unregisterJob = registerOutside();
|
|
2451
|
-
});
|
|
2452
|
-
return () => {
|
|
2453
|
-
unsubReady();
|
|
2454
|
-
unregisterJob?.();
|
|
2455
|
-
};
|
|
2043
|
+
return scheduler.register(
|
|
2044
|
+
(state, delta) => {
|
|
2045
|
+
const frameState = state;
|
|
2046
|
+
if (!frameState.renderer) return;
|
|
2047
|
+
callbackRef.current?.(frameState, delta);
|
|
2048
|
+
},
|
|
2049
|
+
{ id, ...options }
|
|
2050
|
+
);
|
|
2456
2051
|
}
|
|
2457
2052
|
}, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
|
|
2458
2053
|
const isPaused = React.useSyncExternalStore(
|
|
@@ -2543,22 +2138,29 @@ function buildFromCache(input, textureCache) {
|
|
|
2543
2138
|
function useTexture(input, optionsOrOnLoad) {
|
|
2544
2139
|
const renderer = useThree((state) => state.internal.actualRenderer);
|
|
2545
2140
|
const store = useStore();
|
|
2546
|
-
const textureCache = useThree((state) => state.textures);
|
|
2547
2141
|
const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
|
|
2548
|
-
const { onLoad, cache =
|
|
2142
|
+
const { onLoad, cache = true } = options;
|
|
2143
|
+
const onLoadRef = useRef(onLoad);
|
|
2144
|
+
onLoadRef.current = onLoad;
|
|
2145
|
+
const onLoadCalledForRef = useRef(null);
|
|
2549
2146
|
const urls = useMemo(() => getUrls(input), [input]);
|
|
2550
2147
|
const cachedResult = useMemo(() => {
|
|
2551
2148
|
if (!cache) return null;
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2149
|
+
const textures = store.getState().textures;
|
|
2150
|
+
if (!allUrlsCached(urls, textures)) return null;
|
|
2151
|
+
return buildFromCache(input, textures);
|
|
2152
|
+
}, [cache, urls, input, store]);
|
|
2555
2153
|
const loadedTextures = useLoader(
|
|
2556
2154
|
TextureLoader,
|
|
2557
2155
|
IsObject(input) ? Object.values(input) : input
|
|
2558
2156
|
);
|
|
2157
|
+
const inputKey = urls.join("\0");
|
|
2559
2158
|
useLayoutEffect(() => {
|
|
2560
|
-
if (
|
|
2561
|
-
|
|
2159
|
+
if (cachedResult) return;
|
|
2160
|
+
if (onLoadCalledForRef.current === inputKey) return;
|
|
2161
|
+
onLoadCalledForRef.current = inputKey;
|
|
2162
|
+
onLoadRef.current?.(loadedTextures);
|
|
2163
|
+
}, [cachedResult, loadedTextures, inputKey]);
|
|
2562
2164
|
useEffect(() => {
|
|
2563
2165
|
if (cachedResult) return;
|
|
2564
2166
|
if ("initTexture" in renderer) {
|
|
@@ -2591,8 +2193,6 @@ function useTexture(input, optionsOrOnLoad) {
|
|
|
2591
2193
|
}, [input, loadedTextures, cachedResult]);
|
|
2592
2194
|
useEffect(() => {
|
|
2593
2195
|
if (!cache) return;
|
|
2594
|
-
if (cachedResult) return;
|
|
2595
|
-
const set = store.setState;
|
|
2596
2196
|
const urlTextureMap = [];
|
|
2597
2197
|
if (typeof input === "string") {
|
|
2598
2198
|
urlTextureMap.push([input, mappedTextures]);
|
|
@@ -2606,18 +2206,32 @@ function useTexture(input, optionsOrOnLoad) {
|
|
|
2606
2206
|
urlTextureMap.push([url, textureRecord[key]]);
|
|
2607
2207
|
}
|
|
2608
2208
|
}
|
|
2609
|
-
|
|
2610
|
-
const
|
|
2611
|
-
let
|
|
2209
|
+
store.setState((state) => {
|
|
2210
|
+
const refs = new Map(state._textureRefs);
|
|
2211
|
+
let textures = state.textures;
|
|
2212
|
+
let added = false;
|
|
2612
2213
|
for (const [url, texture] of urlTextureMap) {
|
|
2613
|
-
if (!
|
|
2614
|
-
|
|
2615
|
-
|
|
2214
|
+
if (!textures.has(url)) {
|
|
2215
|
+
if (!added) {
|
|
2216
|
+
textures = new Map(textures);
|
|
2217
|
+
added = true;
|
|
2218
|
+
}
|
|
2219
|
+
textures.set(url, texture);
|
|
2616
2220
|
}
|
|
2221
|
+
refs.set(url, (refs.get(url) ?? 0) + 1);
|
|
2617
2222
|
}
|
|
2618
|
-
return
|
|
2223
|
+
return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
|
|
2224
|
+
});
|
|
2225
|
+
return () => store.setState((state) => {
|
|
2226
|
+
const refs = new Map(state._textureRefs);
|
|
2227
|
+
for (const [url] of urlTextureMap) {
|
|
2228
|
+
const next = (refs.get(url) ?? 0) - 1;
|
|
2229
|
+
if (next <= 0) refs.delete(url);
|
|
2230
|
+
else refs.set(url, next);
|
|
2231
|
+
}
|
|
2232
|
+
return { _textureRefs: refs };
|
|
2619
2233
|
});
|
|
2620
|
-
}, [cache, input, mappedTextures, store
|
|
2234
|
+
}, [cache, input, mappedTextures, store]);
|
|
2621
2235
|
return mappedTextures;
|
|
2622
2236
|
}
|
|
2623
2237
|
useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
|
|
@@ -2633,106 +2247,90 @@ const Texture = ({
|
|
|
2633
2247
|
return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
|
|
2634
2248
|
};
|
|
2635
2249
|
|
|
2636
|
-
function
|
|
2637
|
-
if (entry instanceof Texture$1) return entry;
|
|
2638
|
-
if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof Texture$1) {
|
|
2639
|
-
return entry.value;
|
|
2640
|
-
}
|
|
2641
|
-
return null;
|
|
2642
|
-
}
|
|
2643
|
-
function useTextures() {
|
|
2250
|
+
function useTextures(selector) {
|
|
2644
2251
|
const store = useStore();
|
|
2645
|
-
|
|
2646
|
-
const set = store.setState;
|
|
2252
|
+
const registry = useMemo(() => {
|
|
2647
2253
|
const getState = store.getState;
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
const newMap = new Map(state.textures);
|
|
2651
|
-
newMap.set(key, value);
|
|
2652
|
-
return { textures: newMap };
|
|
2653
|
-
});
|
|
2654
|
-
};
|
|
2655
|
-
const addMultiple = (items) => {
|
|
2656
|
-
set((state) => {
|
|
2657
|
-
const newMap = new Map(state.textures);
|
|
2658
|
-
const entries = items instanceof Map ? items.entries() : Object.entries(items);
|
|
2659
|
-
for (const [key, value] of entries) {
|
|
2660
|
-
newMap.set(key, value);
|
|
2661
|
-
}
|
|
2662
|
-
return { textures: newMap };
|
|
2663
|
-
});
|
|
2664
|
-
};
|
|
2665
|
-
const remove = (key) => {
|
|
2666
|
-
set((state) => {
|
|
2667
|
-
const newMap = new Map(state.textures);
|
|
2668
|
-
newMap.delete(key);
|
|
2669
|
-
return { textures: newMap };
|
|
2670
|
-
});
|
|
2671
|
-
};
|
|
2672
|
-
const removeMultiple = (keys) => {
|
|
2673
|
-
set((state) => {
|
|
2674
|
-
const newMap = new Map(state.textures);
|
|
2675
|
-
for (const key of keys) newMap.delete(key);
|
|
2676
|
-
return { textures: newMap };
|
|
2677
|
-
});
|
|
2678
|
-
};
|
|
2679
|
-
const dispose = (key) => {
|
|
2680
|
-
const entry = getState().textures.get(key);
|
|
2681
|
-
if (entry) {
|
|
2682
|
-
const tex = getTextureValue(entry);
|
|
2683
|
-
tex?.dispose();
|
|
2684
|
-
}
|
|
2685
|
-
remove(key);
|
|
2686
|
-
};
|
|
2687
|
-
const disposeMultiple = (keys) => {
|
|
2688
|
-
const textures = getState().textures;
|
|
2689
|
-
for (const key of keys) {
|
|
2690
|
-
const entry = textures.get(key);
|
|
2691
|
-
if (entry) {
|
|
2692
|
-
const tex = getTextureValue(entry);
|
|
2693
|
-
tex?.dispose();
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
removeMultiple(keys);
|
|
2697
|
-
};
|
|
2698
|
-
const disposeAll = () => {
|
|
2699
|
-
const textures = getState().textures;
|
|
2700
|
-
for (const entry of textures.values()) {
|
|
2701
|
-
const tex = getTextureValue(entry);
|
|
2702
|
-
tex?.dispose();
|
|
2703
|
-
}
|
|
2704
|
-
set({ textures: /* @__PURE__ */ new Map() });
|
|
2705
|
-
};
|
|
2254
|
+
const setState = store.setState;
|
|
2255
|
+
const getOne = (key) => getState().textures.get(key);
|
|
2706
2256
|
return {
|
|
2707
|
-
|
|
2708
|
-
get textures() {
|
|
2257
|
+
get all() {
|
|
2709
2258
|
return getState().textures;
|
|
2710
2259
|
},
|
|
2711
|
-
|
|
2712
|
-
|
|
2260
|
+
get(input) {
|
|
2261
|
+
if (typeof input === "string") return getOne(input);
|
|
2262
|
+
if (Array.isArray(input)) return input.map(getOne);
|
|
2263
|
+
const out = {};
|
|
2264
|
+
for (const name in input) out[name] = getOne(input[name]);
|
|
2265
|
+
return out;
|
|
2266
|
+
},
|
|
2713
2267
|
has: (key) => getState().textures.has(key),
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2268
|
+
add(keyOrRecord, texture) {
|
|
2269
|
+
setState((state) => {
|
|
2270
|
+
const textures = new Map(state.textures);
|
|
2271
|
+
if (typeof keyOrRecord === "string") {
|
|
2272
|
+
textures.set(keyOrRecord, texture);
|
|
2273
|
+
} else {
|
|
2274
|
+
for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
|
|
2275
|
+
}
|
|
2276
|
+
return { textures };
|
|
2277
|
+
});
|
|
2278
|
+
},
|
|
2279
|
+
dispose(key, options) {
|
|
2280
|
+
const state = getState();
|
|
2281
|
+
const refs = state._textureRefs.get(key) ?? 0;
|
|
2282
|
+
if (refs > 0 && !options?.force) {
|
|
2283
|
+
console.warn(
|
|
2284
|
+
`[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
|
|
2285
|
+
);
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
state.textures.get(key)?.dispose();
|
|
2289
|
+
setState((s) => {
|
|
2290
|
+
const textures = new Map(s.textures);
|
|
2291
|
+
textures.delete(key);
|
|
2292
|
+
const nextRefs = new Map(s._textureRefs);
|
|
2293
|
+
nextRefs.delete(key);
|
|
2294
|
+
return { textures, _textureRefs: nextRefs };
|
|
2295
|
+
});
|
|
2296
|
+
return true;
|
|
2297
|
+
},
|
|
2298
|
+
disposeAll() {
|
|
2299
|
+
for (const texture of getState().textures.values()) texture.dispose();
|
|
2300
|
+
setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
|
|
2301
|
+
}
|
|
2724
2302
|
};
|
|
2725
2303
|
}, [store]);
|
|
2304
|
+
const subscribe = selector ? () => selector(registry) : (state) => state.textures;
|
|
2305
|
+
const selected = useThree(subscribe);
|
|
2306
|
+
return selector ? selected : registry;
|
|
2726
2307
|
}
|
|
2727
2308
|
|
|
2728
|
-
function useRenderTarget(
|
|
2309
|
+
function useRenderTarget(widthOrOptions, heightOrOptions, options) {
|
|
2729
2310
|
const isLegacy = useThree((s) => s.isLegacy);
|
|
2730
2311
|
const size = useThree((s) => s.size);
|
|
2312
|
+
let width;
|
|
2313
|
+
let height;
|
|
2314
|
+
let opts;
|
|
2315
|
+
if (typeof widthOrOptions === "object") {
|
|
2316
|
+
opts = widthOrOptions;
|
|
2317
|
+
} else if (typeof widthOrOptions === "number") {
|
|
2318
|
+
width = widthOrOptions;
|
|
2319
|
+
if (typeof heightOrOptions === "object") {
|
|
2320
|
+
height = widthOrOptions;
|
|
2321
|
+
opts = heightOrOptions;
|
|
2322
|
+
} else if (typeof heightOrOptions === "number") {
|
|
2323
|
+
height = heightOrOptions;
|
|
2324
|
+
opts = options;
|
|
2325
|
+
} else {
|
|
2326
|
+
height = widthOrOptions;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2731
2329
|
return useMemo(() => {
|
|
2732
2330
|
const w = width ?? size.width;
|
|
2733
2331
|
const h = height ?? size.height;
|
|
2734
|
-
return new RenderTarget(w, h,
|
|
2735
|
-
}, [width, height, size.width, size.height,
|
|
2332
|
+
return new RenderTarget(w, h, opts);
|
|
2333
|
+
}, [width, height, size.width, size.height, opts, isLegacy]);
|
|
2736
2334
|
}
|
|
2737
2335
|
|
|
2738
2336
|
function useStore() {
|
|
@@ -2782,7 +2380,7 @@ function addTail(callback) {
|
|
|
2782
2380
|
function invalidate(state, frames = 1, stackFrames = false) {
|
|
2783
2381
|
getScheduler().invalidate(frames, stackFrames);
|
|
2784
2382
|
}
|
|
2785
|
-
function advance(timestamp
|
|
2383
|
+
function advance(timestamp) {
|
|
2786
2384
|
getScheduler().step(timestamp);
|
|
2787
2385
|
}
|
|
2788
2386
|
|
|
@@ -14236,6 +13834,7 @@ function swapInstances() {
|
|
|
14236
13834
|
instance.object = instance.props.object ?? new target(...instance.props.args ?? []);
|
|
14237
13835
|
instance.object.__r3f = instance;
|
|
14238
13836
|
setFiberRef(fiber, instance.object);
|
|
13837
|
+
delete instance.appliedOnce;
|
|
14239
13838
|
applyProps(instance.object, instance.props);
|
|
14240
13839
|
if (instance.props.attach) {
|
|
14241
13840
|
attach(parent, instance);
|
|
@@ -14309,8 +13908,22 @@ const reconciler = /* @__PURE__ */ createReconciler({
|
|
|
14309
13908
|
const isTailSibling = fiber.sibling === null || (fiber.flags & Update) === NoFlags;
|
|
14310
13909
|
if (isTailSibling) swapInstances();
|
|
14311
13910
|
},
|
|
14312
|
-
finalizeInitialChildren: () =>
|
|
14313
|
-
|
|
13911
|
+
finalizeInitialChildren: (instance) => {
|
|
13912
|
+
for (const prop in instance.props) {
|
|
13913
|
+
if (isFromRef(instance.props[prop])) return true;
|
|
13914
|
+
}
|
|
13915
|
+
return false;
|
|
13916
|
+
},
|
|
13917
|
+
commitMount(instance) {
|
|
13918
|
+
const resolved = {};
|
|
13919
|
+
for (const prop in instance.props) {
|
|
13920
|
+
const value = instance.props[prop];
|
|
13921
|
+
if (isFromRef(value)) {
|
|
13922
|
+
const ref = value[FROM_REF];
|
|
13923
|
+
if (ref.current != null) resolved[prop] = ref.current;
|
|
13924
|
+
}
|
|
13925
|
+
}
|
|
13926
|
+
if (Object.keys(resolved).length) applyProps(instance.object, resolved);
|
|
14314
13927
|
},
|
|
14315
13928
|
getPublicInstance: (instance) => instance?.object,
|
|
14316
13929
|
prepareForCommit: () => null,
|
|
@@ -14531,6 +14144,9 @@ function createRoot(canvas) {
|
|
|
14531
14144
|
let resolve;
|
|
14532
14145
|
pending = new Promise((_resolve) => resolve = _resolve);
|
|
14533
14146
|
const {
|
|
14147
|
+
id: canvasId,
|
|
14148
|
+
primaryCanvas,
|
|
14149
|
+
scheduler: schedulerConfig,
|
|
14534
14150
|
gl: glConfig,
|
|
14535
14151
|
renderer: rendererConfig,
|
|
14536
14152
|
size: propsSize,
|
|
@@ -14538,10 +14154,6 @@ function createRoot(canvas) {
|
|
|
14538
14154
|
events,
|
|
14539
14155
|
onCreated: onCreatedCallback,
|
|
14540
14156
|
shadows = false,
|
|
14541
|
-
linear = false,
|
|
14542
|
-
flat = false,
|
|
14543
|
-
textureColorSpace = SRGBColorSpace,
|
|
14544
|
-
legacy = false,
|
|
14545
14157
|
orthographic = false,
|
|
14546
14158
|
frameloop = "always",
|
|
14547
14159
|
dpr = [1, 2],
|
|
@@ -14553,11 +14165,14 @@ function createRoot(canvas) {
|
|
|
14553
14165
|
onDropMissed,
|
|
14554
14166
|
autoUpdateFrustum = true,
|
|
14555
14167
|
occlusion = false,
|
|
14556
|
-
_sizeProps
|
|
14168
|
+
_sizeProps,
|
|
14169
|
+
forceEven
|
|
14557
14170
|
} = props;
|
|
14171
|
+
const textureColorSpace = is.obj(glConfig) && !is.fun(glConfig) && !isRenderer(glConfig) && glConfig.textureColorSpace || is.obj(rendererConfig) && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && rendererConfig.textureColorSpace || SRGBColorSpace;
|
|
14558
14172
|
const state = store.getState();
|
|
14559
14173
|
const defaultGPUProps = {
|
|
14560
|
-
canvas
|
|
14174
|
+
canvas,
|
|
14175
|
+
antialias: true
|
|
14561
14176
|
};
|
|
14562
14177
|
if (glConfig && !R3F_BUILD_LEGACY) {
|
|
14563
14178
|
throw new Error(
|
|
@@ -14568,15 +14183,52 @@ function createRoot(canvas) {
|
|
|
14568
14183
|
throw new Error("Cannot use both gl and renderer props at the same time");
|
|
14569
14184
|
}
|
|
14570
14185
|
let renderer = state.internal.actualRenderer;
|
|
14571
|
-
if (!state.internal.actualRenderer) {
|
|
14186
|
+
if (primaryCanvas && !state.internal.actualRenderer) {
|
|
14187
|
+
const primary = await waitForPrimary(primaryCanvas);
|
|
14188
|
+
renderer = primary.renderer;
|
|
14189
|
+
state.internal.actualRenderer = renderer;
|
|
14190
|
+
const canvasTarget = new CanvasTarget(canvas);
|
|
14191
|
+
primary.store.setState((prev) => ({
|
|
14192
|
+
internal: { ...prev.internal, isMultiCanvas: true }
|
|
14193
|
+
}));
|
|
14194
|
+
state.set((prev) => ({
|
|
14195
|
+
webGPUSupported: primary.store.getState().webGPUSupported,
|
|
14196
|
+
renderer,
|
|
14197
|
+
primaryStore: primary.store,
|
|
14198
|
+
internal: {
|
|
14199
|
+
...prev.internal,
|
|
14200
|
+
canvasTarget,
|
|
14201
|
+
isMultiCanvas: true,
|
|
14202
|
+
isSecondary: true,
|
|
14203
|
+
targetId: primaryCanvas
|
|
14204
|
+
}
|
|
14205
|
+
}));
|
|
14206
|
+
} else if (!state.internal.actualRenderer) {
|
|
14572
14207
|
renderer = await resolveRenderer(rendererConfig, defaultGPUProps, WebGPURenderer);
|
|
14573
14208
|
if (!renderer.hasInitialized?.()) {
|
|
14209
|
+
const size2 = computeInitialSize(canvas, propsSize);
|
|
14210
|
+
if (size2.width > 0 && size2.height > 0) {
|
|
14211
|
+
const pixelRatio = calculateDpr(dpr);
|
|
14212
|
+
canvas.width = size2.width * pixelRatio;
|
|
14213
|
+
canvas.height = size2.height * pixelRatio;
|
|
14214
|
+
}
|
|
14574
14215
|
await renderer.init();
|
|
14575
14216
|
}
|
|
14576
14217
|
const backend = renderer.backend;
|
|
14577
14218
|
const isWebGPUBackend = backend && "isWebGPUBackend" in backend;
|
|
14578
14219
|
state.internal.actualRenderer = renderer;
|
|
14579
|
-
state.set({ webGPUSupported: isWebGPUBackend, renderer });
|
|
14220
|
+
state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
|
|
14221
|
+
if (canvasId && !state.internal.isSecondary) {
|
|
14222
|
+
const canvasTarget = new CanvasTarget(canvas);
|
|
14223
|
+
const unregisterPrimary = registerPrimary(canvasId, renderer, store);
|
|
14224
|
+
state.set((prev) => ({
|
|
14225
|
+
internal: {
|
|
14226
|
+
...prev.internal,
|
|
14227
|
+
canvasTarget,
|
|
14228
|
+
unregisterPrimary
|
|
14229
|
+
}
|
|
14230
|
+
}));
|
|
14231
|
+
}
|
|
14580
14232
|
}
|
|
14581
14233
|
let raycaster = state.raycaster;
|
|
14582
14234
|
if (!raycaster) state.set({ raycaster: raycaster = new Raycaster() });
|
|
@@ -14585,6 +14237,7 @@ function createRoot(canvas) {
|
|
|
14585
14237
|
if (!is.equ(params, raycaster.params, shallowLoose)) {
|
|
14586
14238
|
applyProps(raycaster, { params: { ...raycaster.params, ...params } });
|
|
14587
14239
|
}
|
|
14240
|
+
let tempCamera = state.camera;
|
|
14588
14241
|
if (!state.camera || state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose)) {
|
|
14589
14242
|
lastCamera = cameraOptions;
|
|
14590
14243
|
const isCamera = cameraOptions?.isCamera;
|
|
@@ -14604,6 +14257,7 @@ function createRoot(canvas) {
|
|
|
14604
14257
|
if (!state.camera && !cameraOptions?.rotation) camera.lookAt(0, 0, 0);
|
|
14605
14258
|
}
|
|
14606
14259
|
state.set({ camera });
|
|
14260
|
+
tempCamera = camera;
|
|
14607
14261
|
raycaster.camera = camera;
|
|
14608
14262
|
}
|
|
14609
14263
|
if (!state.scene) {
|
|
@@ -14621,7 +14275,7 @@ function createRoot(canvas) {
|
|
|
14621
14275
|
rootScene: scene,
|
|
14622
14276
|
internal: { ...prev.internal, container: scene }
|
|
14623
14277
|
}));
|
|
14624
|
-
const camera =
|
|
14278
|
+
const camera = tempCamera;
|
|
14625
14279
|
if (camera && !camera.parent) scene.add(camera);
|
|
14626
14280
|
}
|
|
14627
14281
|
if (events && !state.events.handlers) {
|
|
@@ -14638,6 +14292,9 @@ function createRoot(canvas) {
|
|
|
14638
14292
|
if (_sizeProps !== void 0) {
|
|
14639
14293
|
state.set({ _sizeProps });
|
|
14640
14294
|
}
|
|
14295
|
+
if (forceEven !== void 0 && state.internal.forceEven !== forceEven) {
|
|
14296
|
+
state.set((prev) => ({ internal: { ...prev.internal, forceEven } }));
|
|
14297
|
+
}
|
|
14641
14298
|
const size = computeInitialSize(canvas, propsSize);
|
|
14642
14299
|
if (!state._sizeImperative && !is.equ(size, state.size, shallowLoose)) {
|
|
14643
14300
|
const wasImperative = state._sizeImperative;
|
|
@@ -14664,10 +14321,10 @@ function createRoot(canvas) {
|
|
|
14664
14321
|
lastConfiguredProps.performance = performance;
|
|
14665
14322
|
}
|
|
14666
14323
|
if (!state.xr) {
|
|
14667
|
-
const handleXRFrame = (timestamp,
|
|
14324
|
+
const handleXRFrame = (timestamp, _frame) => {
|
|
14668
14325
|
const state2 = store.getState();
|
|
14669
14326
|
if (state2.frameloop === "never") return;
|
|
14670
|
-
advance(timestamp
|
|
14327
|
+
advance(timestamp);
|
|
14671
14328
|
};
|
|
14672
14329
|
const actualRenderer = state.internal.actualRenderer;
|
|
14673
14330
|
const handleSessionChange = () => {
|
|
@@ -14679,16 +14336,16 @@ function createRoot(canvas) {
|
|
|
14679
14336
|
};
|
|
14680
14337
|
const xr = {
|
|
14681
14338
|
connect() {
|
|
14682
|
-
const { gl, renderer: renderer2
|
|
14683
|
-
const
|
|
14684
|
-
|
|
14685
|
-
|
|
14339
|
+
const { gl, renderer: renderer2 } = store.getState();
|
|
14340
|
+
const xrManager = (renderer2 || gl).xr;
|
|
14341
|
+
xrManager.addEventListener("sessionstart", handleSessionChange);
|
|
14342
|
+
xrManager.addEventListener("sessionend", handleSessionChange);
|
|
14686
14343
|
},
|
|
14687
14344
|
disconnect() {
|
|
14688
|
-
const { gl, renderer: renderer2
|
|
14689
|
-
const
|
|
14690
|
-
|
|
14691
|
-
|
|
14345
|
+
const { gl, renderer: renderer2 } = store.getState();
|
|
14346
|
+
const xrManager = (renderer2 || gl).xr;
|
|
14347
|
+
xrManager.removeEventListener("sessionstart", handleSessionChange);
|
|
14348
|
+
xrManager.removeEventListener("sessionend", handleSessionChange);
|
|
14692
14349
|
}
|
|
14693
14350
|
};
|
|
14694
14351
|
if (typeof renderer.xr?.addEventListener === "function") xr.connect();
|
|
@@ -14700,15 +14357,22 @@ function createRoot(canvas) {
|
|
|
14700
14357
|
const oldType = renderer.shadowMap.type;
|
|
14701
14358
|
renderer.shadowMap.enabled = !!shadows;
|
|
14702
14359
|
if (is.boo(shadows)) {
|
|
14703
|
-
renderer.shadowMap.type =
|
|
14360
|
+
renderer.shadowMap.type = PCFShadowMap;
|
|
14704
14361
|
} else if (is.str(shadows)) {
|
|
14362
|
+
if (shadows === "soft") {
|
|
14363
|
+
notifyDepreciated({
|
|
14364
|
+
heading: 'shadows="soft" is deprecated',
|
|
14365
|
+
body: "Three has depreciated soft and improved basic PCFShadows, we converted for you.",
|
|
14366
|
+
link: "https://github.com/mrdoob/three.js/wiki/Migration-Guide?utm_source=chatgpt.com#181--182"
|
|
14367
|
+
});
|
|
14368
|
+
}
|
|
14705
14369
|
const types = {
|
|
14706
14370
|
basic: BasicShadowMap,
|
|
14707
14371
|
percentage: PCFShadowMap,
|
|
14708
|
-
soft:
|
|
14372
|
+
soft: PCFShadowMap,
|
|
14709
14373
|
variance: VSMShadowMap
|
|
14710
14374
|
};
|
|
14711
|
-
renderer.shadowMap.type = types[shadows] ??
|
|
14375
|
+
renderer.shadowMap.type = types[shadows] ?? PCFShadowMap;
|
|
14712
14376
|
} else if (is.obj(shadows)) {
|
|
14713
14377
|
Object.assign(renderer.shadowMap, shadows);
|
|
14714
14378
|
}
|
|
@@ -14716,27 +14380,69 @@ function createRoot(canvas) {
|
|
|
14716
14380
|
renderer.shadowMap.needsUpdate = true;
|
|
14717
14381
|
}
|
|
14718
14382
|
}
|
|
14383
|
+
if (!configured) {
|
|
14384
|
+
renderer.outputColorSpace = SRGBColorSpace;
|
|
14385
|
+
renderer.toneMapping = ACESFilmicToneMapping;
|
|
14386
|
+
}
|
|
14719
14387
|
if (textureColorSpace !== lastConfiguredProps.textureColorSpace) {
|
|
14720
14388
|
if (state.textureColorSpace !== textureColorSpace) state.set(() => ({ textureColorSpace }));
|
|
14721
14389
|
lastConfiguredProps.textureColorSpace = textureColorSpace;
|
|
14722
14390
|
}
|
|
14391
|
+
const r3fProps = ["textureColorSpace"];
|
|
14392
|
+
const constructorOnlyProps = ["samples", "antialias", "alpha", "canvas", "powerPreference"];
|
|
14393
|
+
const nonApplyProps = [...r3fProps, ...constructorOnlyProps];
|
|
14723
14394
|
if (glConfig && !is.fun(glConfig) && !isRenderer(glConfig) && !is.equ(glConfig, renderer, shallowLoose)) {
|
|
14724
|
-
|
|
14395
|
+
const glProps = {};
|
|
14396
|
+
for (const key in glConfig) {
|
|
14397
|
+
if (!nonApplyProps.includes(key)) glProps[key] = glConfig[key];
|
|
14398
|
+
}
|
|
14399
|
+
applyProps(renderer, glProps);
|
|
14725
14400
|
}
|
|
14726
14401
|
if (rendererConfig && !is.fun(rendererConfig) && !isRenderer(rendererConfig) && state.renderer) {
|
|
14727
14402
|
const currentRenderer = state.renderer;
|
|
14728
14403
|
if (!is.equ(rendererConfig, currentRenderer, shallowLoose)) {
|
|
14729
|
-
|
|
14404
|
+
const rendererProps = {};
|
|
14405
|
+
for (const key in rendererConfig) {
|
|
14406
|
+
if (!nonApplyProps.includes(key)) rendererProps[key] = rendererConfig[key];
|
|
14407
|
+
}
|
|
14408
|
+
applyProps(currentRenderer, rendererProps);
|
|
14730
14409
|
}
|
|
14731
14410
|
}
|
|
14732
14411
|
const scheduler = getScheduler();
|
|
14733
14412
|
const rootId = state.internal.rootId;
|
|
14734
14413
|
if (!rootId) {
|
|
14735
|
-
const newRootId = scheduler.generateRootId();
|
|
14414
|
+
const newRootId = canvasId || scheduler.generateRootId();
|
|
14736
14415
|
const unregisterRoot = scheduler.registerRoot(newRootId, {
|
|
14737
14416
|
getState: () => store.getState(),
|
|
14738
14417
|
onError: (err) => store.getState().setError(err)
|
|
14739
14418
|
});
|
|
14419
|
+
const unregisterCanvasTarget = scheduler.register(
|
|
14420
|
+
() => {
|
|
14421
|
+
const state2 = store.getState();
|
|
14422
|
+
if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
|
|
14423
|
+
const renderer2 = state2.internal.actualRenderer;
|
|
14424
|
+
renderer2.setCanvasTarget(state2.internal.canvasTarget);
|
|
14425
|
+
}
|
|
14426
|
+
},
|
|
14427
|
+
{
|
|
14428
|
+
id: `${newRootId}_canvasTarget`,
|
|
14429
|
+
rootId: newRootId,
|
|
14430
|
+
phase: "start",
|
|
14431
|
+
system: true
|
|
14432
|
+
}
|
|
14433
|
+
);
|
|
14434
|
+
const unregisterEventsFlush = scheduler.register(
|
|
14435
|
+
() => {
|
|
14436
|
+
const state2 = store.getState();
|
|
14437
|
+
state2.events.flush?.();
|
|
14438
|
+
},
|
|
14439
|
+
{
|
|
14440
|
+
id: `${newRootId}_events`,
|
|
14441
|
+
rootId: newRootId,
|
|
14442
|
+
phase: "input",
|
|
14443
|
+
system: true
|
|
14444
|
+
}
|
|
14445
|
+
);
|
|
14740
14446
|
const unregisterFrustum = scheduler.register(
|
|
14741
14447
|
() => {
|
|
14742
14448
|
const state2 = store.getState();
|
|
@@ -14747,7 +14453,7 @@ function createRoot(canvas) {
|
|
|
14747
14453
|
{
|
|
14748
14454
|
id: `${newRootId}_frustum`,
|
|
14749
14455
|
rootId: newRootId,
|
|
14750
|
-
|
|
14456
|
+
before: "render",
|
|
14751
14457
|
system: true
|
|
14752
14458
|
}
|
|
14753
14459
|
);
|
|
@@ -14759,7 +14465,7 @@ function createRoot(canvas) {
|
|
|
14759
14465
|
{
|
|
14760
14466
|
id: `${newRootId}_visibility`,
|
|
14761
14467
|
rootId: newRootId,
|
|
14762
|
-
|
|
14468
|
+
before: "render",
|
|
14763
14469
|
system: true,
|
|
14764
14470
|
after: `${newRootId}_frustum`
|
|
14765
14471
|
}
|
|
@@ -14771,18 +14477,22 @@ function createRoot(canvas) {
|
|
|
14771
14477
|
const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
|
|
14772
14478
|
if (userHandlesRender || state2.internal.priority) return;
|
|
14773
14479
|
try {
|
|
14774
|
-
if (state2.
|
|
14480
|
+
if (state2.renderPipeline?.render) state2.renderPipeline.render();
|
|
14775
14481
|
else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
|
|
14776
14482
|
} catch (error) {
|
|
14777
14483
|
state2.setError(error instanceof Error ? error : new Error(String(error)));
|
|
14778
14484
|
}
|
|
14779
14485
|
},
|
|
14780
14486
|
{
|
|
14781
|
-
|
|
14487
|
+
// Use canvas ID directly as job ID if available, otherwise use generated rootId
|
|
14488
|
+
id: canvasId || `${newRootId}_render`,
|
|
14782
14489
|
rootId: newRootId,
|
|
14783
14490
|
phase: "render",
|
|
14784
|
-
system: true
|
|
14491
|
+
system: true,
|
|
14785
14492
|
// Internal flag: this is a system job, not user-controlled
|
|
14493
|
+
// Apply scheduler config for render ordering and rate limiting
|
|
14494
|
+
...schedulerConfig?.after && { after: schedulerConfig.after },
|
|
14495
|
+
...schedulerConfig?.fps && { fps: schedulerConfig.fps }
|
|
14786
14496
|
}
|
|
14787
14497
|
);
|
|
14788
14498
|
state.set((state2) => ({
|
|
@@ -14791,6 +14501,8 @@ function createRoot(canvas) {
|
|
|
14791
14501
|
rootId: newRootId,
|
|
14792
14502
|
unregisterRoot: () => {
|
|
14793
14503
|
unregisterRoot();
|
|
14504
|
+
unregisterCanvasTarget();
|
|
14505
|
+
unregisterEventsFlush();
|
|
14794
14506
|
unregisterFrustum();
|
|
14795
14507
|
unregisterVisibility();
|
|
14796
14508
|
unregisterRender();
|
|
@@ -14849,15 +14561,24 @@ function unmountComponentAtNode(canvas, callback) {
|
|
|
14849
14561
|
const renderer = state.internal.actualRenderer;
|
|
14850
14562
|
const unregisterRoot = state.internal.unregisterRoot;
|
|
14851
14563
|
if (unregisterRoot) unregisterRoot();
|
|
14564
|
+
const unregisterPrimary = state.internal.unregisterPrimary;
|
|
14565
|
+
if (unregisterPrimary) unregisterPrimary();
|
|
14566
|
+
const canvasTarget = state.internal.canvasTarget;
|
|
14567
|
+
if (canvasTarget?.dispose) canvasTarget.dispose();
|
|
14852
14568
|
state.events.disconnect?.();
|
|
14853
14569
|
cleanupHelperGroup(root.store);
|
|
14854
|
-
renderer
|
|
14855
|
-
|
|
14856
|
-
|
|
14570
|
+
if (state.isLegacy && renderer) {
|
|
14571
|
+
;
|
|
14572
|
+
renderer.renderLists?.dispose?.();
|
|
14573
|
+
renderer.forceContextLoss?.();
|
|
14574
|
+
}
|
|
14575
|
+
if (!state.internal.isSecondary) {
|
|
14576
|
+
if (renderer?.xr) state.xr.disconnect();
|
|
14577
|
+
}
|
|
14857
14578
|
dispose(state.scene);
|
|
14858
14579
|
_roots.delete(canvas);
|
|
14859
14580
|
if (callback) callback(canvas);
|
|
14860
|
-
} catch
|
|
14581
|
+
} catch {
|
|
14861
14582
|
}
|
|
14862
14583
|
}, 500);
|
|
14863
14584
|
}
|
|
@@ -14865,36 +14586,34 @@ function unmountComponentAtNode(canvas, callback) {
|
|
|
14865
14586
|
}
|
|
14866
14587
|
}
|
|
14867
14588
|
function createPortal(children, container, state) {
|
|
14868
|
-
return /* @__PURE__ */ jsx(
|
|
14589
|
+
return /* @__PURE__ */ jsx(Portal, { children, container, state });
|
|
14869
14590
|
}
|
|
14870
|
-
function
|
|
14591
|
+
function Portal({ children, container, state }) {
|
|
14871
14592
|
const isRef = useCallback((obj) => obj && "current" in obj, []);
|
|
14872
|
-
const [resolvedContainer,
|
|
14593
|
+
const [resolvedContainer, _setResolvedContainer] = useState(() => {
|
|
14873
14594
|
if (isRef(container)) return container.current ?? null;
|
|
14874
14595
|
return container;
|
|
14875
14596
|
});
|
|
14597
|
+
const setResolvedContainer = useCallback(
|
|
14598
|
+
(newContainer) => {
|
|
14599
|
+
if (!newContainer || newContainer === resolvedContainer) return;
|
|
14600
|
+
_setResolvedContainer(isRef(newContainer) ? newContainer.current : newContainer);
|
|
14601
|
+
},
|
|
14602
|
+
[resolvedContainer, _setResolvedContainer, isRef]
|
|
14603
|
+
);
|
|
14876
14604
|
useMemo(() => {
|
|
14877
|
-
if (isRef(container)) {
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
const updated = container.current;
|
|
14882
|
-
if (updated && updated !== resolvedContainer) {
|
|
14883
|
-
setResolvedContainer(updated);
|
|
14884
|
-
}
|
|
14885
|
-
});
|
|
14886
|
-
} else if (current !== resolvedContainer) {
|
|
14887
|
-
setResolvedContainer(current);
|
|
14888
|
-
}
|
|
14889
|
-
} else if (container !== resolvedContainer) {
|
|
14890
|
-
setResolvedContainer(container);
|
|
14605
|
+
if (isRef(container) && !container.current) {
|
|
14606
|
+
return queueMicrotask(() => {
|
|
14607
|
+
setResolvedContainer(container.current);
|
|
14608
|
+
});
|
|
14891
14609
|
}
|
|
14892
|
-
|
|
14610
|
+
setResolvedContainer(container);
|
|
14611
|
+
}, [container, isRef, setResolvedContainer]);
|
|
14893
14612
|
if (!resolvedContainer) return /* @__PURE__ */ jsx(Fragment, {});
|
|
14894
14613
|
const portalKey = resolvedContainer.uuid ?? `portal-${resolvedContainer.id ?? "unknown"}`;
|
|
14895
|
-
return /* @__PURE__ */ jsx(
|
|
14614
|
+
return /* @__PURE__ */ jsx(PortalInner, { children, container: resolvedContainer, state }, portalKey);
|
|
14896
14615
|
}
|
|
14897
|
-
function
|
|
14616
|
+
function PortalInner({ state = {}, children, container }) {
|
|
14898
14617
|
const { events, size, injectScene = true, ...rest } = state;
|
|
14899
14618
|
const previousRoot = useStore();
|
|
14900
14619
|
const [raycaster] = useState(() => new Raycaster());
|
|
@@ -14915,11 +14634,12 @@ function Portal({ state = {}, children, container }) {
|
|
|
14915
14634
|
};
|
|
14916
14635
|
}, [portalScene, container, injectScene]);
|
|
14917
14636
|
const inject = useMutableCallback((rootState, injectState) => {
|
|
14637
|
+
const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
|
|
14918
14638
|
let viewport = void 0;
|
|
14919
|
-
if (injectState.camera && size) {
|
|
14639
|
+
if (injectState.camera && (size || injectState.size)) {
|
|
14920
14640
|
const camera = injectState.camera;
|
|
14921
|
-
viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(),
|
|
14922
|
-
if (camera !== rootState.camera) updateCamera(camera,
|
|
14641
|
+
viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
|
|
14642
|
+
if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
|
|
14923
14643
|
}
|
|
14924
14644
|
return {
|
|
14925
14645
|
// The intersect consists of the previous root state
|
|
@@ -14936,7 +14656,7 @@ function Portal({ state = {}, children, container }) {
|
|
|
14936
14656
|
previousRoot,
|
|
14937
14657
|
// Events, size and viewport can be overridden by the inject layer
|
|
14938
14658
|
events: { ...rootState.events, ...injectState.events, ...events },
|
|
14939
|
-
size:
|
|
14659
|
+
size: resolvedSize,
|
|
14940
14660
|
viewport: { ...rootState.viewport, ...viewport },
|
|
14941
14661
|
// Layers are allowed to override events
|
|
14942
14662
|
setEvents: (events2) => injectState.set((state2) => ({ ...state2, events: { ...state2.events, ...events2 } })),
|
|
@@ -14948,9 +14668,13 @@ function Portal({ state = {}, children, container }) {
|
|
|
14948
14668
|
const store = createWithEqualityFn((set, get) => ({ ...rest, set, get }));
|
|
14949
14669
|
const onMutate = (prev) => store.setState((state2) => inject.current(prev, state2));
|
|
14950
14670
|
onMutate(previousRoot.getState());
|
|
14951
|
-
previousRoot.subscribe(onMutate);
|
|
14952
14671
|
return store;
|
|
14953
14672
|
}, [previousRoot, container]);
|
|
14673
|
+
useIsomorphicLayoutEffect(() => {
|
|
14674
|
+
const onMutate = (prev) => usePortalStore.setState((state2) => inject.current(prev, state2));
|
|
14675
|
+
const unsubscribe = previousRoot.subscribe(onMutate);
|
|
14676
|
+
return unsubscribe;
|
|
14677
|
+
}, [previousRoot, usePortalStore]);
|
|
14954
14678
|
return (
|
|
14955
14679
|
// @ts-ignore, reconciler types are not maintained
|
|
14956
14680
|
/* @__PURE__ */ jsx(Fragment, { children: reconciler.createPortal(
|
|
@@ -14964,21 +14688,59 @@ function flushSync(fn) {
|
|
|
14964
14688
|
return reconciler.flushSyncFromReconciler(fn);
|
|
14965
14689
|
}
|
|
14966
14690
|
|
|
14691
|
+
function parseBackground(background) {
|
|
14692
|
+
if (!background) return null;
|
|
14693
|
+
if (typeof background === "object" && !background.isColor) {
|
|
14694
|
+
const { backgroundMap, envMap, files, preset, ...rest } = background;
|
|
14695
|
+
return {
|
|
14696
|
+
...rest,
|
|
14697
|
+
preset,
|
|
14698
|
+
files: envMap || files,
|
|
14699
|
+
backgroundFiles: backgroundMap,
|
|
14700
|
+
background: true
|
|
14701
|
+
};
|
|
14702
|
+
}
|
|
14703
|
+
if (typeof background === "number") {
|
|
14704
|
+
return { color: background, background: true };
|
|
14705
|
+
}
|
|
14706
|
+
if (typeof background === "string") {
|
|
14707
|
+
if (background in presetsObj) {
|
|
14708
|
+
return { preset: background, background: true };
|
|
14709
|
+
}
|
|
14710
|
+
if (/^(https?:\/\/|\/|\.\/|\.\.\/)|\.(hdr|exr|jpg|jpeg|png|webp|gif)$/i.test(background)) {
|
|
14711
|
+
return { files: background, background: true };
|
|
14712
|
+
}
|
|
14713
|
+
return { color: background, background: true };
|
|
14714
|
+
}
|
|
14715
|
+
if (background.isColor) {
|
|
14716
|
+
return { color: background, background: true };
|
|
14717
|
+
}
|
|
14718
|
+
return null;
|
|
14719
|
+
}
|
|
14720
|
+
|
|
14721
|
+
function clearHmrCaches(store) {
|
|
14722
|
+
store.setState((state) => ({
|
|
14723
|
+
nodes: {},
|
|
14724
|
+
uniforms: {},
|
|
14725
|
+
buffers: {},
|
|
14726
|
+
gpuStorage: {},
|
|
14727
|
+
_hmrVersion: state._hmrVersion + 1
|
|
14728
|
+
}));
|
|
14729
|
+
}
|
|
14730
|
+
|
|
14967
14731
|
function CanvasImpl({
|
|
14968
14732
|
ref,
|
|
14969
14733
|
children,
|
|
14970
14734
|
fallback,
|
|
14971
14735
|
resize,
|
|
14972
14736
|
style,
|
|
14737
|
+
id,
|
|
14973
14738
|
gl,
|
|
14974
|
-
renderer,
|
|
14739
|
+
renderer: rendererProp,
|
|
14975
14740
|
events = createPointerEvents,
|
|
14976
14741
|
eventSource,
|
|
14977
14742
|
eventPrefix,
|
|
14978
14743
|
shadows,
|
|
14979
|
-
linear,
|
|
14980
|
-
flat,
|
|
14981
|
-
legacy,
|
|
14982
14744
|
orthographic,
|
|
14983
14745
|
frameloop,
|
|
14984
14746
|
dpr,
|
|
@@ -14986,6 +14748,8 @@ function CanvasImpl({
|
|
|
14986
14748
|
raycaster,
|
|
14987
14749
|
camera,
|
|
14988
14750
|
scene,
|
|
14751
|
+
autoUpdateFrustum,
|
|
14752
|
+
occlusion,
|
|
14989
14753
|
onPointerMissed,
|
|
14990
14754
|
onDragOverMissed,
|
|
14991
14755
|
onDropMissed,
|
|
@@ -14993,10 +14757,25 @@ function CanvasImpl({
|
|
|
14993
14757
|
hmr,
|
|
14994
14758
|
width,
|
|
14995
14759
|
height,
|
|
14760
|
+
background,
|
|
14761
|
+
forceEven,
|
|
14996
14762
|
...props
|
|
14997
14763
|
}) {
|
|
14764
|
+
const isRendererConfig = typeof rendererProp === "object" && rendererProp !== null && !("render" in rendererProp) && ("primaryCanvas" in rendererProp || "scheduler" in rendererProp);
|
|
14765
|
+
let primaryCanvas;
|
|
14766
|
+
let scheduler;
|
|
14767
|
+
let renderer;
|
|
14768
|
+
if (isRendererConfig) {
|
|
14769
|
+
const { primaryCanvas: pc, scheduler: sc, ...rest } = rendererProp;
|
|
14770
|
+
primaryCanvas = pc;
|
|
14771
|
+
scheduler = sc;
|
|
14772
|
+
renderer = Object.keys(rest).length > 0 ? rest : rendererProp;
|
|
14773
|
+
} else {
|
|
14774
|
+
renderer = rendererProp;
|
|
14775
|
+
}
|
|
14998
14776
|
React.useMemo(() => extend(THREE), []);
|
|
14999
14777
|
const Bridge = useBridge();
|
|
14778
|
+
const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
|
|
15000
14779
|
const hasInitialSizeRef = React.useRef(false);
|
|
15001
14780
|
const measureConfig = React.useMemo(() => {
|
|
15002
14781
|
if (!hasInitialSizeRef.current) {
|
|
@@ -15013,15 +14792,20 @@ function CanvasImpl({
|
|
|
15013
14792
|
};
|
|
15014
14793
|
}, [resize, hasInitialSizeRef.current]);
|
|
15015
14794
|
const [containerRef, containerRect] = useMeasure(measureConfig);
|
|
15016
|
-
const effectiveSize = React.useMemo(
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
14795
|
+
const effectiveSize = React.useMemo(() => {
|
|
14796
|
+
let w = width ?? containerRect.width;
|
|
14797
|
+
let h = height ?? containerRect.height;
|
|
14798
|
+
if (forceEven) {
|
|
14799
|
+
w = Math.ceil(w / 2) * 2;
|
|
14800
|
+
h = Math.ceil(h / 2) * 2;
|
|
14801
|
+
}
|
|
14802
|
+
return {
|
|
14803
|
+
width: w,
|
|
14804
|
+
height: h,
|
|
15020
14805
|
top: containerRect.top,
|
|
15021
14806
|
left: containerRect.left
|
|
15022
|
-
}
|
|
15023
|
-
|
|
15024
|
-
);
|
|
14807
|
+
};
|
|
14808
|
+
}, [width, height, containerRect, forceEven]);
|
|
15025
14809
|
if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
|
|
15026
14810
|
hasInitialSizeRef.current = true;
|
|
15027
14811
|
}
|
|
@@ -15061,23 +14845,26 @@ function CanvasImpl({
|
|
|
15061
14845
|
async function run() {
|
|
15062
14846
|
if (!effectActiveRef.current || !root.current) return;
|
|
15063
14847
|
await root.current.configure({
|
|
14848
|
+
id,
|
|
14849
|
+
primaryCanvas,
|
|
14850
|
+
scheduler,
|
|
15064
14851
|
gl,
|
|
15065
14852
|
renderer,
|
|
15066
14853
|
scene,
|
|
15067
14854
|
events,
|
|
15068
14855
|
shadows,
|
|
15069
|
-
linear,
|
|
15070
|
-
flat,
|
|
15071
|
-
legacy,
|
|
15072
14856
|
orthographic,
|
|
15073
14857
|
frameloop,
|
|
15074
14858
|
dpr,
|
|
15075
14859
|
performance,
|
|
15076
14860
|
raycaster,
|
|
15077
14861
|
camera,
|
|
14862
|
+
autoUpdateFrustum,
|
|
14863
|
+
occlusion,
|
|
15078
14864
|
size: effectiveSize,
|
|
15079
14865
|
// Store size props for reset functionality
|
|
15080
14866
|
_sizeProps: width !== void 0 || height !== void 0 ? { width, height } : null,
|
|
14867
|
+
forceEven,
|
|
15081
14868
|
// Pass mutable reference to onPointerMissed so it's free to update
|
|
15082
14869
|
onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
|
|
15083
14870
|
onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
|
|
@@ -15101,7 +14888,10 @@ function CanvasImpl({
|
|
|
15101
14888
|
});
|
|
15102
14889
|
if (!effectActiveRef.current || !root.current) return;
|
|
15103
14890
|
root.current.render(
|
|
15104
|
-
/* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */
|
|
14891
|
+
/* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsxs(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: [
|
|
14892
|
+
backgroundProps && /* @__PURE__ */ jsx(Environment, { ...backgroundProps }),
|
|
14893
|
+
children ?? null
|
|
14894
|
+
] }) }) })
|
|
15105
14895
|
);
|
|
15106
14896
|
}
|
|
15107
14897
|
run();
|
|
@@ -15128,20 +14918,15 @@ function CanvasImpl({
|
|
|
15128
14918
|
const canvas = canvasRef.current;
|
|
15129
14919
|
if (!canvas) return;
|
|
15130
14920
|
const handleHMR = () => {
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
rootEntry
|
|
15134
|
-
|
|
15135
|
-
uniforms: {},
|
|
15136
|
-
_hmrVersion: state._hmrVersion + 1
|
|
15137
|
-
}));
|
|
15138
|
-
}
|
|
14921
|
+
queueMicrotask(() => {
|
|
14922
|
+
const rootEntry = _roots.get(canvas);
|
|
14923
|
+
if (rootEntry?.store) clearHmrCaches(rootEntry.store);
|
|
14924
|
+
});
|
|
15139
14925
|
};
|
|
15140
14926
|
if (typeof import.meta !== "undefined" && import.meta.hot) {
|
|
15141
14927
|
const hot = import.meta.hot;
|
|
15142
14928
|
hot.on("vite:afterUpdate", handleHMR);
|
|
15143
|
-
return () => hot.
|
|
15144
|
-
});
|
|
14929
|
+
return () => hot.off?.("vite:afterUpdate", handleHMR);
|
|
15145
14930
|
}
|
|
15146
14931
|
if (typeof module !== "undefined" && module.hot) {
|
|
15147
14932
|
const hot = module.hot;
|
|
@@ -15164,7 +14949,16 @@ function CanvasImpl({
|
|
|
15164
14949
|
...style
|
|
15165
14950
|
},
|
|
15166
14951
|
...props,
|
|
15167
|
-
children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
|
|
14952
|
+
children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "r3f-canvas-container", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(
|
|
14953
|
+
"canvas",
|
|
14954
|
+
{
|
|
14955
|
+
ref: canvasRef,
|
|
14956
|
+
id,
|
|
14957
|
+
className: "r3f-canvas",
|
|
14958
|
+
style: { display: "block", width: "100%", height: "100%" },
|
|
14959
|
+
children: fallback
|
|
14960
|
+
}
|
|
14961
|
+
) })
|
|
15168
14962
|
}
|
|
15169
14963
|
);
|
|
15170
14964
|
}
|
|
@@ -15237,6 +15031,48 @@ let ScopedStore = _ScopedStore;
|
|
|
15237
15031
|
function createScopedStore(data) {
|
|
15238
15032
|
return new ScopedStore(data);
|
|
15239
15033
|
}
|
|
15034
|
+
function createLazyCreatorState(state) {
|
|
15035
|
+
let _uniforms = null;
|
|
15036
|
+
let _nodes = null;
|
|
15037
|
+
let _buffers = null;
|
|
15038
|
+
let _gpuStorage = null;
|
|
15039
|
+
return Object.create(state, {
|
|
15040
|
+
uniforms: {
|
|
15041
|
+
get() {
|
|
15042
|
+
return _uniforms ?? (_uniforms = createScopedStore(state.uniforms));
|
|
15043
|
+
}
|
|
15044
|
+
},
|
|
15045
|
+
nodes: {
|
|
15046
|
+
get() {
|
|
15047
|
+
return _nodes ?? (_nodes = createScopedStore(state.nodes));
|
|
15048
|
+
}
|
|
15049
|
+
},
|
|
15050
|
+
buffers: {
|
|
15051
|
+
get() {
|
|
15052
|
+
return _buffers ?? (_buffers = createScopedStore(state.buffers));
|
|
15053
|
+
}
|
|
15054
|
+
},
|
|
15055
|
+
gpuStorage: {
|
|
15056
|
+
get() {
|
|
15057
|
+
return _gpuStorage ?? (_gpuStorage = createScopedStore(state.gpuStorage));
|
|
15058
|
+
}
|
|
15059
|
+
}
|
|
15060
|
+
});
|
|
15061
|
+
}
|
|
15062
|
+
|
|
15063
|
+
function resolvePrimary(local) {
|
|
15064
|
+
return local.getState().primaryStore ?? local;
|
|
15065
|
+
}
|
|
15066
|
+
function usePrimaryStore() {
|
|
15067
|
+
const local = useStore();
|
|
15068
|
+
const primary = local.getState().primaryStore;
|
|
15069
|
+
return useMemo(() => primary ?? local, [primary, local]);
|
|
15070
|
+
}
|
|
15071
|
+
function usePrimaryThree(selector = (state) => state, equalityFn) {
|
|
15072
|
+
const local = useStore();
|
|
15073
|
+
const primary = resolvePrimary(local);
|
|
15074
|
+
return primary(selector, equalityFn);
|
|
15075
|
+
}
|
|
15240
15076
|
|
|
15241
15077
|
function addTexture(set, key, value) {
|
|
15242
15078
|
set((state) => {
|
|
@@ -15277,6 +15113,27 @@ function createTextureOperations(set) {
|
|
|
15277
15113
|
removeMultiple: (keys) => removeTextures(set, keys)
|
|
15278
15114
|
};
|
|
15279
15115
|
}
|
|
15116
|
+
function extractTSLValue(value) {
|
|
15117
|
+
if (value === null || value === void 0) return value;
|
|
15118
|
+
if (typeof value !== "object") return value;
|
|
15119
|
+
const node = value;
|
|
15120
|
+
if (!node.isNode) return value;
|
|
15121
|
+
if (node.isConstNode) {
|
|
15122
|
+
return node.value;
|
|
15123
|
+
}
|
|
15124
|
+
if ("value" in node) {
|
|
15125
|
+
let extractedValue = node.value;
|
|
15126
|
+
if (typeof node.traverse === "function") {
|
|
15127
|
+
node.traverse((n) => {
|
|
15128
|
+
if (n.isConstNode) {
|
|
15129
|
+
extractedValue = n.value;
|
|
15130
|
+
}
|
|
15131
|
+
});
|
|
15132
|
+
}
|
|
15133
|
+
return extractedValue;
|
|
15134
|
+
}
|
|
15135
|
+
return value;
|
|
15136
|
+
}
|
|
15280
15137
|
function vectorize(inObject) {
|
|
15281
15138
|
if (inObject === null || inObject === void 0) return inObject;
|
|
15282
15139
|
if (typeof inObject === "string") {
|
|
@@ -15289,9 +15146,16 @@ function vectorize(inObject) {
|
|
|
15289
15146
|
}
|
|
15290
15147
|
if (typeof inObject !== "object") return inObject;
|
|
15291
15148
|
const obj = inObject;
|
|
15149
|
+
if (obj.isNode) {
|
|
15150
|
+
return extractTSLValue(inObject);
|
|
15151
|
+
}
|
|
15292
15152
|
if (obj.isVector2 || obj.isVector3 || obj.isVector4) return inObject;
|
|
15293
15153
|
if (obj.isMatrix3 || obj.isMatrix4) return inObject;
|
|
15294
15154
|
if (obj.isColor || obj.isEuler || obj.isQuaternion || obj.isSpherical) return inObject;
|
|
15155
|
+
if ("r" in obj && "g" in obj && "b" in obj && typeof obj.r === "number" && typeof obj.g === "number" && typeof obj.b === "number") {
|
|
15156
|
+
const scale = obj.r > 1 || obj.g > 1 || obj.b > 1 ? 1 / 255 : 1;
|
|
15157
|
+
return new Color(obj.r * scale, obj.g * scale, obj.b * scale);
|
|
15158
|
+
}
|
|
15295
15159
|
if ("x" in obj && "y" in obj && typeof obj.x === "number" && typeof obj.y === "number") {
|
|
15296
15160
|
if ("w" in obj && typeof obj.w === "number" && "z" in obj && typeof obj.z === "number") {
|
|
15297
15161
|
return new Vector4(obj.x, obj.y, obj.z, obj.w);
|
|
@@ -15315,7 +15179,7 @@ function useCompareMemoize(value, deep) {
|
|
|
15315
15179
|
|
|
15316
15180
|
const isUniformNode$1 = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
|
|
15317
15181
|
function useUniforms(creatorOrScope, scope) {
|
|
15318
|
-
const store =
|
|
15182
|
+
const store = usePrimaryStore();
|
|
15319
15183
|
const removeUniforms2 = useCallback(
|
|
15320
15184
|
(names, targetScope) => {
|
|
15321
15185
|
const nameArray = Array.isArray(names) ? names : [names];
|
|
@@ -15354,17 +15218,14 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
15354
15218
|
const rebuildUniforms = useCallback(
|
|
15355
15219
|
(targetScope) => {
|
|
15356
15220
|
store.setState((state) => {
|
|
15357
|
-
let newUniforms =
|
|
15221
|
+
let newUniforms = {};
|
|
15358
15222
|
if (targetScope && targetScope !== "root") {
|
|
15359
15223
|
const { [targetScope]: _, ...rest } = state.uniforms;
|
|
15360
15224
|
newUniforms = rest;
|
|
15361
15225
|
} else if (targetScope === "root") {
|
|
15362
|
-
newUniforms = {};
|
|
15363
15226
|
for (const [key, value] of Object.entries(state.uniforms)) {
|
|
15364
15227
|
if (!isUniformNode$1(value)) newUniforms[key] = value;
|
|
15365
15228
|
}
|
|
15366
|
-
} else {
|
|
15367
|
-
newUniforms = {};
|
|
15368
15229
|
}
|
|
15369
15230
|
return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
|
|
15370
15231
|
});
|
|
@@ -15372,20 +15233,26 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
15372
15233
|
[store]
|
|
15373
15234
|
);
|
|
15374
15235
|
const inputForMemoization = useMemo(() => {
|
|
15236
|
+
let raw = creatorOrScope;
|
|
15375
15237
|
if (is.fun(creatorOrScope)) {
|
|
15376
|
-
const
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15238
|
+
const wrappedState = createLazyCreatorState(store.getState());
|
|
15239
|
+
raw = creatorOrScope(wrappedState);
|
|
15240
|
+
}
|
|
15241
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
15242
|
+
const normalized = {};
|
|
15243
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
15244
|
+
normalized[key] = vectorize(value);
|
|
15245
|
+
}
|
|
15246
|
+
return normalized;
|
|
15383
15247
|
}
|
|
15384
|
-
return
|
|
15248
|
+
return raw;
|
|
15385
15249
|
}, [creatorOrScope, store]);
|
|
15386
15250
|
const memoizedInput = useCompareMemoize(inputForMemoization);
|
|
15387
15251
|
const isReader = memoizedInput === void 0 || typeof memoizedInput === "string";
|
|
15388
|
-
const storeUniforms =
|
|
15252
|
+
const storeUniforms = usePrimaryThree((s) => s.uniforms);
|
|
15253
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
15254
|
+
const readerDep = isReader ? storeUniforms : null;
|
|
15255
|
+
const creatorDep = isReader ? null : hmrVersion;
|
|
15389
15256
|
const uniforms = useMemo(() => {
|
|
15390
15257
|
if (memoizedInput === void 0) {
|
|
15391
15258
|
return storeUniforms;
|
|
@@ -15440,28 +15307,20 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
15440
15307
|
}
|
|
15441
15308
|
}
|
|
15442
15309
|
return result;
|
|
15443
|
-
}, [
|
|
15444
|
-
store,
|
|
15445
|
-
memoizedInput,
|
|
15446
|
-
scope,
|
|
15447
|
-
// Only include storeUniforms in deps for reader modes to enable reactivity
|
|
15448
|
-
isReader ? storeUniforms : null
|
|
15449
|
-
]);
|
|
15310
|
+
}, [store, memoizedInput, scope, readerDep, creatorDep]);
|
|
15450
15311
|
return { ...uniforms, removeUniforms: removeUniforms2, clearUniforms, rebuildUniforms };
|
|
15451
15312
|
}
|
|
15452
15313
|
function rebuildAllUniforms(store, scope) {
|
|
15314
|
+
store = store.getState().primaryStore ?? store;
|
|
15453
15315
|
store.setState((state) => {
|
|
15454
|
-
let newUniforms =
|
|
15316
|
+
let newUniforms = {};
|
|
15455
15317
|
if (scope && scope !== "root") {
|
|
15456
15318
|
const { [scope]: _, ...rest } = state.uniforms;
|
|
15457
15319
|
newUniforms = rest;
|
|
15458
15320
|
} else if (scope === "root") {
|
|
15459
|
-
newUniforms = {};
|
|
15460
15321
|
for (const [key, value] of Object.entries(state.uniforms)) {
|
|
15461
15322
|
if (!isUniformNode$1(value)) newUniforms[key] = value;
|
|
15462
15323
|
}
|
|
15463
|
-
} else {
|
|
15464
|
-
newUniforms = {};
|
|
15465
15324
|
}
|
|
15466
15325
|
return { uniforms: newUniforms, _hmrVersion: state._hmrVersion + 1 };
|
|
15467
15326
|
});
|
|
@@ -15529,15 +15388,17 @@ function isSameThreeType(a, b) {
|
|
|
15529
15388
|
}
|
|
15530
15389
|
|
|
15531
15390
|
const isUniformNode = (value) => value !== null && typeof value === "object" && "value" in value && "uuid" in value;
|
|
15391
|
+
const isTSLNode$1 = (value) => value !== null && typeof value === "object" && "uuid" in value && "nodeType" in value;
|
|
15532
15392
|
function useUniform(name, value) {
|
|
15533
15393
|
const store = useStore();
|
|
15394
|
+
const hmrVersion = useThree((s) => s._hmrVersion);
|
|
15534
15395
|
return useMemo(() => {
|
|
15535
15396
|
const state = store.getState();
|
|
15536
15397
|
const set = store.setState;
|
|
15537
15398
|
const existing = state.uniforms[name];
|
|
15538
15399
|
if (existing && isUniformNode(existing)) {
|
|
15539
|
-
if (value !== void 0) {
|
|
15540
|
-
existing.value = value;
|
|
15400
|
+
if (value !== void 0 && !isTSLNode$1(value) && !isUniformNode(value)) {
|
|
15401
|
+
existing.value = typeof value === "string" ? new Color(value) : value;
|
|
15541
15402
|
}
|
|
15542
15403
|
return existing;
|
|
15543
15404
|
}
|
|
@@ -15546,7 +15407,24 @@ function useUniform(name, value) {
|
|
|
15546
15407
|
`[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
|
|
15547
15408
|
);
|
|
15548
15409
|
}
|
|
15549
|
-
|
|
15410
|
+
if (isUniformNode(value)) {
|
|
15411
|
+
const node2 = value;
|
|
15412
|
+
if (typeof node2.setName === "function") {
|
|
15413
|
+
node2.setName(name);
|
|
15414
|
+
}
|
|
15415
|
+
set((s) => ({
|
|
15416
|
+
uniforms: { ...s.uniforms, [name]: node2 }
|
|
15417
|
+
}));
|
|
15418
|
+
return node2;
|
|
15419
|
+
}
|
|
15420
|
+
let node;
|
|
15421
|
+
if (isTSLNode$1(value)) {
|
|
15422
|
+
node = uniform(value);
|
|
15423
|
+
} else if (typeof value === "string") {
|
|
15424
|
+
node = uniform(new Color(value));
|
|
15425
|
+
} else {
|
|
15426
|
+
node = uniform(value);
|
|
15427
|
+
}
|
|
15550
15428
|
if (typeof node.setName === "function") {
|
|
15551
15429
|
node.setName(name);
|
|
15552
15430
|
}
|
|
@@ -15557,12 +15435,12 @@ function useUniform(name, value) {
|
|
|
15557
15435
|
}
|
|
15558
15436
|
}));
|
|
15559
15437
|
return node;
|
|
15560
|
-
}, [store, name]);
|
|
15438
|
+
}, [store, name, hmrVersion]);
|
|
15561
15439
|
}
|
|
15562
15440
|
|
|
15563
15441
|
const isTSLNode = (value) => value !== null && typeof value === "object" && ("uuid" in value || "nodeType" in value);
|
|
15564
15442
|
function useNodes(creatorOrScope, scope) {
|
|
15565
|
-
const store =
|
|
15443
|
+
const store = usePrimaryStore();
|
|
15566
15444
|
const removeNodes2 = useCallback(
|
|
15567
15445
|
(names, targetScope) => {
|
|
15568
15446
|
const nameArray = Array.isArray(names) ? names : [names];
|
|
@@ -15619,8 +15497,11 @@ function useNodes(creatorOrScope, scope) {
|
|
|
15619
15497
|
[store]
|
|
15620
15498
|
);
|
|
15621
15499
|
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
15622
|
-
const storeNodes =
|
|
15623
|
-
const hmrVersion =
|
|
15500
|
+
const storeNodes = usePrimaryThree((s) => s.nodes);
|
|
15501
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
15502
|
+
const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
|
|
15503
|
+
const readerDep = isReader ? storeNodes : null;
|
|
15504
|
+
const creatorDep = isReader ? null : hmrVersion;
|
|
15624
15505
|
const nodes = useMemo(() => {
|
|
15625
15506
|
if (creatorOrScope === void 0) {
|
|
15626
15507
|
return storeNodes;
|
|
@@ -15633,11 +15514,7 @@ function useNodes(creatorOrScope, scope) {
|
|
|
15633
15514
|
const state = store.getState();
|
|
15634
15515
|
const set = store.setState;
|
|
15635
15516
|
const creator = creatorOrScope;
|
|
15636
|
-
const wrappedState =
|
|
15637
|
-
...state,
|
|
15638
|
-
uniforms: createScopedStore(state.uniforms),
|
|
15639
|
-
nodes: createScopedStore(state.nodes)
|
|
15640
|
-
};
|
|
15517
|
+
const wrappedState = createLazyCreatorState(state);
|
|
15641
15518
|
const created = creator(wrappedState);
|
|
15642
15519
|
const result = {};
|
|
15643
15520
|
let hasNewNodes = false;
|
|
@@ -15647,7 +15524,7 @@ function useNodes(creatorOrScope, scope) {
|
|
|
15647
15524
|
if (currentScope[name]) {
|
|
15648
15525
|
result[name] = currentScope[name];
|
|
15649
15526
|
} else {
|
|
15650
|
-
|
|
15527
|
+
node.setName?.(`${scope}.${name}`);
|
|
15651
15528
|
result[name] = node;
|
|
15652
15529
|
hasNewNodes = true;
|
|
15653
15530
|
}
|
|
@@ -15667,7 +15544,7 @@ function useNodes(creatorOrScope, scope) {
|
|
|
15667
15544
|
if (existing && isTSLNode(existing)) {
|
|
15668
15545
|
result[name] = existing;
|
|
15669
15546
|
} else {
|
|
15670
|
-
|
|
15547
|
+
node.setName?.(name);
|
|
15671
15548
|
result[name] = node;
|
|
15672
15549
|
hasNewNodes = true;
|
|
15673
15550
|
}
|
|
@@ -15676,18 +15553,11 @@ function useNodes(creatorOrScope, scope) {
|
|
|
15676
15553
|
set((s) => ({ nodes: { ...s.nodes, ...result } }));
|
|
15677
15554
|
}
|
|
15678
15555
|
return result;
|
|
15679
|
-
}, [
|
|
15680
|
-
store,
|
|
15681
|
-
typeof creatorOrScope === "string" ? creatorOrScope : scope,
|
|
15682
|
-
// Only include storeNodes in deps for reader modes to enable reactivity
|
|
15683
|
-
// Creator mode intentionally excludes it to avoid re-running creator on unrelated changes
|
|
15684
|
-
isReader ? storeNodes : null,
|
|
15685
|
-
// Include hmrVersion for creator modes to allow rebuildNodes() to bust the cache
|
|
15686
|
-
isReader ? null : hmrVersion
|
|
15687
|
-
]);
|
|
15556
|
+
}, [store, scopeDep, readerDep, creatorDep]);
|
|
15688
15557
|
return { ...nodes, removeNodes: removeNodes2, clearNodes, rebuildNodes };
|
|
15689
15558
|
}
|
|
15690
15559
|
function rebuildAllNodes(store, scope) {
|
|
15560
|
+
store = store.getState().primaryStore ?? store;
|
|
15691
15561
|
store.setState((state) => {
|
|
15692
15562
|
let newNodes = state.nodes;
|
|
15693
15563
|
if (scope && scope !== "root") {
|
|
@@ -15732,22 +15602,364 @@ function clearRootNodes(set) {
|
|
|
15732
15602
|
});
|
|
15733
15603
|
}
|
|
15734
15604
|
function useLocalNodes(creator) {
|
|
15735
|
-
const store =
|
|
15736
|
-
const uniforms =
|
|
15737
|
-
const nodes =
|
|
15738
|
-
const textures =
|
|
15605
|
+
const store = usePrimaryStore();
|
|
15606
|
+
const uniforms = usePrimaryThree((s) => s.uniforms);
|
|
15607
|
+
const nodes = usePrimaryThree((s) => s.nodes);
|
|
15608
|
+
const textures = usePrimaryThree((s) => s.textures);
|
|
15609
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
15739
15610
|
return useMemo(() => {
|
|
15740
|
-
const
|
|
15741
|
-
const wrappedState = {
|
|
15742
|
-
...state,
|
|
15743
|
-
uniforms: createScopedStore(state.uniforms),
|
|
15744
|
-
nodes: createScopedStore(state.nodes)
|
|
15745
|
-
};
|
|
15611
|
+
const wrappedState = createLazyCreatorState(store.getState());
|
|
15746
15612
|
return creator(wrappedState);
|
|
15747
|
-
}, [store, creator, uniforms, nodes, textures]);
|
|
15613
|
+
}, [store, creator, uniforms, nodes, textures, hmrVersion]);
|
|
15614
|
+
}
|
|
15615
|
+
|
|
15616
|
+
const isBufferLike = (value) => {
|
|
15617
|
+
if (value === null || typeof value !== "object") return false;
|
|
15618
|
+
if (ArrayBuffer.isView(value)) return true;
|
|
15619
|
+
if ("isBufferAttribute" in value) return true;
|
|
15620
|
+
if ("uuid" in value || "nodeType" in value) return true;
|
|
15621
|
+
return false;
|
|
15622
|
+
};
|
|
15623
|
+
const disposeBuffer = (buffer) => {
|
|
15624
|
+
if (buffer === null || typeof buffer !== "object") return;
|
|
15625
|
+
if ("dispose" in buffer && typeof buffer.dispose === "function") {
|
|
15626
|
+
buffer.dispose();
|
|
15627
|
+
}
|
|
15628
|
+
};
|
|
15629
|
+
function useBuffers(creatorOrScope, scope) {
|
|
15630
|
+
const store = usePrimaryStore();
|
|
15631
|
+
const removeBuffers = useCallback(
|
|
15632
|
+
(names, targetScope) => {
|
|
15633
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15634
|
+
store.setState((state) => {
|
|
15635
|
+
if (targetScope) {
|
|
15636
|
+
const currentScope = { ...state.buffers[targetScope] };
|
|
15637
|
+
for (const name of nameArray) delete currentScope[name];
|
|
15638
|
+
return { buffers: { ...state.buffers, [targetScope]: currentScope } };
|
|
15639
|
+
}
|
|
15640
|
+
const buffers2 = { ...state.buffers };
|
|
15641
|
+
for (const name of nameArray) if (isBufferLike(buffers2[name])) delete buffers2[name];
|
|
15642
|
+
return { buffers: buffers2 };
|
|
15643
|
+
});
|
|
15644
|
+
},
|
|
15645
|
+
[store]
|
|
15646
|
+
);
|
|
15647
|
+
const clearBuffers = useCallback(
|
|
15648
|
+
(targetScope) => {
|
|
15649
|
+
store.setState((state) => {
|
|
15650
|
+
if (targetScope && targetScope !== "root") {
|
|
15651
|
+
const { [targetScope]: _, ...rest } = state.buffers;
|
|
15652
|
+
return { buffers: rest };
|
|
15653
|
+
}
|
|
15654
|
+
if (targetScope === "root") {
|
|
15655
|
+
const buffers2 = {};
|
|
15656
|
+
for (const [key, value] of Object.entries(state.buffers)) {
|
|
15657
|
+
if (!isBufferLike(value)) buffers2[key] = value;
|
|
15658
|
+
}
|
|
15659
|
+
return { buffers: buffers2 };
|
|
15660
|
+
}
|
|
15661
|
+
return { buffers: {} };
|
|
15662
|
+
});
|
|
15663
|
+
},
|
|
15664
|
+
[store]
|
|
15665
|
+
);
|
|
15666
|
+
const rebuildBuffers = useCallback(
|
|
15667
|
+
(targetScope) => {
|
|
15668
|
+
store.setState((state) => {
|
|
15669
|
+
let newBuffers = state.buffers;
|
|
15670
|
+
if (targetScope && targetScope !== "root") {
|
|
15671
|
+
const { [targetScope]: _, ...rest } = state.buffers;
|
|
15672
|
+
newBuffers = rest;
|
|
15673
|
+
} else if (targetScope === "root") {
|
|
15674
|
+
newBuffers = {};
|
|
15675
|
+
for (const [key, value] of Object.entries(state.buffers)) {
|
|
15676
|
+
if (!isBufferLike(value)) newBuffers[key] = value;
|
|
15677
|
+
}
|
|
15678
|
+
} else {
|
|
15679
|
+
newBuffers = {};
|
|
15680
|
+
}
|
|
15681
|
+
return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
|
|
15682
|
+
});
|
|
15683
|
+
},
|
|
15684
|
+
[store]
|
|
15685
|
+
);
|
|
15686
|
+
const disposeBuffers = useCallback(
|
|
15687
|
+
(names, targetScope) => {
|
|
15688
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15689
|
+
const state = store.getState();
|
|
15690
|
+
for (const name of nameArray) {
|
|
15691
|
+
const buffer = targetScope ? state.buffers[targetScope]?.[name] : state.buffers[name];
|
|
15692
|
+
if (buffer && isBufferLike(buffer)) {
|
|
15693
|
+
disposeBuffer(buffer);
|
|
15694
|
+
}
|
|
15695
|
+
}
|
|
15696
|
+
removeBuffers(names, targetScope);
|
|
15697
|
+
},
|
|
15698
|
+
[store, removeBuffers]
|
|
15699
|
+
);
|
|
15700
|
+
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
15701
|
+
const storeBuffers = usePrimaryThree((s) => s.buffers);
|
|
15702
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
15703
|
+
const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
|
|
15704
|
+
const readerDep = isReader ? storeBuffers : null;
|
|
15705
|
+
const creatorDep = isReader ? null : hmrVersion;
|
|
15706
|
+
const buffers = useMemo(() => {
|
|
15707
|
+
if (creatorOrScope === void 0) {
|
|
15708
|
+
return storeBuffers;
|
|
15709
|
+
}
|
|
15710
|
+
if (typeof creatorOrScope === "string") {
|
|
15711
|
+
const scopeData = storeBuffers[creatorOrScope];
|
|
15712
|
+
if (scopeData && !isBufferLike(scopeData)) return scopeData;
|
|
15713
|
+
return {};
|
|
15714
|
+
}
|
|
15715
|
+
const state = store.getState();
|
|
15716
|
+
const set = store.setState;
|
|
15717
|
+
const creator = creatorOrScope;
|
|
15718
|
+
const wrappedState = createLazyCreatorState(state);
|
|
15719
|
+
const created = creator(wrappedState);
|
|
15720
|
+
const result = {};
|
|
15721
|
+
let hasNewBuffers = false;
|
|
15722
|
+
if (scope) {
|
|
15723
|
+
const currentScope = state.buffers[scope] ?? {};
|
|
15724
|
+
for (const [name, buffer] of Object.entries(created)) {
|
|
15725
|
+
if (currentScope[name]) {
|
|
15726
|
+
result[name] = currentScope[name];
|
|
15727
|
+
} else {
|
|
15728
|
+
if ("setName" in buffer && typeof buffer.setName === "function") {
|
|
15729
|
+
buffer.setName(`${scope}.${name}`);
|
|
15730
|
+
}
|
|
15731
|
+
result[name] = buffer;
|
|
15732
|
+
hasNewBuffers = true;
|
|
15733
|
+
}
|
|
15734
|
+
}
|
|
15735
|
+
if (hasNewBuffers) {
|
|
15736
|
+
set((s) => ({
|
|
15737
|
+
buffers: {
|
|
15738
|
+
...s.buffers,
|
|
15739
|
+
[scope]: { ...s.buffers[scope], ...result }
|
|
15740
|
+
}
|
|
15741
|
+
}));
|
|
15742
|
+
}
|
|
15743
|
+
return result;
|
|
15744
|
+
}
|
|
15745
|
+
for (const [name, buffer] of Object.entries(created)) {
|
|
15746
|
+
const existing = state.buffers[name];
|
|
15747
|
+
if (existing && isBufferLike(existing)) {
|
|
15748
|
+
result[name] = existing;
|
|
15749
|
+
} else {
|
|
15750
|
+
if ("setName" in buffer && typeof buffer.setName === "function") {
|
|
15751
|
+
buffer.setName(name);
|
|
15752
|
+
}
|
|
15753
|
+
result[name] = buffer;
|
|
15754
|
+
hasNewBuffers = true;
|
|
15755
|
+
}
|
|
15756
|
+
}
|
|
15757
|
+
if (hasNewBuffers) {
|
|
15758
|
+
set((s) => ({ buffers: { ...s.buffers, ...result } }));
|
|
15759
|
+
}
|
|
15760
|
+
return result;
|
|
15761
|
+
}, [store, scopeDep, readerDep, creatorDep]);
|
|
15762
|
+
return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
|
|
15763
|
+
}
|
|
15764
|
+
function rebuildAllBuffers(store, scope) {
|
|
15765
|
+
store = store.getState().primaryStore ?? store;
|
|
15766
|
+
store.setState((state) => {
|
|
15767
|
+
let newBuffers = state.buffers;
|
|
15768
|
+
if (scope && scope !== "root") {
|
|
15769
|
+
const { [scope]: _, ...rest } = state.buffers;
|
|
15770
|
+
newBuffers = rest;
|
|
15771
|
+
} else if (scope === "root") {
|
|
15772
|
+
newBuffers = {};
|
|
15773
|
+
for (const [key, value] of Object.entries(state.buffers)) {
|
|
15774
|
+
if (!isBufferLike(value)) newBuffers[key] = value;
|
|
15775
|
+
}
|
|
15776
|
+
} else {
|
|
15777
|
+
newBuffers = {};
|
|
15778
|
+
}
|
|
15779
|
+
return { buffers: newBuffers, _hmrVersion: state._hmrVersion + 1 };
|
|
15780
|
+
});
|
|
15781
|
+
}
|
|
15782
|
+
|
|
15783
|
+
const isStorageLike = (value) => {
|
|
15784
|
+
if (value === null || typeof value !== "object") return false;
|
|
15785
|
+
if ("isTexture" in value) return true;
|
|
15786
|
+
if ("isData3DTexture" in value) return true;
|
|
15787
|
+
if ("uuid" in value || "nodeType" in value) return true;
|
|
15788
|
+
return false;
|
|
15789
|
+
};
|
|
15790
|
+
const disposeStorage = (storage) => {
|
|
15791
|
+
if (storage === null || typeof storage !== "object") return;
|
|
15792
|
+
if ("dispose" in storage && typeof storage.dispose === "function") {
|
|
15793
|
+
storage.dispose();
|
|
15794
|
+
}
|
|
15795
|
+
};
|
|
15796
|
+
function useGPUStorage(creatorOrScope, scope) {
|
|
15797
|
+
const store = usePrimaryStore();
|
|
15798
|
+
const removeStorage = useCallback(
|
|
15799
|
+
(names, targetScope) => {
|
|
15800
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15801
|
+
store.setState((state) => {
|
|
15802
|
+
if (targetScope) {
|
|
15803
|
+
const currentScope = { ...state.gpuStorage[targetScope] };
|
|
15804
|
+
for (const name of nameArray) delete currentScope[name];
|
|
15805
|
+
return { gpuStorage: { ...state.gpuStorage, [targetScope]: currentScope } };
|
|
15806
|
+
}
|
|
15807
|
+
const gpuStorage2 = { ...state.gpuStorage };
|
|
15808
|
+
for (const name of nameArray) if (isStorageLike(gpuStorage2[name])) delete gpuStorage2[name];
|
|
15809
|
+
return { gpuStorage: gpuStorage2 };
|
|
15810
|
+
});
|
|
15811
|
+
},
|
|
15812
|
+
[store]
|
|
15813
|
+
);
|
|
15814
|
+
const clearStorage = useCallback(
|
|
15815
|
+
(targetScope) => {
|
|
15816
|
+
store.setState((state) => {
|
|
15817
|
+
if (targetScope && targetScope !== "root") {
|
|
15818
|
+
const { [targetScope]: _, ...rest } = state.gpuStorage;
|
|
15819
|
+
return { gpuStorage: rest };
|
|
15820
|
+
}
|
|
15821
|
+
if (targetScope === "root") {
|
|
15822
|
+
const gpuStorage2 = {};
|
|
15823
|
+
for (const [key, value] of Object.entries(state.gpuStorage)) {
|
|
15824
|
+
if (!isStorageLike(value)) gpuStorage2[key] = value;
|
|
15825
|
+
}
|
|
15826
|
+
return { gpuStorage: gpuStorage2 };
|
|
15827
|
+
}
|
|
15828
|
+
return { gpuStorage: {} };
|
|
15829
|
+
});
|
|
15830
|
+
},
|
|
15831
|
+
[store]
|
|
15832
|
+
);
|
|
15833
|
+
const rebuildStorage = useCallback(
|
|
15834
|
+
(targetScope) => {
|
|
15835
|
+
store.setState((state) => {
|
|
15836
|
+
let newStorage = state.gpuStorage;
|
|
15837
|
+
if (targetScope && targetScope !== "root") {
|
|
15838
|
+
const { [targetScope]: _, ...rest } = state.gpuStorage;
|
|
15839
|
+
newStorage = rest;
|
|
15840
|
+
} else if (targetScope === "root") {
|
|
15841
|
+
newStorage = {};
|
|
15842
|
+
for (const [key, value] of Object.entries(state.gpuStorage)) {
|
|
15843
|
+
if (!isStorageLike(value)) newStorage[key] = value;
|
|
15844
|
+
}
|
|
15845
|
+
} else {
|
|
15846
|
+
newStorage = {};
|
|
15847
|
+
}
|
|
15848
|
+
return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
|
|
15849
|
+
});
|
|
15850
|
+
},
|
|
15851
|
+
[store]
|
|
15852
|
+
);
|
|
15853
|
+
const disposeStorageFn = useCallback(
|
|
15854
|
+
(names, targetScope) => {
|
|
15855
|
+
const nameArray = Array.isArray(names) ? names : [names];
|
|
15856
|
+
const state = store.getState();
|
|
15857
|
+
for (const name of nameArray) {
|
|
15858
|
+
const storage = targetScope ? state.gpuStorage[targetScope]?.[name] : state.gpuStorage[name];
|
|
15859
|
+
if (storage && isStorageLike(storage)) {
|
|
15860
|
+
disposeStorage(storage);
|
|
15861
|
+
}
|
|
15862
|
+
}
|
|
15863
|
+
removeStorage(names, targetScope);
|
|
15864
|
+
},
|
|
15865
|
+
[store, removeStorage]
|
|
15866
|
+
);
|
|
15867
|
+
const isReader = creatorOrScope === void 0 || typeof creatorOrScope === "string";
|
|
15868
|
+
const storeStorage = usePrimaryThree((s) => s.gpuStorage);
|
|
15869
|
+
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
15870
|
+
const scopeDep = typeof creatorOrScope === "string" ? creatorOrScope : scope;
|
|
15871
|
+
const readerDep = isReader ? storeStorage : null;
|
|
15872
|
+
const creatorDep = isReader ? null : hmrVersion;
|
|
15873
|
+
const gpuStorage = useMemo(() => {
|
|
15874
|
+
if (creatorOrScope === void 0) {
|
|
15875
|
+
return storeStorage;
|
|
15876
|
+
}
|
|
15877
|
+
if (typeof creatorOrScope === "string") {
|
|
15878
|
+
const scopeData = storeStorage[creatorOrScope];
|
|
15879
|
+
if (scopeData && !isStorageLike(scopeData)) return scopeData;
|
|
15880
|
+
return {};
|
|
15881
|
+
}
|
|
15882
|
+
const state = store.getState();
|
|
15883
|
+
const set = store.setState;
|
|
15884
|
+
const creator = creatorOrScope;
|
|
15885
|
+
const wrappedState = createLazyCreatorState(state);
|
|
15886
|
+
const created = creator(wrappedState);
|
|
15887
|
+
const result = {};
|
|
15888
|
+
let hasNewStorage = false;
|
|
15889
|
+
if (scope) {
|
|
15890
|
+
const currentScope = state.gpuStorage[scope] ?? {};
|
|
15891
|
+
for (const [name, storage] of Object.entries(created)) {
|
|
15892
|
+
if (currentScope[name]) {
|
|
15893
|
+
result[name] = currentScope[name];
|
|
15894
|
+
} else {
|
|
15895
|
+
if ("setName" in storage && typeof storage.setName === "function") {
|
|
15896
|
+
storage.setName(`${scope}.${name}`);
|
|
15897
|
+
}
|
|
15898
|
+
if ("name" in storage && typeof storage.name === "string") {
|
|
15899
|
+
storage.name = `${scope}.${name}`;
|
|
15900
|
+
}
|
|
15901
|
+
result[name] = storage;
|
|
15902
|
+
hasNewStorage = true;
|
|
15903
|
+
}
|
|
15904
|
+
}
|
|
15905
|
+
if (hasNewStorage) {
|
|
15906
|
+
set((s) => ({
|
|
15907
|
+
gpuStorage: {
|
|
15908
|
+
...s.gpuStorage,
|
|
15909
|
+
[scope]: { ...s.gpuStorage[scope], ...result }
|
|
15910
|
+
}
|
|
15911
|
+
}));
|
|
15912
|
+
}
|
|
15913
|
+
return result;
|
|
15914
|
+
}
|
|
15915
|
+
for (const [name, storage] of Object.entries(created)) {
|
|
15916
|
+
const existing = state.gpuStorage[name];
|
|
15917
|
+
if (existing && isStorageLike(existing)) {
|
|
15918
|
+
result[name] = existing;
|
|
15919
|
+
} else {
|
|
15920
|
+
if ("setName" in storage && typeof storage.setName === "function") {
|
|
15921
|
+
storage.setName(name);
|
|
15922
|
+
}
|
|
15923
|
+
if ("name" in storage && typeof storage.name === "string") {
|
|
15924
|
+
storage.name = name;
|
|
15925
|
+
}
|
|
15926
|
+
result[name] = storage;
|
|
15927
|
+
hasNewStorage = true;
|
|
15928
|
+
}
|
|
15929
|
+
}
|
|
15930
|
+
if (hasNewStorage) {
|
|
15931
|
+
set((s) => ({ gpuStorage: { ...s.gpuStorage, ...result } }));
|
|
15932
|
+
}
|
|
15933
|
+
return result;
|
|
15934
|
+
}, [store, scopeDep, readerDep, creatorDep]);
|
|
15935
|
+
return {
|
|
15936
|
+
...gpuStorage,
|
|
15937
|
+
removeStorage,
|
|
15938
|
+
clearStorage,
|
|
15939
|
+
rebuildStorage,
|
|
15940
|
+
disposeStorage: disposeStorageFn
|
|
15941
|
+
};
|
|
15942
|
+
}
|
|
15943
|
+
function rebuildAllStorage(store, scope) {
|
|
15944
|
+
store = store.getState().primaryStore ?? store;
|
|
15945
|
+
store.setState((state) => {
|
|
15946
|
+
let newStorage = state.gpuStorage;
|
|
15947
|
+
if (scope && scope !== "root") {
|
|
15948
|
+
const { [scope]: _, ...rest } = state.gpuStorage;
|
|
15949
|
+
newStorage = rest;
|
|
15950
|
+
} else if (scope === "root") {
|
|
15951
|
+
newStorage = {};
|
|
15952
|
+
for (const [key, value] of Object.entries(state.gpuStorage)) {
|
|
15953
|
+
if (!isStorageLike(value)) newStorage[key] = value;
|
|
15954
|
+
}
|
|
15955
|
+
} else {
|
|
15956
|
+
newStorage = {};
|
|
15957
|
+
}
|
|
15958
|
+
return { gpuStorage: newStorage, _hmrVersion: state._hmrVersion + 1 };
|
|
15959
|
+
});
|
|
15748
15960
|
}
|
|
15749
15961
|
|
|
15750
|
-
function
|
|
15962
|
+
function useRenderPipeline(mainCB, setupCB) {
|
|
15751
15963
|
const store = useStore();
|
|
15752
15964
|
const { scene, camera, renderer, isLegacy } = useThree();
|
|
15753
15965
|
const callbacksRanRef = useRef(false);
|
|
@@ -15766,7 +15978,7 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
15766
15978
|
}, [store]);
|
|
15767
15979
|
const reset = useCallback(() => {
|
|
15768
15980
|
store.setState({
|
|
15769
|
-
|
|
15981
|
+
renderPipeline: null,
|
|
15770
15982
|
passes: {}
|
|
15771
15983
|
});
|
|
15772
15984
|
callbacksRanRef.current = false;
|
|
@@ -15779,13 +15991,13 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
15779
15991
|
}, []);
|
|
15780
15992
|
useLayoutEffect(() => {
|
|
15781
15993
|
if (isLegacy) {
|
|
15782
|
-
throw new Error("
|
|
15994
|
+
throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
|
|
15783
15995
|
}
|
|
15784
15996
|
if (!renderer || !scene || !camera) return;
|
|
15785
15997
|
const state = store.getState();
|
|
15786
15998
|
const set = store.setState;
|
|
15787
15999
|
try {
|
|
15788
|
-
let pp = state.
|
|
16000
|
+
let pp = state.renderPipeline;
|
|
15789
16001
|
let currentPasses = { ...state.passes };
|
|
15790
16002
|
let justCreatedPP = false;
|
|
15791
16003
|
if (!pp) {
|
|
@@ -15802,7 +16014,7 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
15802
16014
|
}
|
|
15803
16015
|
currentPasses.scenePass = scenePass;
|
|
15804
16016
|
if (!pp.outputNode || justCreatedPP) pp.outputNode = scenePass;
|
|
15805
|
-
set({
|
|
16017
|
+
set({ renderPipeline: pp, passes: currentPasses });
|
|
15806
16018
|
const shouldRunCallbacks = justCreatedPP || !callbacksRanRef.current || !cacheValid;
|
|
15807
16019
|
if (shouldRunCallbacks) {
|
|
15808
16020
|
if (setupCBRef.current) {
|
|
@@ -15824,22 +16036,22 @@ function usePostProcessing(mainCB, setupCB) {
|
|
|
15824
16036
|
callbacksRanRef.current = true;
|
|
15825
16037
|
}
|
|
15826
16038
|
} catch (error) {
|
|
15827
|
-
console.error("[
|
|
16039
|
+
console.error("[useRenderPipeline] Setup error:", error);
|
|
15828
16040
|
}
|
|
15829
16041
|
}, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
|
|
15830
16042
|
const passes = useThree((s) => s.passes);
|
|
15831
|
-
const
|
|
16043
|
+
const renderPipeline = useThree((s) => s.renderPipeline);
|
|
15832
16044
|
return {
|
|
15833
16045
|
passes,
|
|
15834
|
-
|
|
16046
|
+
renderPipeline,
|
|
15835
16047
|
clearPasses,
|
|
15836
16048
|
reset,
|
|
15837
16049
|
rebuild,
|
|
15838
|
-
// isReady indicates if
|
|
15839
|
-
isReady:
|
|
16050
|
+
// isReady indicates if RenderPipeline is configured and ready for rendering
|
|
16051
|
+
isReady: renderPipeline !== null
|
|
15840
16052
|
};
|
|
15841
16053
|
}
|
|
15842
16054
|
|
|
15843
16055
|
extend(THREE);
|
|
15844
16056
|
|
|
15845
|
-
export { Block, Canvas, ErrorBoundary, IsObject, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS,
|
|
16057
|
+
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 };
|