@react-three/fiber 10.0.0-alpha.0 → 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/legacy.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as three from 'three';
2
- import { Layers, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, SRGBColorSpace, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFSoftShadowMap, VSMShadowMap, PCFShadowMap, BasicShadowMap, ColorManagement, LinearSRGBColorSpace, NoToneMapping, ACESFilmicToneMapping, WebGLRenderer } from 'three';
2
+ import { WebGLRenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, SRGBColorSpace, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFSoftShadowMap, VSMShadowMap, PCFShadowMap, BasicShadowMap, ColorManagement, LinearSRGBColorSpace, NoToneMapping, ACESFilmicToneMapping, WebGLRenderer } from 'three';
3
3
  import { jsx, Fragment } from 'react/jsx-runtime';
4
4
  import * as React from 'react';
5
5
  import React__default, { useMemo, useLayoutEffect, useEffect, useContext, useRef, useImperativeHandle, useCallback, useState } from 'react';
@@ -33,12 +33,15 @@ const WebGPURenderer = class WebGPURenderer2 {
33
33
  throw new Error("WebGPURenderer is not available in legacy builds. Use @react-three/fiber/webgpu instead.");
34
34
  }
35
35
  };
36
+ const RenderTarget = null;
36
37
 
37
38
  const THREE = /*#__PURE__*/_mergeNamespaces({
38
39
  __proto__: null,
39
40
  Inspector: Inspector,
40
41
  R3F_BUILD_LEGACY: R3F_BUILD_LEGACY,
41
42
  R3F_BUILD_WEBGPU: R3F_BUILD_WEBGPU,
43
+ RenderTarget: RenderTarget,
44
+ RenderTargetCompat: WebGLRenderTarget,
42
45
  WebGPURenderer: WebGPURenderer
43
46
  }, [three]);
44
47
 
@@ -147,6 +150,13 @@ function updateCamera(camera, size) {
147
150
  }
148
151
  camera.updateProjectionMatrix();
149
152
  }
153
+ const frustumMatrix = new Matrix4();
154
+ function updateFrustum(camera, frustum) {
155
+ const target = frustum ?? new Frustum();
156
+ frustumMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
157
+ target.setFromProjectionMatrix(frustumMatrix);
158
+ return target;
159
+ }
150
160
 
151
161
  const REACT_INTERNAL_PROPS = ["children", "key", "ref"];
