@react-three/fiber 10.0.0-canary.6eb9584 → 10.0.0-canary.7a50545

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -905,7 +905,7 @@ const createStore = (invalidate, advance) => {
905
905
  size: newSize,
906
906
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
907
907
  }));
908
- getScheduler().invalidate();
908
+ get().invalidate();
909
909
  }
910
910
  }
911
911
  return;
@@ -920,15 +920,16 @@ const createStore = (invalidate, advance) => {
920
920
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
921
921
  _sizeImperative: true
922
922
  }));
923
- getScheduler().invalidate();
923
+ get().invalidate();
924
924
  },
925
925
  setDpr: (dpr) => set((state2) => {
926
926
  const resolved = calculateDpr(dpr);
927
927
  return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
928
928
  }),
929
929
  setFrameloop: (frameloop = "always") => {
930
+ const rootId = get().internal.rootId;
931
+ if (rootId) getScheduler().setRootFrameloop(rootId, frameloop);
930
932
  set(() => ({ frameloop }));
931
- getScheduler().frameloop = frameloop;
932
933
  },
933
934
  setError: (error) => set(() => ({ error })),
934
935
  error: null,
@@ -1044,17 +1045,10 @@ const createStore = (invalidate, advance) => {
1044
1045
  oldSize = size;
1045
1046
  oldDpr = viewport.dpr;
1046
1047
  updateCamera(camera, size);
1047
- if (internal.isSecondary && canvasTarget) {
1048
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1049
- canvasTarget.setSize(size.width, size.height, false);
1050
- } else {
1051
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1052
- actualRenderer.setSize(size.width, size.height, false);
1053
- if (canvasTarget) {
1054
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1055
- canvasTarget.setSize(size.width, size.height, false);
1056
- }
1057
- }
1048
+ const resizeTarget = canvasTarget ?? actualRenderer;
1049
+ if (viewport.dpr > 0) resizeTarget.setPixelRatio(viewport.dpr);
1050
+ resizeTarget.setSize(size.width, size.height, false);
1051
+ internal.canvasTargetSizeDirty = true;
1058
1052
  }
1059
1053
  if (camera !== oldCamera) {
1060
1054
  oldCamera = camera;
@@ -1116,7 +1110,11 @@ function useLoader(loader, input, extensions, onProgress, cacheKey) {
1116
1110
  const results = cacheKeys.map(
1117
1111
  (key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
1118
1112
  );
1119
- return Array.isArray(input) ? results : results[0];
1113
+ const stableRef = useRef(null);
1114
+ const previous = stableRef.current;
1115
+ const stable = previous !== null && previous.length === results.length && previous.every((value, i) => value === results[i]) ? previous : results;
1116
+ stableRef.current = stable;
1117
+ return Array.isArray(input) ? stable : stable[0];
1120
1118
  }
1121
1119
  useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
1122
1120
  const inputs = Array.isArray(input) ? input : [input];
@@ -1236,6 +1234,13 @@ function useFrame(callback, priorityOrOptions) {
1236
1234
  * Use for controlling the entire frame loop, adding phases, etc.
1237
1235
  */
1238
1236
  scheduler: scheduler2,
1237
+ /**
1238
+ * The root that currently owns this job. Resolve on access because ambient
1239
+ * jobs can be adopted by a Canvas after the controls object is created.
1240
+ */
1241
+ get rootId() {
1242
+ return getScheduler().getJobRootId(id);
1243
+ },
1239
1244
  /**
1240
1245
  * Manually step this job only.
1241
1246
  * Bypasses FPS limiting - always runs.
@@ -1252,6 +1257,14 @@ function useFrame(callback, priorityOrOptions) {
1252
1257
  stepAll: (timestamp) => {
1253
1258
  getScheduler().step(timestamp);
1254
1259
  },
1260
+ /**
1261
+ * Request frames for this job's owning root without waking sibling roots.
1262
+ */
1263
+ invalidate: (frames, stackFrames) => {
1264
+ const scheduler3 = getScheduler();
1265
+ const rootId = scheduler3.getJobRootId(id);
1266
+ if (rootId) scheduler3.invalidateRoot(rootId, frames, stackFrames);
1267
+ },
1255
1268
  /**
1256
1269
  * Pause this job (set enabled=false).
1257
1270
  * Job remains registered but won't run.
@@ -1281,6 +1294,11 @@ function getUrls(input) {
1281
1294
  if (Array.isArray(input)) return input;
1282
1295
  return Object.values(input);
1283
1296
  }
1297
+ function inputSignature(input) {
1298
+ if (typeof input === "string") return input;
1299
+ if (Array.isArray(input)) return input.join("\0");
1300
+ return Object.keys(input).sort().map((key) => `${key}\0${input[key]}`).join("");
1301
+ }
1284
1302
  function allUrlsCached(urls, textureCache) {
1285
1303
  return urls.every((url) => textureCache.has(url));
1286
1304
  }
@@ -1306,18 +1324,19 @@ function useTexture(input, optionsOrOnLoad) {
1306
1324
  const onLoadRef = useRef(onLoad);
1307
1325
  onLoadRef.current = onLoad;
1308
1326
  const onLoadCalledForRef = useRef(null);
1309
- const urls = useMemo(() => getUrls(input), [input]);
1327
+ const inputKey = useMemo(() => inputSignature(input), [input]);
1328
+ const stableInput = useMemo(() => input, [inputKey]);
1329
+ const urls = useMemo(() => getUrls(stableInput), [stableInput]);
1310
1330
  const cachedResult = useMemo(() => {
1311
1331
  if (!cache) return null;
1312
1332
  const textures = store.getState().textures;
1313
1333
  if (!allUrlsCached(urls, textures)) return null;
1314
- return buildFromCache(input, textures);
1315
- }, [cache, urls, input, store]);
1334
+ return buildFromCache(stableInput, textures);
1335
+ }, [cache, urls, stableInput, store]);
1316
1336
  const loadedTextures = useLoader(
1317
1337
  TextureLoader,
1318
- IsObject(input) ? Object.values(input) : input
1338
+ IsObject(stableInput) ? Object.values(stableInput) : stableInput
1319
1339
  );
1320
- const inputKey = urls.join("\0");
1321
1340
  useLayoutEffect(() => {
1322
1341
  if (cachedResult) return;
1323
1342
  if (onLoadCalledForRef.current === inputKey) return;
@@ -1344,28 +1363,28 @@ function useTexture(input, optionsOrOnLoad) {
1344
1363
  }, [renderer, loadedTextures, cachedResult]);
1345
1364
  const mappedTextures = useMemo(() => {
1346
1365
  if (cachedResult) return cachedResult;
1347
- if (IsObject(input)) {
1366
+ if (IsObject(stableInput)) {
1348
1367
  const keyed = {};
1349
1368
  const textureArray = loadedTextures;
1350
1369
  let i = 0;
1351
- for (const key in input) keyed[key] = textureArray[i++];
1370
+ for (const key in stableInput) keyed[key] = textureArray[i++];
1352
1371
  return keyed;
1353
1372
  } else {
1354
1373
  return loadedTextures;
1355
1374
  }
1356
- }, [input, loadedTextures, cachedResult]);
1375
+ }, [stableInput, loadedTextures, cachedResult]);
1357
1376
  useEffect(() => {
1358
1377
  if (!cache) return;
1359
1378
  const urlTextureMap = [];
1360
- if (typeof input === "string") {
1361
- urlTextureMap.push([input, mappedTextures]);
1362
- } else if (Array.isArray(input)) {
1379
+ if (typeof stableInput === "string") {
1380
+ urlTextureMap.push([stableInput, mappedTextures]);
1381
+ } else if (Array.isArray(stableInput)) {
1363
1382
  const textureArray = mappedTextures;
1364
- input.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1365
- } else if (IsObject(input)) {
1383
+ stableInput.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1384
+ } else if (IsObject(stableInput)) {
1366
1385
  const textureRecord = mappedTextures;
1367
- for (const key in input) {
1368
- const url = input[key];
1386
+ for (const key in stableInput) {
1387
+ const url = stableInput[key];
1369
1388
  urlTextureMap.push([url, textureRecord[key]]);
1370
1389
  }
1371
1390
  }
@@ -1394,7 +1413,7 @@ function useTexture(input, optionsOrOnLoad) {
1394
1413
  }
1395
1414
  return { _textureRefs: refs };
1396
1415
  });
1397
- }, [cache, input, mappedTextures, store]);
1416
+ }, [cache, stableInput, mappedTextures, store]);
1398
1417
  return mappedTextures;
1399
1418
  }
1400
1419
  useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
@@ -1665,10 +1684,16 @@ function addTail(callback) {
1665
1684
  return getScheduler().onIdle(callback);
1666
1685
  }
1667
1686
  function invalidate(state, frames = 1, stackFrames = false) {
1668
- getScheduler().invalidate(frames, stackFrames);
1687
+ const rootId = state?.internal?.rootId;
1688
+ const scheduler = getScheduler();
1689
+ if (rootId) scheduler.invalidateRoot(rootId, frames, stackFrames);
1690
+ else scheduler.invalidate(frames, stackFrames);
1669
1691
  }
1670
- function advance(timestamp) {
1671
- getScheduler().step(timestamp);
1692
+ function advance(timestamp, runGlobalEffects, state) {
1693
+ const rootId = state?.internal?.rootId;
1694
+ const scheduler = getScheduler();
1695
+ if (rootId) scheduler.stepRoot(rootId, timestamp);
1696
+ else scheduler.step(timestamp);
1672
1697
  }
1673
1698
 
1674
1699
  const version = "10.0.0-alpha.3";
@@ -14330,7 +14355,7 @@ function createRoot(canvas) {
14330
14355
  const handleXRFrame = (timestamp, _frame) => {
14331
14356
  const state2 = store.getState();
14332
14357
  if (state2.frameloop === "never") return;
14333
- advance(timestamp);
14358
+ advance(timestamp, true, state2);
14334
14359
  };
14335
14360
  const actualRenderer = state.internal.actualRenderer;
14336
14361
  const handleSessionChange = () => {
@@ -14338,7 +14363,7 @@ function createRoot(canvas) {
14338
14363
  const renderer2 = state2.internal.actualRenderer;
14339
14364
  actualRenderer.xr.enabled = actualRenderer.xr.isPresenting;
14340
14365
  renderer2.xr.setAnimationLoop(renderer2.xr.isPresenting ? handleXRFrame : null);
14341
- if (!renderer2.xr.isPresenting) invalidate();
14366
+ if (!renderer2.xr.isPresenting) invalidate(state2);
14342
14367
  };
14343
14368
  const xr = {
14344
14369
  connect() {
@@ -14420,7 +14445,11 @@ function createRoot(canvas) {
14420
14445
  const newRootId = canvasId || scheduler.generateRootId();
14421
14446
  const unregisterRoot = scheduler.registerRoot(newRootId, {
14422
14447
  getState: () => store.getState(),
14423
- onError: (err) => store.getState().setError(err)
14448
+ onError: (err) => store.getState().setError(err),
14449
+ frameloop: store.getState().frameloop,
14450
+ before: schedulerConfig?.before,
14451
+ after: schedulerConfig?.after,
14452
+ order: schedulerConfig?.order
14424
14453
  });
14425
14454
  const unregisterCanvasTarget = scheduler.register(
14426
14455
  () => {
@@ -14428,6 +14457,10 @@ function createRoot(canvas) {
14428
14457
  if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14429
14458
  const renderer2 = state2.internal.actualRenderer;
14430
14459
  renderer2.setCanvasTarget(state2.internal.canvasTarget);
14460
+ if (state2.internal.canvasTargetSizeDirty) {
14461
+ state2.internal.canvasTargetSizeDirty = false;
14462
+ renderer2.backend?.updateSize?.();
14463
+ }
14431
14464
  }
14432
14465
  },
14433
14466
  {
@@ -14496,8 +14529,7 @@ function createRoot(canvas) {
14496
14529
  phase: "render",
14497
14530
  system: true,
14498
14531
  // Internal flag: this is a system job, not user-controlled
14499
- // Apply scheduler config for render ordering and rate limiting
14500
- ...schedulerConfig?.after && { after: schedulerConfig.after },
14532
+ // FPS throttles only the default render job; Canvas ordering belongs to the root.
14501
14533
  ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14502
14534
  }
14503
14535
  );
@@ -14516,8 +14548,21 @@ function createRoot(canvas) {
14516
14548
  scheduler
14517
14549
  }
14518
14550
  }));
14551
+ } else {
14552
+ const constraintsChanged = !is.equ(schedulerConfig?.before, lastConfiguredProps.schedulerBefore, shallowLoose) || !is.equ(schedulerConfig?.after, lastConfiguredProps.schedulerAfter, shallowLoose);
14553
+ if (constraintsChanged) {
14554
+ scheduler.setRootConstraints(rootId, {
14555
+ before: schedulerConfig?.before,
14556
+ after: schedulerConfig?.after
14557
+ });
14558
+ }
14559
+ if (schedulerConfig?.order !== lastConfiguredProps.schedulerOrder) {
14560
+ scheduler.setRootOrder(rootId, schedulerConfig?.order ?? 0);
14561
+ }
14519
14562
  }
14520
- scheduler.frameloop = frameloop;
14563
+ lastConfiguredProps.schedulerBefore = schedulerConfig?.before;
14564
+ lastConfiguredProps.schedulerAfter = schedulerConfig?.after;
14565
+ lastConfiguredProps.schedulerOrder = schedulerConfig?.order;
14521
14566
  onCreated = onCreatedCallback;
14522
14567
  configured = true;
14523
14568
  resolve();
@@ -14559,14 +14604,19 @@ function unmountComponentAtNode(canvas, callback) {
14559
14604
  const fiber = root?.fiber;
14560
14605
  if (fiber) {
14561
14606
  const state = root?.store.getState();
14562
- if (state) state.internal.active = false;
14607
+ if (state) {
14608
+ state.internal.active = false;
14609
+ const unregisterRoot = state.internal.unregisterRoot;
14610
+ if (unregisterRoot) {
14611
+ unregisterRoot();
14612
+ state.internal.unregisterRoot = void 0;
14613
+ }
14614
+ }
14563
14615
  reconciler.updateContainer(null, fiber, null, () => {
14564
14616
  if (state) {
14565
14617
  setTimeout(() => {
14566
14618
  try {
14567
14619
  const renderer = state.internal.actualRenderer;
14568
- const unregisterRoot = state.internal.unregisterRoot;
14569
- if (unregisterRoot) unregisterRoot();
14570
14620
  const unregisterPrimary = state.internal.unregisterPrimary;
14571
14621
  if (unregisterPrimary) unregisterPrimary();
14572
14622
  const canvasTarget = state.internal.canvasTarget;
@@ -15160,6 +15210,7 @@ function CanvasImpl({
15160
15210
  const canvas = canvasRef.current;
15161
15211
  if (canvas) {
15162
15212
  return () => {
15213
+ if (canvas.isConnected) return;
15163
15214
  unmountComponentAtNode(canvas);
15164
15215
  root.current = null;
15165
15216
  };
package/dist/legacy.cjs CHANGED
@@ -929,7 +929,7 @@ const createStore = (invalidate, advance) => {
929
929
  size: newSize,
930
930
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
931
931
  }));
932
- scheduler.getScheduler().invalidate();
932
+ get().invalidate();
933
933
  }
934
934
  }
935
935
  return;
@@ -944,15 +944,16 @@ const createStore = (invalidate, advance) => {
944
944
  viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
945
945
  _sizeImperative: true
946
946
  }));
947
- scheduler.getScheduler().invalidate();
947
+ get().invalidate();
948
948
  },
949
949
  setDpr: (dpr) => set((state2) => {
950
950
  const resolved = calculateDpr(dpr);
951
951
  return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
952
952
  }),
953
953
  setFrameloop: (frameloop = "always") => {
954
+ const rootId = get().internal.rootId;
955
+ if (rootId) scheduler.getScheduler().setRootFrameloop(rootId, frameloop);
954
956
  set(() => ({ frameloop }));
955
- scheduler.getScheduler().frameloop = frameloop;
956
957
  },
957
958
  setError: (error) => set(() => ({ error })),
958
959
  error: null,
@@ -1068,17 +1069,10 @@ const createStore = (invalidate, advance) => {
1068
1069
  oldSize = size;
1069
1070
  oldDpr = viewport.dpr;
1070
1071
  updateCamera(camera, size);
1071
- if (internal.isSecondary && canvasTarget) {
1072
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1073
- canvasTarget.setSize(size.width, size.height, false);
1074
- } else {
1075
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1076
- actualRenderer.setSize(size.width, size.height, false);
1077
- if (canvasTarget) {
1078
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1079
- canvasTarget.setSize(size.width, size.height, false);
1080
- }
1081
- }
1072
+ const resizeTarget = canvasTarget ?? actualRenderer;
1073
+ if (viewport.dpr > 0) resizeTarget.setPixelRatio(viewport.dpr);
1074
+ resizeTarget.setSize(size.width, size.height, false);
1075
+ internal.canvasTargetSizeDirty = true;
1082
1076
  }
1083
1077
  if (camera !== oldCamera) {
1084
1078
  oldCamera = camera;
@@ -1140,7 +1134,11 @@ function useLoader(loader, input, extensions, onProgress, cacheKey) {
1140
1134
  const results = cacheKeys.map(
1141
1135
  (key, index) => suspendReact.suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
1142
1136
  );
1143
- return Array.isArray(input) ? results : results[0];
1137
+ const stableRef = React.useRef(null);
1138
+ const previous = stableRef.current;
1139
+ const stable = previous !== null && previous.length === results.length && previous.every((value, i) => value === results[i]) ? previous : results;
1140
+ stableRef.current = stable;
1141
+ return Array.isArray(input) ? stable : stable[0];
1144
1142
  }
1145
1143
  useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
1146
1144
  const inputs = Array.isArray(input) ? input : [input];
@@ -1260,6 +1258,13 @@ function useFrame(callback, priorityOrOptions) {
1260
1258
  * Use for controlling the entire frame loop, adding phases, etc.
1261
1259
  */
1262
1260
  scheduler: scheduler2,
1261
+ /**
1262
+ * The root that currently owns this job. Resolve on access because ambient
1263
+ * jobs can be adopted by a Canvas after the controls object is created.
1264
+ */
1265
+ get rootId() {
1266
+ return scheduler.getScheduler().getJobRootId(id);
1267
+ },
1263
1268
  /**
1264
1269
  * Manually step this job only.
1265
1270
  * Bypasses FPS limiting - always runs.
@@ -1276,6 +1281,14 @@ function useFrame(callback, priorityOrOptions) {
1276
1281
  stepAll: (timestamp) => {
1277
1282
  scheduler.getScheduler().step(timestamp);
1278
1283
  },
1284
+ /**
1285
+ * Request frames for this job's owning root without waking sibling roots.
1286
+ */
1287
+ invalidate: (frames, stackFrames) => {
1288
+ const scheduler3 = scheduler.getScheduler();
1289
+ const rootId = scheduler3.getJobRootId(id);
1290
+ if (rootId) scheduler3.invalidateRoot(rootId, frames, stackFrames);
1291
+ },
1279
1292
  /**
1280
1293
  * Pause this job (set enabled=false).
1281
1294
  * Job remains registered but won't run.
@@ -1305,6 +1318,11 @@ function getUrls(input) {
1305
1318
  if (Array.isArray(input)) return input;
1306
1319
  return Object.values(input);
1307
1320
  }
1321
+ function inputSignature(input) {
1322
+ if (typeof input === "string") return input;
1323
+ if (Array.isArray(input)) return input.join("\0");
1324
+ return Object.keys(input).sort().map((key) => `${key}\0${input[key]}`).join("");
1325
+ }
1308
1326
  function allUrlsCached(urls, textureCache) {
1309
1327
  return urls.every((url) => textureCache.has(url));
1310
1328
  }
@@ -1330,18 +1348,19 @@ function useTexture(input, optionsOrOnLoad) {
1330
1348
  const onLoadRef = React.useRef(onLoad);
1331
1349
  onLoadRef.current = onLoad;
1332
1350
  const onLoadCalledForRef = React.useRef(null);
1333
- const urls = React.useMemo(() => getUrls(input), [input]);
1351
+ const inputKey = React.useMemo(() => inputSignature(input), [input]);
1352
+ const stableInput = React.useMemo(() => input, [inputKey]);
1353
+ const urls = React.useMemo(() => getUrls(stableInput), [stableInput]);
1334
1354
  const cachedResult = React.useMemo(() => {
1335
1355
  if (!cache) return null;
1336
1356
  const textures = store.getState().textures;
1337
1357
  if (!allUrlsCached(urls, textures)) return null;
1338
- return buildFromCache(input, textures);
1339
- }, [cache, urls, input, store]);
1358
+ return buildFromCache(stableInput, textures);
1359
+ }, [cache, urls, stableInput, store]);
1340
1360
  const loadedTextures = useLoader(
1341
1361
  three.TextureLoader,
1342
- IsObject(input) ? Object.values(input) : input
1362
+ IsObject(stableInput) ? Object.values(stableInput) : stableInput
1343
1363
  );
1344
- const inputKey = urls.join("\0");
1345
1364
  React.useLayoutEffect(() => {
1346
1365
  if (cachedResult) return;
1347
1366
  if (onLoadCalledForRef.current === inputKey) return;
@@ -1368,28 +1387,28 @@ function useTexture(input, optionsOrOnLoad) {
1368
1387
  }, [renderer, loadedTextures, cachedResult]);
1369
1388
  const mappedTextures = React.useMemo(() => {
1370
1389
  if (cachedResult) return cachedResult;
1371
- if (IsObject(input)) {
1390
+ if (IsObject(stableInput)) {
1372
1391
  const keyed = {};
1373
1392
  const textureArray = loadedTextures;
1374
1393
  let i = 0;
1375
- for (const key in input) keyed[key] = textureArray[i++];
1394
+ for (const key in stableInput) keyed[key] = textureArray[i++];
1376
1395
  return keyed;
1377
1396
  } else {
1378
1397
  return loadedTextures;
1379
1398
  }
1380
- }, [input, loadedTextures, cachedResult]);
1399
+ }, [stableInput, loadedTextures, cachedResult]);
1381
1400
  React.useEffect(() => {
1382
1401
  if (!cache) return;
1383
1402
  const urlTextureMap = [];
1384
- if (typeof input === "string") {
1385
- urlTextureMap.push([input, mappedTextures]);
1386
- } else if (Array.isArray(input)) {
1403
+ if (typeof stableInput === "string") {
1404
+ urlTextureMap.push([stableInput, mappedTextures]);
1405
+ } else if (Array.isArray(stableInput)) {
1387
1406
  const textureArray = mappedTextures;
1388
- input.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1389
- } else if (IsObject(input)) {
1407
+ stableInput.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1408
+ } else if (IsObject(stableInput)) {
1390
1409
  const textureRecord = mappedTextures;
1391
- for (const key in input) {
1392
- const url = input[key];
1410
+ for (const key in stableInput) {
1411
+ const url = stableInput[key];
1393
1412
  urlTextureMap.push([url, textureRecord[key]]);
1394
1413
  }
1395
1414
  }
@@ -1418,7 +1437,7 @@ function useTexture(input, optionsOrOnLoad) {
1418
1437
  }
1419
1438
  return { _textureRefs: refs };
1420
1439
  });
1421
- }, [cache, input, mappedTextures, store]);
1440
+ }, [cache, stableInput, mappedTextures, store]);
1422
1441
  return mappedTextures;
1423
1442
  }
1424
1443
  useTexture.preload = (url) => useLoader.preload(three.TextureLoader, url);
@@ -1687,10 +1706,16 @@ function addTail(callback) {
1687
1706
  return scheduler.getScheduler().onIdle(callback);
1688
1707
  }
1689
1708
  function invalidate(state, frames = 1, stackFrames = false) {
1690
- scheduler.getScheduler().invalidate(frames, stackFrames);
1709
+ const rootId = state?.internal?.rootId;
1710
+ const scheduler$1 = scheduler.getScheduler();
1711
+ if (rootId) scheduler$1.invalidateRoot(rootId, frames, stackFrames);
1712
+ else scheduler$1.invalidate(frames, stackFrames);
1691
1713
  }
1692
- function advance(timestamp) {
1693
- scheduler.getScheduler().step(timestamp);
1714
+ function advance(timestamp, runGlobalEffects, state) {
1715
+ const rootId = state?.internal?.rootId;
1716
+ const scheduler$1 = scheduler.getScheduler();
1717
+ if (rootId) scheduler$1.stepRoot(rootId, timestamp);
1718
+ else scheduler$1.step(timestamp);
1694
1719
  }
1695
1720
 
1696
1721
  const version = "10.0.0-alpha.3";
@@ -14305,7 +14330,7 @@ function createRoot(canvas) {
14305
14330
  const handleXRFrame = (timestamp, _frame) => {
14306
14331
  const state2 = store.getState();
14307
14332
  if (state2.frameloop === "never") return;
14308
- advance(timestamp);
14333
+ advance(timestamp, true, state2);
14309
14334
  };
14310
14335
  const actualRenderer = state.internal.actualRenderer;
14311
14336
  const handleSessionChange = () => {
@@ -14313,7 +14338,7 @@ function createRoot(canvas) {
14313
14338
  const renderer2 = state2.internal.actualRenderer;
14314
14339
  actualRenderer.xr.enabled = actualRenderer.xr.isPresenting;
14315
14340
  renderer2.xr.setAnimationLoop(renderer2.xr.isPresenting ? handleXRFrame : null);
14316
- if (!renderer2.xr.isPresenting) invalidate();
14341
+ if (!renderer2.xr.isPresenting) invalidate(state2);
14317
14342
  };
14318
14343
  const xr = {
14319
14344
  connect() {
@@ -14395,7 +14420,11 @@ function createRoot(canvas) {
14395
14420
  const newRootId = canvasId || scheduler$1.generateRootId();
14396
14421
  const unregisterRoot = scheduler$1.registerRoot(newRootId, {
14397
14422
  getState: () => store.getState(),
14398
- onError: (err) => store.getState().setError(err)
14423
+ onError: (err) => store.getState().setError(err),
14424
+ frameloop: store.getState().frameloop,
14425
+ before: schedulerConfig?.before,
14426
+ after: schedulerConfig?.after,
14427
+ order: schedulerConfig?.order
14399
14428
  });
14400
14429
  const unregisterCanvasTarget = scheduler$1.register(
14401
14430
  () => {
@@ -14403,6 +14432,10 @@ function createRoot(canvas) {
14403
14432
  if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14404
14433
  const renderer2 = state2.internal.actualRenderer;
14405
14434
  renderer2.setCanvasTarget(state2.internal.canvasTarget);
14435
+ if (state2.internal.canvasTargetSizeDirty) {
14436
+ state2.internal.canvasTargetSizeDirty = false;
14437
+ renderer2.backend?.updateSize?.();
14438
+ }
14406
14439
  }
14407
14440
  },
14408
14441
  {
@@ -14471,8 +14504,7 @@ function createRoot(canvas) {
14471
14504
  phase: "render",
14472
14505
  system: true,
14473
14506
  // Internal flag: this is a system job, not user-controlled
14474
- // Apply scheduler config for render ordering and rate limiting
14475
- ...schedulerConfig?.after && { after: schedulerConfig.after },
14507
+ // FPS throttles only the default render job; Canvas ordering belongs to the root.
14476
14508
  ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14477
14509
  }
14478
14510
  );
@@ -14491,8 +14523,21 @@ function createRoot(canvas) {
14491
14523
  scheduler: scheduler$1
14492
14524
  }
14493
14525
  }));
14526
+ } else {
14527
+ const constraintsChanged = !is.equ(schedulerConfig?.before, lastConfiguredProps.schedulerBefore, shallowLoose) || !is.equ(schedulerConfig?.after, lastConfiguredProps.schedulerAfter, shallowLoose);
14528
+ if (constraintsChanged) {
14529
+ scheduler$1.setRootConstraints(rootId, {
14530
+ before: schedulerConfig?.before,
14531
+ after: schedulerConfig?.after
14532
+ });
14533
+ }
14534
+ if (schedulerConfig?.order !== lastConfiguredProps.schedulerOrder) {
14535
+ scheduler$1.setRootOrder(rootId, schedulerConfig?.order ?? 0);
14536
+ }
14494
14537
  }
14495
- scheduler$1.frameloop = frameloop;
14538
+ lastConfiguredProps.schedulerBefore = schedulerConfig?.before;
14539
+ lastConfiguredProps.schedulerAfter = schedulerConfig?.after;
14540
+ lastConfiguredProps.schedulerOrder = schedulerConfig?.order;
14496
14541
  onCreated = onCreatedCallback;
14497
14542
  configured = true;
14498
14543
  resolve();
@@ -14534,14 +14579,19 @@ function unmountComponentAtNode(canvas, callback) {
14534
14579
  const fiber = root?.fiber;
14535
14580
  if (fiber) {
14536
14581
  const state = root?.store.getState();
14537
- if (state) state.internal.active = false;
14582
+ if (state) {
14583
+ state.internal.active = false;
14584
+ const unregisterRoot = state.internal.unregisterRoot;
14585
+ if (unregisterRoot) {
14586
+ unregisterRoot();
14587
+ state.internal.unregisterRoot = void 0;
14588
+ }
14589
+ }
14538
14590
  reconciler.updateContainer(null, fiber, null, () => {
14539
14591
  if (state) {
14540
14592
  setTimeout(() => {
14541
14593
  try {
14542
14594
  const renderer = state.internal.actualRenderer;
14543
- const unregisterRoot = state.internal.unregisterRoot;
14544
- if (unregisterRoot) unregisterRoot();
14545
14595
  const unregisterPrimary = state.internal.unregisterPrimary;
14546
14596
  if (unregisterPrimary) unregisterPrimary();
14547
14597
  const canvasTarget = state.internal.canvasTarget;
@@ -15135,6 +15185,7 @@ function CanvasImpl({
15135
15185
  const canvas = canvasRef.current;
15136
15186
  if (canvas) {
15137
15187
  return () => {
15188
+ if (canvas.isConnected) return;
15138
15189
  unmountComponentAtNode(canvas);
15139
15190
  root.current = null;
15140
15191
  };