152
162
  function findInitialRoot(instance) {
@@ -227,6 +237,205 @@ function invalidateInstance(instance) {
227
237
  if (state && state.internal.frames === 0) state.invalidate();
228
238
  }
229
239
 
240
+ const tempFrustum = new Frustum();
241
+ let hasWarnedWebGL = false;
242
+ let tslModule = null;
243
+ async function loadTSL() {
244
+ if (tslModule) return tslModule;
245
+ try {
246
+ const tsl = await import('three/tsl');
247
+ tslModule = { uniform: tsl.uniform, nodeObject: tsl.nodeObject };
248
+ return tslModule;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+ function createOcclusionObserverNode(store, uniform) {
254
+ const node = new Node("float");
255
+ node.updateType = NodeUpdateType.OBJECT;
256
+ node.update = function(frame) {
257
+ const { internal } = store.getState();
258
+ const registry = internal.visibilityRegistry;
259
+ const cache = internal.occlusionCache;
260
+ for (const entry of registry.values()) {
261
+ const { object, handlers } = entry;
262
+ if (handlers.onOccluded || handlers.onVisible) {
263
+ const isOccluded = frame.renderer.isOccluded(object);
264
+ cache.set(object, isOccluded);
265
+ }
266
+ }
267
+ };
268
+ node.setup = function() {
269
+ return uniform(0);
270
+ };
271
+ return node;
272
+ }
273
+ let occlusionSetupPromise = null;
274
+ function enableOcclusion(store) {
275
+ const state = store.getState();
276
+ const { internal, renderer, rootScene } = state;
277
+ if (internal.occlusionEnabled || occlusionSetupPromise) return;
278
+ const hasOcclusionSupport = typeof renderer?.isOccluded === "function";
279
+ if (!hasOcclusionSupport) {
280
+ if (!hasWarnedWebGL) {
281
+ console.warn(
282
+ "[R3F] Warning: onOccluded/onVisible occlusion queries require WebGPU renderer. Occlusion events will not fire on WebGL."
283
+ );
284
+ hasWarnedWebGL = true;
285
+ }
286
+ return;
287
+ }
288
+ occlusionSetupPromise = setupOcclusion(store);
289
+ }
290
+ async function setupOcclusion(store) {
291
+ const state = store.getState();
292
+ const { internal, rootScene, set } = state;
293
+ const tsl = await loadTSL();
294
+ if (!tsl) {
295
+ console.warn("[R3F] Warning: TSL module not available. Occlusion queries disabled.");
296
+ occlusionSetupPromise = null;
297
+ return;
298
+ }
299
+ const { uniform, nodeObject } = tsl;
300
+ let helperGroup = internal.helperGroup;
301
+ if (!helperGroup) {
302
+ helperGroup = new Group();
303
+ helperGroup.name = "__r3fInternal";
304
+ helperGroup.__r3fInternal = true;
305
+ rootScene.add(helperGroup);
306
+ }
307
+ const geometry = new BoxGeometry(1, 1, 1);
308
+ const material = new MeshBasicNodeMaterial({
309
+ transparent: true,
310
+ opacity: 0
311
+ });
312
+ const observerNode = nodeObject(createOcclusionObserverNode(store, uniform));
313
+ material.colorNode = observerNode;
314
+ material.needsUpdate = true;
315
+ const mesh = new Mesh(geometry, material);
316
+ mesh.name = "__r3fOcclusionObserver";
317
+ mesh.scale.setScalar(1e-4);
318
+ mesh.frustumCulled = false;
319
+ mesh.__r3fInternal = true;
320
+ helperGroup.add(mesh);
321
+ set((state2) => ({
322
+ internal: {
323
+ ...state2.internal,
324
+ helperGroup,
325
+ occlusionObserver: mesh,
326
+ occlusionEnabled: true
327
+ }
328
+ }));
329
+ occlusionSetupPromise = null;
330
+ }
331
+ function disableOcclusion(store) {
332
+ const { internal, set } = store.getState();
333
+ if (!internal.occlusionEnabled) return;
334
+ if (internal.occlusionObserver) {
335
+ internal.occlusionObserver.removeFromParent();
336
+ internal.occlusionObserver.geometry.dispose();
337
+ internal.occlusionObserver.material.dispose();
338
+ }
339
+ internal.occlusionCache.clear();
340
+ set((state) => ({
341
+ internal: {
342
+ ...state.internal,
343
+ occlusionObserver: null,
344
+ occlusionEnabled: false
345
+ }
346
+ }));
347
+ }
348
+ function cleanupHelperGroup(store) {
349
+ const { internal, set } = store.getState();
350
+ disableOcclusion(store);
351
+ if (internal.helperGroup) {
352
+ internal.helperGroup.removeFromParent();
353
+ set((state) => ({
354
+ internal: {
355
+ ...state.internal,
356
+ helperGroup: null
357
+ }
358
+ }));
359
+ }
360
+ }
361
+ function registerVisibility(store, object, handlers) {
362
+ const { internal } = store.getState();
363
+ const registry = internal.visibilityRegistry;
364
+ const entry = {
365
+ object,
366
+ handlers,
367
+ lastFramedState: null,
368
+ lastOccludedState: null,
369
+ lastVisibleState: null
370
+ };
371
+ registry.set(object.uuid, entry);
372
+ if (handlers.onOccluded || handlers.onVisible) {
373
+ object.occlusionTest = true;
374
+ if (!internal.occlusionEnabled) {
375
+ enableOcclusion(store);
376
+ }
377
+ }
378
+ }
379
+ function unregisterVisibility(store, object) {
380
+ const { internal } = store.getState();
381
+ internal.visibilityRegistry.delete(object.uuid);
382
+ internal.occlusionCache.delete(object);
383
+ }
384
+ function checkVisibility(state) {
385
+ const { internal, camera } = state;
386
+ const registry = internal.visibilityRegistry;
387
+ if (registry.size === 0) return;
388
+ updateFrustum(camera, tempFrustum);
389
+ for (const entry of registry.values()) {
390
+ const { object, handlers, lastFramedState, lastOccludedState, lastVisibleState } = entry;
391
+ let inFrustum = null;
392
+ const computeFrustum = () => {
393
+ if (inFrustum === null) {
394
+ if (object.geometry?.boundingSphere === null) {
395
+ object.geometry?.computeBoundingSphere();
396
+ }
397
+ inFrustum = tempFrustum.intersectsObject(object);
398
+ }
399
+ return inFrustum;
400
+ };
401
+ if (handlers.onFramed) {
402
+ const currentInFrustum = computeFrustum();
403
+ if (currentInFrustum !== lastFramedState) {
404
+ entry.lastFramedState = currentInFrustum;
405
+ handlers.onFramed(currentInFrustum);
406
+ }
407
+ }
408
+ let currentOcclusion = null;
409
+ if (handlers.onOccluded && internal.occlusionEnabled) {
410
+ currentOcclusion = internal.occlusionCache.get(object) ?? null;
411
+ if (currentOcclusion !== null && currentOcclusion !== lastOccludedState) {
412
+ entry.lastOccludedState = currentOcclusion;
413
+ handlers.onOccluded(currentOcclusion);
414
+ }
415
+ }
416
+ if (handlers.onVisible) {
417
+ const currentInFrustum = computeFrustum();
418
+ if (!handlers.onFramed && currentInFrustum !== lastFramedState) {
419
+ entry.lastFramedState = currentInFrustum;
420
+ }
421
+ let isOccluded = currentOcclusion;
422
+ if (isOccluded === null && internal.occlusionEnabled) {
423
+ isOccluded = internal.occlusionCache.get(object) ?? null;
424
+ }
425
+ if (isOccluded === null) isOccluded = false;
426
+ const isVisible = currentInFrustum && !isOccluded && object.visible;
427
+ if (isVisible !== lastVisibleState) {
428
+ entry.lastVisibleState = isVisible;
429
+ handlers.onVisible(isVisible);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ function hasVisibilityHandlers(handlers) {
435
+ if (!handlers) return false;
436
+ return !!(handlers.onFramed || handlers.onOccluded || handlers.onVisible);
437
+ }
438
+
230
439
  const RESERVED_PROPS = [
231
440
  "children",
232
441
  "key",
@@ -241,6 +450,7 @@ const RESERVED_PROPS = [
241
450
  "dispose"
242
451
  ];
243
452
  const EVENT_REGEX = /^on(Pointer|Drag|Drop|Click|DoubleClick|ContextMenu|Wheel)/;
453
+ const VISIBILITY_EVENT_REGEX = /^on(Framed|Occluded|Visible)$/;
244
454
  const INDEX_REGEX = /-\d+$/;
245
455
  const MEMOIZED_PROTOTYPES = /* @__PURE__ */ new Map();
246
456
  const colorMaps = ["map", "emissiveMap", "sheenColorMap", "specularColorMap", "envMap"];
@@ -335,6 +545,12 @@ function applyProps(object, props) {
335
545
  instance.eventCount = Object.keys(instance.handlers).length;
336
546
  continue;
337
547
  }
548
+ if (instance && VISIBILITY_EVENT_REGEX.test(prop)) {
549
+ if (typeof value === "function") instance.handlers[prop] = value;
550
+ else delete instance.handlers[prop];
551
+ instance.eventCount = Object.keys(instance.handlers).length;
552
+ continue;
553
+ }
338
554
  if (value === void 0) continue;
339
555
  let { root, key, target } = resolve(object, prop);
340
556
  if (target === void 0 && (typeof root !== "object" || root === null)) {
@@ -371,6 +587,17 @@ function applyProps(object, props) {
371
587
  if (instance.eventCount && object2.raycast !== null) {
372
588
  rootState.internal.interaction.push(object2);
373
589
  }
590
+ const root = findInitialRoot(instance);
591
+ const visibilityHandlers = {
592
+ onFramed: instance.handlers.onFramed,
593
+ onOccluded: instance.handlers.onOccluded,
594
+ onVisible: instance.handlers.onVisible
595
+ };
596
+ if (hasVisibilityHandlers(visibilityHandlers)) {
597
+ registerVisibility(root, object2, visibilityHandlers);
598
+ } else {
599
+ unregisterVisibility(root, object2);
600
+ }
374
601
  }
375
602
  if (instance && instance.props.attach === void 0) {
376
603
  if (instance.object.isBufferGeometry) instance.props.attach = "geometry";
@@ -405,6 +632,7 @@ function removeInteractivity(store, object) {
405
632
  internal.capturedMap.forEach((captures, pointerId) => {
406
633
  releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
407
634
  });
635
+ unregisterVisibility(store, object);
408
636
  }
409
637
  function createEvents(store) {
410
638
  function calculateDistance(event) {
@@ -787,8 +1015,21 @@ function formatLocation(url, line) {
787
1015
  const file = clean.split("/").pop() ?? clean;
788
1016
  return `${file}:${line}`;
789
1017
  }
1018
+ function notifyAlpha({ message, link }) {
1019
+ if (typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) && process.env.R3F_SHOW_ALPHA_WARNINGS !== "true") {
1020
+ return;
1021
+ }
1022
+ if (shownNotices.has(message)) return;
1023
+ shownNotices.add(message);
1024
+ const boxStyle = "background: #6366f1; color: #ffffff; padding: 6px 10px; border-radius: 4px; font-weight: 500;";
1025
+ console.log(`%c\u{1F52C} ${message}`, boxStyle);
1026
+ {
1027
+ console.log(`%cMore info: ${link}`, "color: #6366f1; font-weight: normal;");
1028
+ }
1029
+ }
790
1030
 
791
- const context = /* @__PURE__ */ React.createContext(null);
1031
+ const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
1032
+ const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
792
1033
  const createStore = (invalidate, advance) => {
793
1034
  const rootStore = createWithEqualityFn((set, get) => {
794
1035
  const position = new Vector3();
@@ -819,6 +1060,8 @@ const createStore = (invalidate, advance) => {
819
1060
  gl: null,
820
1061
  renderer: null,
821
1062
  camera: null,
1063
+ frustum: new Frustum(),
1064
+ autoUpdateFrustum: true,
822
1065
  raycaster: null,
823
1066
  events: { priority: 1, enabled: true, connected: false },
824
1067
  scene: null,
@@ -900,6 +1143,13 @@ const createStore = (invalidate, advance) => {
900
1143
  initialHits: [],
901
1144
  capturedMap: /* @__PURE__ */ new Map(),
902
1145
  lastEvent: React.createRef(),
1146
+ // Visibility tracking (onFramed, onOccluded, onVisible)
1147
+ visibilityRegistry: /* @__PURE__ */ new Map(),
1148
+ // Occlusion system (WebGPU only)
1149
+ occlusionEnabled: false,
1150
+ occlusionObserver: null,
1151
+ occlusionCache: /* @__PURE__ */ new Map(),
1152
+ helperGroup: null,
903
1153
  // Updates
904
1154
  active: false,
905
1155
  frames: 0,
@@ -985,7 +1235,15 @@ const createStore = (invalidate, advance) => {
985
1235
  }
986
1236
  if (camera !== oldCamera) {
987
1237
  oldCamera = camera;
1238
+ const { rootScene } = rootStore.getState();
1239
+ if (camera && rootScene && !camera.parent) {
1240
+ rootScene.add(camera);
1241
+ }
988
1242
  set((state2) => ({ viewport: { ...state2.viewport, ...state2.viewport.getCurrentViewport(camera) } }));
1243
+ const currentState = rootStore.getState();
1244
+ if (currentState.autoUpdateFrustum && camera) {
1245
+ updateFrustum(camera, currentState.frustum);
1246
+ }
989
1247
  }
990
1248
  });
991
1249
  rootStore.subscribe((state2) => invalidate(state2));
@@ -1349,6 +1607,10 @@ const _Scheduler = class _Scheduler {
1349
1607
  __publicField(this, "jobStateListeners", /* @__PURE__ */ new Map());
1350
1608
  __publicField(this, "pendingFrames", 0);
1351
1609
  __publicField(this, "_frameloop", "always");
1610
+ //* Independent Mode & Error Handling State ================================
1611
+ __publicField(this, "_independent", false);
1612
+ __publicField(this, "errorHandler", null);
1613
+ __publicField(this, "rootReadyCallbacks", /* @__PURE__ */ new Set());
1352
1614
  //* Core Loop Execution Methods ================================
1353
1615
  /**
1354
1616
  * Main RAF loop callback.
@@ -1371,6 +1633,12 @@ const _Scheduler = class _Scheduler {
1371
1633
  });
1372
1634
  this.phaseGraph = new PhaseGraph();
1373
1635
  }
1636
+ static get instance() {
1637
+ return globalThis[_Scheduler.INSTANCE_KEY] ?? null;
1638
+ }
1639
+ static set instance(value) {
1640
+ globalThis[_Scheduler.INSTANCE_KEY] = value;
1641
+ }
1374
1642
  /**
1375
1643
  * Get the global scheduler instance (creates if doesn't exist).
1376
1644
  * Uses HMR data to preserve instance across hot reloads.
@@ -1419,29 +1687,43 @@ const _Scheduler = class _Scheduler {
1419
1687
  get isRunning() {
1420
1688
  return this.loopState.running;
1421
1689
  }
1690
+ get isReady() {
1691
+ return this.roots.size > 0;
1692
+ }
1693
+ get independent() {
1694
+ return this._independent;
1695
+ }
1696
+ set independent(value) {
1697
+ this._independent = value;
1698
+ if (value) this.ensureDefaultRoot();
1699
+ }
1422
1700
  //* Root Management Methods ================================
1423
1701
  /**
1424
1702
  * Register a root (Canvas) with the scheduler.
1425
1703
  * The first root to register starts the RAF loop (if frameloop='always').
1426
1704
  * @param {string} id - Unique identifier for this root
1427
- * @param {() => RootState} getState - Function to get the root's current state
1705
+ * @param {RootOptions} [options] - Optional configuration with getState and onError callbacks
1428
1706
  * @returns {() => void} Unsubscribe function to remove this root
1429
1707
  */
1430
- registerRoot(id, getState) {
1708
+ registerRoot(id, options = {}) {
1431
1709
  if (this.roots.has(id)) {
1432
1710
  console.warn(`[Scheduler] Root "${id}" already registered`);
1433
1711
  return () => this.unregisterRoot(id);
1434
1712
  }
1435
1713
  const entry = {
1436
1714
  id,
1437
- getState,
1715
+ getState: options.getState ?? (() => ({})),
1438
1716
  jobs: /* @__PURE__ */ new Map(),
1439
1717
  sortedJobs: [],
1440
1718
  needsRebuild: false
1441
1719
  };
1720
+ if (options.onError) {
1721
+ this.errorHandler = options.onError;
1722
+ }
1442
1723
  this.roots.set(id, entry);
1443
- if (this.roots.size === 1 && this._frameloop === "always") {
1444
- this.start();
1724
+ if (this.roots.size === 1) {
1725
+ this.notifyRootReady();
1726
+ if (this._frameloop === "always") this.start();
1445
1727
  }
1446
1728
  return () => this.unregisterRoot(id);
1447
1729
  }
@@ -1461,8 +1743,61 @@ const _Scheduler = class _Scheduler {
1461
1743
  this.roots.delete(id);
1462
1744
  if (this.roots.size === 0) {
1463
1745
  this.stop();
1746
+ this.errorHandler = null;
1464
1747
  }
1465
1748
  }
1749
+ /**
1750
+ * Subscribe to be notified when a root becomes available.
1751
+ * Fires immediately if a root already exists.
1752
+ * @param {() => void} callback - Function called when first root registers
1753
+ * @returns {() => void} Unsubscribe function
1754
+ */
1755
+ onRootReady(callback) {
1756
+ if (this.roots.size > 0) {
1757
+ callback();
1758
+ return () => {
1759
+ };
1760
+ }
1761
+ this.rootReadyCallbacks.add(callback);
1762
+ return () => this.rootReadyCallbacks.delete(callback);
1763
+ }
1764
+ /**
1765
+ * Notify all registered root-ready callbacks.
1766
+ * Called when the first root registers.
1767
+ * @returns {void}
1768
+ * @private
1769
+ */
1770
+ notifyRootReady() {
1771
+ for (const cb of this.rootReadyCallbacks) {
1772
+ try {
1773
+ cb();
1774
+ } catch (error) {
1775
+ console.error("[Scheduler] Error in root-ready callback:", error);
1776
+ }
1777
+ }
1778
+ this.rootReadyCallbacks.clear();
1779
+ }
1780
+ /**
1781
+ * Ensure a default root exists for independent mode.
1782
+ * Creates a minimal root with no state provider.
1783
+ * @returns {void}
1784
+ * @private
1785
+ */
1786
+ ensureDefaultRoot() {
1787
+ if (!this.roots.has("__default__")) {
1788
+ this.registerRoot("__default__");
1789
+ }
1790
+ }
1791
+ /**
1792
+ * Trigger error handling for job errors.
1793
+ * Uses the bound error handler if available, otherwise logs to console.
1794
+ * @param {Error} error - The error to handle
1795
+ * @returns {void}
1796
+ */
1797
+ triggerError(error) {
1798
+ if (this.errorHandler) this.errorHandler(error);
1799
+ else console.error("[Scheduler]", error);
1800
+ }
1466
1801
  //* Phase Management Methods ================================
1467
1802
  /**
1468
1803
  * Add a named phase to the scheduler's execution order.
@@ -1821,9 +2156,9 @@ const _Scheduler = class _Scheduler {
1821
2156
  const deltaMs = this.loopState.lastTime !== null ? now - this.loopState.lastTime : 0;
1822
2157
  const delta = deltaMs / 1e3;
1823
2158
  const elapsed = now - this.loopState.createdAt;
1824
- const rootState = root.getState();
2159
+ const providedState = root.getState?.() ?? {};
1825
2160
  const frameState = {
1826
- ...rootState,
2161
+ ...providedState,
1827
2162
  time: now,
1828
2163
  delta,
1829
2164
  elapsed,
@@ -1833,6 +2168,7 @@ const _Scheduler = class _Scheduler {
1833
2168
  job.callback(frameState, delta);
1834
2169
  } catch (error) {
1835
2170
  console.error(`[Scheduler] Error in job "${job.id}":`, error);
2171
+ this.triggerError(error instanceof Error ? error : new Error(String(error)));
1836
2172
  }
1837
2173
  }
1838
2174
  /**
@@ -1874,7 +2210,7 @@ const _Scheduler = class _Scheduler {
1874
2210
  /**
1875
2211
  * Execute all jobs for a single root in sorted order.
1876
2212
  * Rebuilds sorted job list if needed, then dispatches each job.
1877
- * Errors are caught and propagated to the root's error boundary.
2213
+ * Errors are caught and propagated via triggerError.
1878
2214
  * @param {RootEntry} root - The root entry to tick
1879
2215
  * @param {number} timestamp - RAF timestamp in milliseconds
1880
2216
  * @param {number} delta - Time since last frame in seconds
@@ -1886,10 +2222,9 @@ const _Scheduler = class _Scheduler {
1886
2222
  root.sortedJobs = rebuildSortedJobs(root.jobs, this.phaseGraph);
1887
2223
  root.needsRebuild = false;
1888
2224
  }
1889
- const rootState = root.getState();
1890
- if (!rootState) return;
2225
+ const providedState = root.getState?.() ?? {};
1891
2226
  const frameState = {
1892
- ...rootState,
2227
+ ...providedState,
1893
2228
  time: timestamp,
1894
2229
  delta,
1895
2230
  elapsed: this.loopState.elapsedTime / 1e3,
@@ -1902,7 +2237,7 @@ const _Scheduler = class _Scheduler {
1902
2237
  job.callback(frameState, delta);
1903
2238
  } catch (error) {
1904
2239
  console.error(`[Scheduler] Error in job "${job.id}":`, error);
1905
- rootState.setError(error instanceof Error ? error : new Error(String(error)));
2240
+ this.triggerError(error instanceof Error ? error : new Error(String(error)));
1906
2241
  }
1907
2242
  }
1908
2243
  }
@@ -1987,8 +2322,11 @@ const _Scheduler = class _Scheduler {
1987
2322
  return /* @__PURE__ */ new Set([value]);
1988
2323
  }
1989
2324
  };
1990
- //* Static State & Methods (Singlton Usage) ================================
1991
- __publicField(_Scheduler, "instance", null);
2325
+ //* Static State & Methods (Singleton Usage) ================================
2326
+ //* Cross-Bundle Singleton Key ==============================
2327
+ // Use Symbol.for() to ensure scheduler is shared across bundle boundaries
2328
+ // This prevents issues when mixing imports from @react-three/fiber and @react-three/fiber/webgpu
2329
+ __publicField(_Scheduler, "INSTANCE_KEY", Symbol.for("@react-three/fiber.scheduler"));
1992
2330
  let Scheduler = _Scheduler;
1993
2331
  const getScheduler = () => Scheduler.get();
1994
2332
  if (hmrData) {
@@ -1996,11 +2334,9 @@ if (hmrData) {
1996
2334
  }
1997
2335
 
1998
2336
  function useFrame(callback, priorityOrOptions) {
1999
- const store = useStore();
2000
- const getRootId = React.useCallback(() => {
2001
- const state = store.getState();
2002
- return state.internal.rootId;
2003
- }, [store]);
2337
+ const store = React.useContext(context);
2338
+ const isInsideCanvas = store !== null;
2339
+ const scheduler = getScheduler();
2004
2340
  const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
2005
2341
  id: priorityOrOptions.id,
2006
2342
  phase: priorityOrOptions.phase,
@@ -2020,55 +2356,71 @@ function useFrame(callback, priorityOrOptions) {
2020
2356
  const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
2021
2357
  useIsomorphicLayoutEffect(() => {
2022
2358
  if (!callback) return;
2023
- const scheduler = getScheduler();
2024
- const rootId = getRootId();
2025
- const state = store.getState();
2026
- if (isLegacyPriority) {
2027
- state.internal.priority++;
2028
- let parentRoot = state.previousRoot;
2029
- while (parentRoot) {
2030
- const parentState = parentRoot.getState();
2031
- if (parentState?.internal) parentState.internal.priority++;
2032
- parentRoot = parentState?.previousRoot;
2033
- }
2034
- notifyDepreciated({
2035
- heading: "useFrame with numeric priority is deprecated",
2036
- 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 })',
2037
- link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2038
- });
2039
- }
2040
- const wrappedCallback = (frameState, delta) => {
2041
- const localState = store.getState();
2042
- const mergedState = {
2043
- ...localState,
2044
- time: frameState.time,
2045
- delta: frameState.delta,
2046
- elapsed: frameState.elapsed,
2047
- frame: frameState.frame
2048
- };
2049
- callbackRef.current?.(mergedState, delta);
2050
- };
2051
- const unregister = scheduler.register(wrappedCallback, {
2052
- id,
2053
- rootId,
2054
- ...options
2055
- });
2056
- return () => {
2057
- unregister();
2359
+ if (isInsideCanvas) {
2360
+ const state = store.getState();
2361
+ const rootId = state.internal.rootId;
2058
2362
  if (isLegacyPriority) {
2059
- const currentState = store.getState();
2060
- if (currentState.internal) {
2061
- currentState.internal.priority--;
2062
- let parentRoot = currentState.previousRoot;
2063
- while (parentRoot) {
2064
- const parentState = parentRoot.getState();
2065
- if (parentState?.internal) parentState.internal.priority--;
2066
- parentRoot = parentState?.previousRoot;
2363
+ state.internal.priority++;
2364
+ let parentRoot = state.previousRoot;
2365
+ while (parentRoot) {
2366
+ const parentState = parentRoot.getState();
2367
+ if (parentState?.internal) parentState.internal.priority++;
2368
+ parentRoot = parentState?.previousRoot;
2369
+ }
2370
+ notifyDepreciated({
2371
+ heading: "useFrame with numeric priority is deprecated",
2372
+ 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 })',
2373
+ link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
2374
+ });
2375
+ }
2376
+ const wrappedCallback = (frameState, delta) => {
2377
+ const localState = store.getState();
2378
+ const mergedState = {
2379
+ ...localState,
2380
+ time: frameState.time,
2381
+ delta: frameState.delta,
2382
+ elapsed: frameState.elapsed,
2383
+ frame: frameState.frame
2384
+ };
2385
+ callbackRef.current?.(mergedState, delta);
2386
+ };
2387
+ const unregister = scheduler.register(wrappedCallback, {
2388
+ id,
2389
+ rootId,
2390
+ ...options
2391
+ });
2392
+ return () => {
2393
+ unregister();
2394
+ if (isLegacyPriority) {
2395
+ const currentState = store.getState();
2396
+ if (currentState.internal) {
2397
+ currentState.internal.priority--;
2398
+ let parentRoot = currentState.previousRoot;
2399
+ while (parentRoot) {
2400
+ const parentState = parentRoot.getState();
2401
+ if (parentState?.internal) parentState.internal.priority--;
2402
+ parentRoot = parentState?.previousRoot;
2403
+ }
2067
2404
  }
2068
2405
  }
2406
+ };
2407
+ } else {
2408
+ const registerOutside = () => {
2409
+ return scheduler.register((state, delta) => callbackRef.current?.(state, delta), { id, ...options });
2410
+ };
2411
+ if (scheduler.independent || scheduler.isReady) {
2412
+ return registerOutside();
2069
2413
  }
2070
- };
2071
- }, [store, id, optionsKey, isLegacyPriority]);
2414
+ let unregisterJob = null;
2415
+ const unsubReady = scheduler.onRootReady(() => {
2416
+ unregisterJob = registerOutside();
2417
+ });
2418
+ return () => {
2419
+ unsubReady();
2420
+ unregisterJob?.();
2421
+ };
2422
+ }
2423
+ }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
2072
2424
  const isPaused = React.useSyncExternalStore(
2073
2425
  // Subscribe function
2074
2426
  React.useCallback(
@@ -2083,7 +2435,7 @@ function useFrame(callback, priorityOrOptions) {
2083
2435
  React.useCallback(() => false, [])
2084
2436
  );
2085
2437
  const controls = React.useMemo(() => {
2086
- const scheduler = getScheduler();
2438
+ const scheduler2 = getScheduler();
2087
2439
  return {
2088
2440
  /** The job's unique ID */
2089
2441
  id,
@@ -2091,7 +2443,7 @@ function useFrame(callback, priorityOrOptions) {
2091
2443
  * Access to the global scheduler for frame loop control.
2092
2444
  * Use for controlling the entire frame loop, adding phases, etc.
2093
2445
  */
2094
- scheduler,
2446
+ scheduler: scheduler2,
2095
2447
  /**
2096
2448
  * Manually step this job only.
2097
2449
  * Bypasses FPS limiting - always runs.
@@ -2339,6 +2691,16 @@ function useTextures() {
2339
2691
  }, [store]);
2340
2692
  }
2341
2693
 
2694
+ function useRenderTarget(width, height, options) {
2695
+ const isLegacy = useThree((s) => s.isLegacy);
2696
+ const size = useThree((s) => s.size);
2697
+ return useMemo(() => {
2698
+ const w = width ?? size.width;
2699
+ const h = height ?? size.height;
2700
+ return new WebGLRenderTarget(w, h, options);
2701
+ }, [width, height, size.width, size.height, options, isLegacy]);
2702
+ }
2703
+
2342
2704
  function useStore() {
2343
2705
  const store = useContext(context);
2344
2706
  if (!store) throw new Error("R3F: Hooks can only be used within the Canvas component!");
@@ -2390,7 +2752,7 @@ function advance(timestamp, runGlobalEffects = true, state, frame) {
2390
2752
  getScheduler().step(timestamp);
2391
2753
  }
2392
2754
 
2393
- const version = "10.0.0-alpha.0";
2755
+ const version = "10.0.0-alpha.1";
2394
2756
  const packageData = {
2395
2757
  version: version};
2396
2758
 
@@ -13691,7 +14053,8 @@ function createReconciler(config) {
13691
14053
  return reconciler2;
13692
14054
  }
13693
14055
  const NoEventPriority = 0;
13694
- const catalogue = {};
14056
+ const R3F_CATALOGUE = Symbol.for("@react-three/fiber.catalogue");
14057
+ const catalogue = globalThis[R3F_CATALOGUE] ?? (globalThis[R3F_CATALOGUE] = {});
13695
14058
  const PREFIX_REGEX = /^three(?=[A-Z])/;
13696
14059
  const toPascalCase = (type) => `${type[0].toUpperCase()}${type.slice(1)}`;
13697
14060
  let i = 0;
@@ -14189,7 +14552,9 @@ function createRoot(canvas) {
14189
14552
  camera: cameraOptions,
14190
14553
  onPointerMissed,
14191
14554
  onDragOverMissed,
14192
- onDropMissed
14555
+ onDropMissed,
14556
+ autoUpdateFrustum = true,
14557
+ occlusion = false
14193
14558
  } = props;
14194
14559
  let state = store.getState();
14195
14560
  const defaultGLProps = {
@@ -14255,6 +14620,8 @@ function createRoot(canvas) {
14255
14620
  rootScene: scene,
14256
14621
  internal: { ...prev.internal, container: scene }
14257
14622
  }));
14623
+ const camera = state.camera;
14624
+ if (camera && !camera.parent) scene.add(camera);
14258
14625
  }
14259
14626
  if (events && !state.events.handlers) {
14260
14627
  state.set({ events: events(store) });
@@ -14282,6 +14649,10 @@ function createRoot(canvas) {
14282
14649
  if (!state.onPointerMissed) state.set({ onPointerMissed });
14283
14650
  if (!state.onDragOverMissed) state.set({ onDragOverMissed });
14284
14651
  if (!state.onDropMissed) state.set({ onDropMissed });
14652
+ if (state.autoUpdateFrustum !== autoUpdateFrustum) state.set({ autoUpdateFrustum });
14653
+ if (occlusion && !state.internal.occlusionEnabled) {
14654
+ enableOcclusion(store);
14655
+ }
14285
14656
  if (performance && !is.equ(performance, lastConfiguredProps.performance, shallowLoose)) {
14286
14657
  state.set((state2) => ({ performance: { ...state2.performance, ...performance } }));
14287
14658
  lastConfiguredProps.performance = performance;
@@ -14374,7 +14745,37 @@ function createRoot(canvas) {
14374
14745
  const rootId = state.internal.rootId;
14375
14746
  if (!rootId) {
14376
14747
  const newRootId = scheduler.generateRootId();
14377
- const unregisterRoot = scheduler.registerRoot(newRootId, () => store.getState());
14748
+ const unregisterRoot = scheduler.registerRoot(newRootId, {
14749
+ getState: () => store.getState(),
14750
+ onError: (err) => store.getState().setError(err)
14751
+ });
14752
+ const unregisterFrustum = scheduler.register(
14753
+ () => {
14754
+ const state2 = store.getState();
14755
+ if (state2.autoUpdateFrustum && state2.camera) {
14756
+ updateFrustum(state2.camera, state2.frustum);
14757
+ }
14758
+ },
14759
+ {
14760
+ id: `${newRootId}_frustum`,
14761
+ rootId: newRootId,
14762
+ phase: "preRender",
14763
+ system: true
14764
+ }
14765
+ );
14766
+ const unregisterVisibility = scheduler.register(
14767
+ () => {
14768
+ const state2 = store.getState();
14769
+ checkVisibility(state2);
14770
+ },
14771
+ {
14772
+ id: `${newRootId}_visibility`,
14773
+ rootId: newRootId,
14774
+ phase: "preRender",
14775
+ system: true,
14776
+ after: `${newRootId}_frustum`
14777
+ }
14778
+ );
14378
14779
  const unregisterRender = scheduler.register(
14379
14780
  () => {
14380
14781
  const state2 = store.getState();
@@ -14402,6 +14803,8 @@ function createRoot(canvas) {
14402
14803
  rootId: newRootId,
14403
14804
  unregisterRoot: () => {
14404
14805
  unregisterRoot();
14806
+ unregisterFrustum();
14807
+ unregisterVisibility();
14405
14808
  unregisterRender();
14406
14809
  },
14407
14810
  scheduler
@@ -14459,6 +14862,7 @@ function unmountComponentAtNode(canvas, callback) {
14459
14862
  const unregisterRoot = state.internal.unregisterRoot;
14460
14863
  if (unregisterRoot) unregisterRoot();
14461
14864
  state.events.disconnect?.();
14865
+ cleanupHelperGroup(root.store);
14462
14866
  renderer?.renderLists?.dispose?.();
14463
14867
  renderer?.forceContextLoss?.();
14464
14868
  if (renderer?.xr) state.xr.disconnect();
@@ -14638,7 +15042,22 @@ function CanvasImpl({
14638
15042
  effectActiveRef.current = true;
14639
15043
  const canvas = canvasRef.current;
14640
15044
  if (containerRect.width > 0 && containerRect.height > 0 && canvas) {
14641
- if (!root.current) root.current = createRoot(canvas);
15045
+ if (!root.current) {
15046
+ root.current = createRoot(canvas);
15047
+ notifyAlpha({
15048
+ message: "React Three Fiber v10 is in ALPHA - expect breaking changes",
15049
+ link: "https://github.com/pmndrs/react-three-fiber/discussions"
15050
+ });
15051
+ const rootEntry = _roots.get(canvas);
15052
+ if (rootEntry?.store) {
15053
+ if (unsubscribeErrorRef.current) unsubscribeErrorRef.current();
15054
+ unsubscribeErrorRef.current = rootEntry.store.subscribe((state) => {
15055
+ if (state.error && effectActiveRef.current) {
15056
+ setError(state.error);
15057
+ }
15058
+ });
15059
+ }
15060
+ }
14642
15061
  async function run() {
14643
15062
  if (!effectActiveRef.current || !root.current) return;
14644
15063
  await root.current.configure({
@@ -14679,15 +15098,9 @@ function CanvasImpl({
14679
15098
  }
14680
15099
  });
14681
15100
  if (!effectActiveRef.current || !root.current) return;
14682
- const store = root.current.render(
15101
+ root.current.render(
14683
15102
  /* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(ErrorBoundary, { set: setError, children: /* @__PURE__ */ jsx(React.Suspense, { fallback: /* @__PURE__ */ jsx(Block, { set: setBlock }), children: children ?? null }) }) })
14684
15103
  );
14685
- if (unsubscribeErrorRef.current) unsubscribeErrorRef.current();
14686
- unsubscribeErrorRef.current = store.subscribe((state) => {
14687
- if (state.error && effectActiveRef.current) {
14688
- setError(state.error);
14689
- }
14690
- });
14691
15104
  }
14692
15105
  run();
14693
15106
  }
@@ -14732,4 +15145,4 @@ function Canvas(props) {
14732
15145
 
14733
15146
  extend(THREE);
14734
15147
 
14735
- export { Block, Canvas, ErrorBoundary, IsObject, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, getInstanceProps, getRootState, getScheduler, getUuidPrefix, hasConstructor, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isObject3D, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, prepare, reconciler, removeInteractivity, resolve, unmountComponentAtNode, updateCamera, useBridge, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useStore, useTexture, useTextures, useThree };
15148
+ export { Block, Canvas, ErrorBoundary, IsObject, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, getInstanceProps, getRootState, getScheduler, getUuidPrefix, hasConstructor, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isObject3D, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, prepare, reconciler, removeInteractivity, resolve, unmountComponentAtNode, updateCamera, updateFrustum, useBridge, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree };