@react-three/tsl 10.0.0-canary.777b94d → 10.0.0-canary.88b1ca7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -73,6 +73,13 @@ function withStagedOverlay(store, kind, committed) {
73
73
  }
74
74
  return view;
75
75
  }
76
+ function readWithStaged(store, kind, committed, scope, isLeaf) {
77
+ if (scope === void 0) return withStagedOverlay(store, kind, committed);
78
+ const scopeData = committed[scope];
79
+ const current = scopeData && typeof scopeData === "object" && !isLeaf(scopeData) ? scopeData : {};
80
+ const staged = scope === ROOT_SCOPE ? void 0 : peekStaged(store, kind, scope);
81
+ return staged ? { ...current, ...staged } : current;
82
+ }
76
83
  function flushStagedScope(store, kind, scopeKey, isLeaf) {
77
84
  const registry = getKindRegistry(store, kind);
78
85
  const staged = registry.staged.get(scopeKey);
@@ -177,6 +184,10 @@ function extractTSLValue(value) {
177
184
  if (node.isConstNode) {
178
185
  return node.value;
179
186
  }
187
+ if (node.isVarNode && node.node) {
188
+ const inner = extractTSLValue(node.node);
189
+ return inner === node.node ? value : inner;
190
+ }
180
191
  if ("value" in node) {
181
192
  let extractedValue = node.value;
182
193
  if (typeof node.traverse === "function") {
@@ -267,6 +278,103 @@ function configureTSL(next) {
267
278
  for (const store of primaries) applyConfig(store, config);
268
279
  }
269
280
 
281
+ const owners = /* @__PURE__ */ new WeakMap();
282
+ const warned$1 = /* @__PURE__ */ new WeakSet();
283
+ const sweeps = /* @__PURE__ */ new WeakMap();
284
+ function createOwner(store, rebuild) {
285
+ return { store, pipeline: null, scenePass: null, passes: {}, mounted: false, releaseTimer: null, rebuild };
286
+ }
287
+ function disposeOwnerResources(owner) {
288
+ owner.scenePass?.node.dispose();
289
+ owner.pipeline?.dispose();
290
+ owner.scenePass = null;
291
+ owner.pipeline = null;
292
+ owner.passes = {};
293
+ }
294
+ function activeOwner(store) {
295
+ return owners.get(store)?.at(-1);
296
+ }
297
+ function renderThroughPipeline(store) {
298
+ return () => {
299
+ const pipeline = store.getState().renderPipeline;
300
+ if (pipeline?.render) pipeline.render();
301
+ };
302
+ }
303
+ function claimOwnership(owner) {
304
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
305
+ owner.releaseTimer = null;
306
+ owner.mounted = true;
307
+ let list = owners.get(owner.store);
308
+ if (!list) owners.set(owner.store, list = []);
309
+ if (list.includes(owner)) return;
310
+ if (list.some((other) => other.mounted) && !warned$1.has(owner.store)) {
311
+ warned$1.add(owner.store);
312
+ if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
313
+ console.warn(
314
+ "[useRenderPipeline] More than one mounted component configures this canvas's render pipeline. The most recently mounted one renders; the others take over again when it unmounts."
315
+ );
316
+ }
317
+ }
318
+ list.push(owner);
319
+ scheduleSweep(owner.store);
320
+ }
321
+ function scheduleSweep(store) {
322
+ if (sweeps.has(store)) return;
323
+ sweeps.set(
324
+ store,
325
+ setTimeout(() => {
326
+ sweeps.delete(store);
327
+ const active = activeOwner(store);
328
+ for (const owner of [...owners.get(store) ?? []]) {
329
+ if (owner !== active && !owner.mounted && owner.releaseTimer === null) releaseOwner(owner);
330
+ }
331
+ })
332
+ );
333
+ }
334
+ function publishOwner(owner) {
335
+ if (activeOwner(owner.store) !== owner || !owner.pipeline) return;
336
+ owner.store.setState({ renderPipeline: owner.pipeline, passes: owner.passes });
337
+ extension.setRenderOverride(owner.store, renderThroughPipeline(owner.store));
338
+ }
339
+ function scheduleRelease(owner) {
340
+ owner.mounted = false;
341
+ if (owner.releaseTimer !== null) return;
342
+ owner.releaseTimer = setTimeout(() => releaseOwner(owner));
343
+ }
344
+ function releaseOwner(owner) {
345
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
346
+ owner.releaseTimer = null;
347
+ const { store } = owner;
348
+ const list = owners.get(store);
349
+ const index = list?.indexOf(owner) ?? -1;
350
+ if (list && index !== -1) {
351
+ const wasActive = index === list.length - 1;
352
+ list.splice(index, 1);
353
+ if (wasActive) {
354
+ const next = activeOwner(store);
355
+ if (next) {
356
+ publishOwner(next);
357
+ } else {
358
+ store.setState({ renderPipeline: null, passes: {} });
359
+ extension.setRenderOverride(store, null);
360
+ }
361
+ }
362
+ }
363
+ disposeOwnerResources(owner);
364
+ }
365
+ function releaseAllOwners(store) {
366
+ clearTimeout(sweeps.get(store));
367
+ sweeps.delete(store);
368
+ const list = owners.get(store);
369
+ if (!list) return;
370
+ owners.delete(store);
371
+ for (const owner of list) {
372
+ if (owner.releaseTimer !== null) clearTimeout(owner.releaseTimer);
373
+ owner.releaseTimer = null;
374
+ disposeOwnerResources(owner);
375
+ }
376
+ }
377
+
270
378
  const SHARED_KEYS = ["uniforms", "nodes", "buffers", "gpuStorage", "_hmrVersion"];
271
379
  const following = /* @__PURE__ */ new WeakMap();
272
380
  function createResourceState() {
@@ -295,6 +403,7 @@ function followPrimary(store, primary) {
295
403
  following.set(store, primary.subscribe(sync));
296
404
  }
297
405
  function dispose(store) {
406
+ releaseAllOwners(store);
298
407
  untrackPrimary(store);
299
408
  following.get(store)?.();
300
409
  following.delete(store);
@@ -312,33 +421,129 @@ function ensureTSLExtension() {
312
421
  });
313
422
  }
314
423
 
424
+ const SCOPE = Symbol("readTracking.scope");
425
+ const LEAF_GUARDS = {
426
+ uniforms: isUniformNode,
427
+ nodes: isTSLNode,
428
+ buffers: isBufferLike,
429
+ gpuStorage: isStorageLike
430
+ };
431
+ function isScope(kind, value) {
432
+ return !!value && typeof value === "object" && !LEAF_GUARDS[kind](value);
433
+ }
434
+ function classify(kind, value) {
435
+ return kind !== "textures" && isScope(kind, value) ? SCOPE : value;
436
+ }
437
+ function resolveContainer(kind, root, scopePath) {
438
+ let container = root;
439
+ for (const key of scopePath) {
440
+ const next = container?.[key];
441
+ container = isScope(kind, next) ? next : void 0;
442
+ }
443
+ return container;
444
+ }
445
+ function observeNow(read, view) {
446
+ if (read.kind === "textures") {
447
+ const map = view("textures");
448
+ if (read.op === "keys") return [...map.keys()];
449
+ return read.op === "has" ? map.has(read.path[0]) : map.get(read.path[0]);
450
+ }
451
+ const root = view(read.kind);
452
+ if (read.op === "keys") return Object.keys(resolveContainer(read.kind, root, read.path) ?? {});
453
+ const container = resolveContainer(read.kind, root, read.path.slice(0, -1));
454
+ const key = read.path[read.path.length - 1];
455
+ if (read.op === "has") return !!container && key in container;
456
+ return classify(read.kind, container?.[key]);
457
+ }
458
+ function sameKeys(a, b) {
459
+ return a.length === b.length && a.every((key, i) => key === b[i]);
460
+ }
461
+ function hasChanged(read, view) {
462
+ const now = observeNow(read, view);
463
+ return read.op === "keys" ? !sameKeys(read.seen, now) : !Object.is(read.seen, now);
464
+ }
465
+ const warned = /* @__PURE__ */ new Set();
466
+ function warnOnce(message) {
467
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
468
+ if (warned.has(message)) return;
469
+ warned.add(message);
470
+ console.warn(message);
471
+ }
472
+ function describeRead(kind, path) {
473
+ if (kind === "textures") return `textures.get('${path[0]}')`;
474
+ const scopes = path.slice(0, -1).map((key) => `.scope('${key}')`);
475
+ return `${kind}${scopes.join("")}.${path[path.length - 1] ?? ""}`;
476
+ }
477
+ function createReadTracker(hookName) {
478
+ const tracker = {
479
+ reads: [],
480
+ closed: false,
481
+ stale: false,
482
+ observe: (read) => {
483
+ if (tracker.closed) {
484
+ warnOnce(
485
+ `[${hookName}] ${describeRead(read.kind, read.path)} was read after the creator returned, probably inside Fn(). That read is not tracked, so replacing the resource will not rebuild this graph. Read the resource in the creator and close over it in Fn.`
486
+ );
487
+ return;
488
+ }
489
+ tracker.reads.push(read);
490
+ }
491
+ };
492
+ return tracker;
493
+ }
494
+ function isTrackerStale(tracker, view) {
495
+ if (!tracker.stale) tracker.stale = tracker.reads.some((read) => hasChanged(read, view));
496
+ return tracker.stale;
497
+ }
498
+ function warnMissingReads(hookName) {
499
+ return {
500
+ nested: false,
501
+ observe: (read) => {
502
+ if (read.op !== "get" || read.seen !== void 0) return;
503
+ warnOnce(
504
+ `[${hookName}] The creator read ${describeRead(read.kind, read.path)}, which does not exist yet. ${hookName} creators run once per generation, so this one will not re-run when it appears. Register it earlier (above this hook, or higher in the tree), or call rebuild* once it exists. To probe on purpose, use .has().`
505
+ );
506
+ }
507
+ };
508
+ }
509
+
315
510
  var __defProp = Object.defineProperty;
316
511
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
317
512
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
318
- var _a, _b;
513
+ var _a, _b, _c, _d;
319
514
  const INTERNAL_DATA = Symbol("ScopedStore.data");
320
515
  const INTERNAL_IS_LEAF = Symbol("ScopedStore.isLeaf");
321
- _b = INTERNAL_DATA, _a = INTERNAL_IS_LEAF;
516
+ const INTERNAL_READS = Symbol("ScopedStore.reads");
517
+ const INTERNAL_CHILDREN = Symbol("ScopedStore.children");
518
+ _d = INTERNAL_DATA, _c = INTERNAL_IS_LEAF, _b = INTERNAL_READS, _a = INTERNAL_CHILDREN;
322
519
  const _ScopedStore = class _ScopedStore {
323
- constructor(data, isLeaf) {
520
+ constructor(data, isLeaf, reads) {
521
+ /** @internal */
522
+ __publicField(this, _d);
523
+ /** @internal */
524
+ __publicField(this, _c);
525
+ /** @internal */
324
526
  __publicField(this, _b);
527
+ /** @internal */
325
528
  __publicField(this, _a);
326
529
  this[INTERNAL_DATA] = data;
327
530
  this[INTERNAL_IS_LEAF] = isLeaf;
531
+ this[INTERNAL_READS] = reads;
328
532
  return new Proxy(this, {
329
533
  get(target, prop, receiver) {
330
534
  if (typeof prop === "string") {
331
535
  if (prop === "scope" || prop === "has" || prop === "keys") {
332
536
  return Reflect.get(target, prop, receiver);
333
537
  }
334
- return target[INTERNAL_DATA][prop];
538
+ return readEntry(target, prop);
335
539
  }
336
540
  return Reflect.get(target, prop, receiver);
337
541
  },
338
542
  has(target, prop) {
339
- return typeof prop === "string" ? prop in target[INTERNAL_DATA] : Reflect.has(target, prop);
543
+ return typeof prop === "string" ? target.has(prop) : Reflect.has(target, prop);
340
544
  },
341
545
  ownKeys(target) {
546
+ observeKeys(target);
342
547
  return Reflect.ownKeys(target[INTERNAL_DATA]);
343
548
  },
344
549
  getOwnPropertyDescriptor(target, prop) {
@@ -360,54 +565,152 @@ const _ScopedStore = class _ScopedStore {
360
565
  scope(key) {
361
566
  const value = this[INTERNAL_DATA][key];
362
567
  const isLeaf = this[INTERNAL_IS_LEAF];
568
+ const reads = this[INTERNAL_READS];
363
569
  const scope = value && typeof value === "object" && !isLeaf(value) ? value : {};
364
- return new _ScopedStore(scope, isLeaf);
570
+ const child = reads ? { ...reads, path: [...reads.path, key] } : void 0;
571
+ return new _ScopedStore(scope, isLeaf, child);
365
572
  }
366
573
  /**
367
574
  * Check if a key exists in the store.
368
575
  */
369
576
  has(key) {
370
- return key in this[INTERNAL_DATA];
577
+ const found = key in this[INTERNAL_DATA];
578
+ const reads = this[INTERNAL_READS];
579
+ reads?.observe({ kind: reads.kind, op: "has", path: [...reads.path, key], seen: found });
580
+ return found;
371
581
  }
372
582
  /**
373
583
  * Get all keys in the store.
374
584
  */
375
585
  keys() {
586
+ observeKeys(this);
376
587
  return Object.keys(this[INTERNAL_DATA]);
377
588
  }
378
589
  };
379
590
  let ScopedStore = _ScopedStore;
591
+ function readEntry(target, key) {
592
+ const value = target[INTERNAL_DATA][key];
593
+ const reads = target[INTERNAL_READS];
594
+ if (!reads) return value;
595
+ const isLeaf = target[INTERNAL_IS_LEAF];
596
+ const isScope = !!value && typeof value === "object" && !isLeaf(value);
597
+ const path = [...reads.path, key];
598
+ reads.observe({ kind: reads.kind, op: "get", path, seen: isScope ? SCOPE : value });
599
+ if (!isScope || !reads.nested) return value;
600
+ const children = target[INTERNAL_CHILDREN] ?? (target[INTERNAL_CHILDREN] = /* @__PURE__ */ new Map());
601
+ let child = children.get(key);
602
+ if (!child) {
603
+ child = new ScopedStore(value, isLeaf, { ...reads, path });
604
+ children.set(key, child);
605
+ }
606
+ return child;
607
+ }
608
+ function observeKeys(target) {
609
+ const reads = target[INTERNAL_READS];
610
+ reads?.observe({ kind: reads.kind, op: "keys", path: reads.path, seen: Object.keys(target[INTERNAL_DATA]) });
611
+ }
380
612
  function createScopedStore(data, isLeaf) {
381
613
  return new ScopedStore(data, isLeaf);
382
614
  }
383
- function createLazyCreatorState(state, store) {
615
+ function observeTextures(map, observe) {
616
+ const observeKeys2 = () => observe({ kind: "textures", op: "keys", path: [], seen: [...map.keys()] });
617
+ const observeAll = () => {
618
+ observeKeys2();
619
+ for (const [url, texture] of map) observe({ kind: "textures", op: "get", path: [url], seen: texture });
620
+ };
621
+ return new Proxy(map, {
622
+ get(target, prop) {
623
+ switch (prop) {
624
+ case "get":
625
+ return (url) => {
626
+ const texture = target.get(url);
627
+ observe({ kind: "textures", op: "get", path: [url], seen: texture });
628
+ return texture;
629
+ };
630
+ case "has":
631
+ return (url) => {
632
+ const found = target.has(url);
633
+ observe({ kind: "textures", op: "has", path: [url], seen: found });
634
+ return found;
635
+ };
636
+ case "size":
637
+ observeKeys2();
638
+ return target.size;
639
+ case "keys":
640
+ return () => {
641
+ observeKeys2();
642
+ return target.keys();
643
+ };
644
+ case "values":
645
+ return () => {
646
+ observeAll();
647
+ return target.values();
648
+ };
649
+ case "entries":
650
+ case Symbol.iterator:
651
+ return () => {
652
+ observeAll();
653
+ return target.entries();
654
+ };
655
+ case "forEach":
656
+ return (callback, thisArg) => {
657
+ observeAll();
658
+ target.forEach(callback, thisArg);
659
+ };
660
+ }
661
+ const value = Reflect.get(target, prop, target);
662
+ return typeof value === "function" ? value.bind(target) : value;
663
+ }
664
+ });
665
+ }
666
+ function createResourceView(primary, local = primary) {
667
+ return ((kind) => kind === "textures" ? extension.getTextureView(local) : withStagedOverlay(primary, kind, primary.getState()[kind]));
668
+ }
669
+ function createLazyCreatorState(state, store, options = {}) {
670
+ const { reads } = options;
671
+ const view = options.view ?? (store ? createResourceView(store) : void 0);
384
672
  let _uniforms = null;
385
673
  let _nodes = null;
386
674
  let _buffers = null;
387
675
  let _gpuStorage = null;
388
- const view = (kind) => store ? withStagedOverlay(store, kind, state[kind]) : state[kind];
389
- return Object.create(state, {
676
+ let _textures = null;
677
+ const read = (kind) => view ? view(kind) : state[kind];
678
+ const wrap = (kind, isLeaf) => new ScopedStore(
679
+ read(kind),
680
+ isLeaf,
681
+ reads && { ...reads, kind, path: [] }
682
+ );
683
+ const properties = {
390
684
  uniforms: {
391
685
  get() {
392
- return _uniforms ?? (_uniforms = createScopedStore(view("uniforms"), isUniformNode));
686
+ return _uniforms ?? (_uniforms = wrap("uniforms", isUniformNode));
393
687
  }
394
688
  },
395
689
  nodes: {
396
690
  get() {
397
- return _nodes ?? (_nodes = createScopedStore(view("nodes"), isTSLNode));
691
+ return _nodes ?? (_nodes = wrap("nodes", isTSLNode));
398
692
  }
399
693
  },
400
694
  buffers: {
401
695
  get() {
402
- return _buffers ?? (_buffers = createScopedStore(view("buffers"), isBufferLike));
696
+ return _buffers ?? (_buffers = wrap("buffers", isBufferLike));
403
697
  }
404
698
  },
405
699
  gpuStorage: {
406
700
  get() {
407
- return _gpuStorage ?? (_gpuStorage = createScopedStore(view("gpuStorage"), isStorageLike));
701
+ return _gpuStorage ?? (_gpuStorage = wrap("gpuStorage", isStorageLike));
408
702
  }
409
703
  }
410
- });
704
+ };
705
+ if (options.view) {
706
+ properties.textures = {
707
+ get() {
708
+ const textures = options.view("textures");
709
+ return _textures ?? (_textures = reads ? observeTextures(textures, reads.observe) : textures);
710
+ }
711
+ };
712
+ }
713
+ return Object.create(state, properties);
411
714
  }
412
715
 
413
716
  function resolvePrimary(local) {
@@ -483,7 +786,7 @@ function useUniforms(creatorOrScope, scope) {
483
786
  const processedInput = React.useMemo(() => {
484
787
  let raw = creatorOrScope;
485
788
  if (typeof creatorOrScope === "function") {
486
- const wrappedState = createLazyCreatorState(store.getState(), store);
789
+ const wrappedState = createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useUniforms") });
487
790
  raw = creatorOrScope(wrappedState);
488
791
  }
489
792
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
@@ -506,8 +809,9 @@ function useUniforms(creatorOrScope, scope) {
506
809
  input: memoizedInput,
507
810
  create: () => {
508
811
  if (isReader) return {};
509
- if (typeof processedInput.candidates !== "object" || processedInput.candidates === null)
812
+ if (typeof processedInput.candidates !== "object" || processedInput.candidates === null) {
510
813
  throw new Error("Invalid uniform input");
814
+ }
511
815
  return processedInput.candidates;
512
816
  },
513
817
  prepare: (name, candidate) => createUniform(name, candidate, scope),
@@ -525,13 +829,7 @@ function useUniforms(creatorOrScope, scope) {
525
829
  if (!equals) existing.value = newVal;
526
830
  }
527
831
  });
528
- let uniforms = created;
529
- if (memoizedInput === void 0) {
530
- uniforms = storeUniforms;
531
- } else if (typeof memoizedInput === "string") {
532
- const scopeData = storeUniforms[memoizedInput];
533
- uniforms = scopeData && !isUniformNode(scopeData) ? scopeData : {};
534
- }
832
+ const uniforms = isReader ? readWithStaged(store, "uniforms", storeUniforms, memoizedInput, isUniformNode) : created;
535
833
  return { ...uniforms, removeUniforms, clearUniforms, rebuildUniforms };
536
834
  }
537
835
  function rebuildAllUniforms(store, scope) {
@@ -565,18 +863,20 @@ function isSameThreeType(a, b) {
565
863
  const uniformOf = tsl.uniform;
566
864
  function useUniform(name, value) {
567
865
  const store = usePrimaryStore();
866
+ const memoizedInput = useCompareMemoize(value === void 0 ? name : { [name]: normalizeValue(value) });
568
867
  const registered = useScopedResource({
569
868
  store,
570
869
  kind: "uniforms",
571
870
  scope: void 0,
572
871
  isLeaf: isUniformNode,
573
- input: name,
872
+ input: memoizedInput,
574
873
  create: () => value === void 0 ? {} : { [name]: value },
575
874
  prepare: createNamedUniform,
576
875
  reconcile: (existing2) => {
577
- if (value !== void 0 && !isTSLNode(value) && !isUniformNode(value)) {
578
- existing2.value = typeof value === "string" ? new THREE.Color(value) : value;
579
- }
876
+ if (value === void 0 || isUniformNode(value)) return;
877
+ const next = normalizeValue(value);
878
+ if (isTSLNode(next)) return;
879
+ existing2.value = next;
580
880
  }
581
881
  });
582
882
  if (registered[name]) return registered[name];
@@ -586,6 +886,9 @@ function useUniform(name, value) {
586
886
  `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
587
887
  );
588
888
  }
889
+ function normalizeValue(value) {
890
+ return typeof value === "string" ? new THREE.Color(value) : vectorize(value);
891
+ }
589
892
  function createNamedUniform(name, value) {
590
893
  let node;
591
894
  if (isUniformNode(value)) {
@@ -626,7 +929,16 @@ function useNodes(creatorOrScope, scope) {
626
929
  isLeaf: isTSLNode,
627
930
  create: () => {
628
931
  if (isReader) return {};
629
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
932
+ const nodes2 = creatorOrScope(
933
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useNodes") })
934
+ );
935
+ if (nodes2 == null) {
936
+ warnOnce(
937
+ "[useNodes] The creator returned nothing, so nothing was registered. useNodes registers the nodes its creator returns. To put a node onto a Three object (scene.fogNode = ...), use useLocalNodes and return a function that assigns it: it runs after commit and may return a cleanup."
938
+ );
939
+ return {};
940
+ }
941
+ return nodes2;
630
942
  },
631
943
  prepare: (name, node) => {
632
944
  const setName = Reflect.get(node, "setName");
@@ -634,28 +946,106 @@ function useNodes(creatorOrScope, scope) {
634
946
  return node;
635
947
  }
636
948
  });
637
- let nodes = created;
638
- if (creatorOrScope === void 0) {
639
- nodes = storeNodes;
640
- } else if (typeof creatorOrScope === "string") {
641
- const scopeData = storeNodes[creatorOrScope];
642
- nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
643
- }
949
+ const nodes = isReader ? readWithStaged(store, "nodes", storeNodes, creatorOrScope, isTSLNode) : created;
644
950
  return { ...nodes, removeNodes, clearNodes, rebuildNodes };
645
951
  }
646
952
  function rebuildAllNodes(store, scope) {
647
953
  rebuildResource(store, "nodes", scope);
648
954
  }
649
- function useLocalNodes(creator) {
955
+ function areDepsEqual(next, prev) {
956
+ if (next.length !== prev.length) return false;
957
+ for (let i = 0; i < next.length; i++) if (!Object.is(next[i], prev[i])) return false;
958
+ return true;
959
+ }
960
+ function useDependencyToken(deps) {
961
+ const previous = React.useRef(null);
962
+ const record = previous.current;
963
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && record) {
964
+ const hadDeps = record.deps !== void 0;
965
+ const hasDeps = deps !== void 0;
966
+ if (hadDeps !== hasDeps) {
967
+ console.warn(
968
+ `[useLocalNodes] The dependency array was ${hasDeps ? "added" : "omitted"} between renders. Pass an array on every render or on none; the mode must not change for a mounted component.`
969
+ );
970
+ } else if (hasDeps && record.deps.length !== deps.length) {
971
+ console.warn(
972
+ `[useLocalNodes] The dependency array length changed between renders (${record.deps.length} \u2192 ${deps.length}). Declare a fixed-length list; conditional dependencies belong inside the array as values.`
973
+ );
974
+ }
975
+ }
976
+ if (deps === void 0) {
977
+ previous.current = { deps: void 0, token: {} };
978
+ } else if (!record || record.deps === void 0 || !areDepsEqual(deps, record.deps)) {
979
+ previous.current = { deps, token: {} };
980
+ } else if (record.deps !== deps) {
981
+ previous.current = { deps, token: record.token };
982
+ }
983
+ return previous.current.token;
984
+ }
985
+ function useLocalNodes(creator, deps) {
986
+ const local = extension.useStore();
650
987
  const store = usePrimaryStore();
651
- const uniforms = usePrimaryThree((s) => s.uniforms);
652
- const nodes = usePrimaryThree((s) => s.nodes);
653
- const textures = usePrimaryThree((s) => s.textures);
988
+ const view = React.useMemo(() => createResourceView(store, local), [store, local]);
654
989
  const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
655
- return React.useMemo(() => {
656
- const wrappedState = createLazyCreatorState(store.getState(), store);
657
- return creator(wrappedState);
658
- }, [store, creator, uniforms, nodes, textures, hmrVersion]);
990
+ const depsToken = useDependencyToken(deps);
991
+ const committedReads = React.useRef(null);
992
+ const readsVersion = React.useRef(0);
993
+ const getReadsVersion = () => {
994
+ const tracker2 = committedReads.current;
995
+ if (tracker2 && !tracker2.stale && isTrackerStale(tracker2, view)) readsVersion.current++;
996
+ return readsVersion.current;
997
+ };
998
+ const subscribe = React.useCallback(
999
+ (onChange) => {
1000
+ const unsubscribe = store.subscribe(onChange);
1001
+ if (local === store) return unsubscribe;
1002
+ const unsubscribeLocal = local.subscribe(onChange);
1003
+ return () => {
1004
+ unsubscribe();
1005
+ unsubscribeLocal();
1006
+ };
1007
+ },
1008
+ [store, local]
1009
+ );
1010
+ const resourceVersion = React.useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
1011
+ const evaluation = React.useMemo(() => {
1012
+ const tracker2 = createReadTracker("useLocalNodes");
1013
+ const reads = { observe: tracker2.observe, nested: true };
1014
+ const value2 = creator(createLazyCreatorState(local.getState(), store, { reads, view }));
1015
+ tracker2.closed = true;
1016
+ return { value: value2, tracker: tracker2 };
1017
+ }, [view, hmrVersion, depsToken, resourceVersion]);
1018
+ const { value, tracker } = evaluation;
1019
+ const installing = typeof value === "function";
1020
+ useModeDiagnostics(value, installing);
1021
+ useIsomorphicLayoutEffect(() => {
1022
+ committedReads.current = tracker;
1023
+ if (!installing) return;
1024
+ tracker.closed = false;
1025
+ let cleanup;
1026
+ try {
1027
+ cleanup = value();
1028
+ } finally {
1029
+ tracker.closed = true;
1030
+ }
1031
+ return typeof cleanup === "function" ? cleanup : void 0;
1032
+ }, [evaluation]);
1033
+ return installing ? void 0 : value;
1034
+ }
1035
+ function useModeDiagnostics(value, installing) {
1036
+ const previous = React.useRef(null);
1037
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
1038
+ if (value === void 0 || value === null) {
1039
+ warnOnce(
1040
+ "[useLocalNodes] The creator returned nothing. To put a node onto a Three object (scene.fogNode = ...), build it in the creator and return a function that assigns it: that function runs after commit and may return a cleanup. Assigning inside the creator runs during render, which React may discard or repeat."
1041
+ );
1042
+ }
1043
+ if (previous.current !== null && previous.current !== installing) {
1044
+ warnOnce(
1045
+ "[useLocalNodes] The creator switched between returning a record and returning an install function. Keep one form for a mounted component."
1046
+ );
1047
+ }
1048
+ previous.current = installing;
659
1049
  }
660
1050
 
661
1051
  const disposeBuffer = (buffer) => {
@@ -701,7 +1091,9 @@ function useBuffers(creatorOrScope, scope) {
701
1091
  isLeaf: isBufferLike,
702
1092
  create: () => {
703
1093
  if (isReader) return {};
704
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
1094
+ return creatorOrScope(
1095
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useBuffers") })
1096
+ );
705
1097
  },
706
1098
  prepare: (name, buffer) => {
707
1099
  const setName = Reflect.get(buffer, "setName");
@@ -709,13 +1101,7 @@ function useBuffers(creatorOrScope, scope) {
709
1101
  return buffer;
710
1102
  }
711
1103
  });
712
- let buffers = created;
713
- if (creatorOrScope === void 0) {
714
- buffers = storeBuffers;
715
- } else if (typeof creatorOrScope === "string") {
716
- const scopeData = storeBuffers[creatorOrScope];
717
- buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
718
- }
1104
+ const buffers = isReader ? readWithStaged(store, "buffers", storeBuffers, creatorOrScope, isBufferLike) : created;
719
1105
  return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
720
1106
  }
721
1107
  function rebuildAllBuffers(store, scope) {
@@ -765,7 +1151,9 @@ function useGPUStorage(creatorOrScope, scope) {
765
1151
  isLeaf: isStorageLike,
766
1152
  create: () => {
767
1153
  if (isReader) return {};
768
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
1154
+ return creatorOrScope(
1155
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useGPUStorage") })
1156
+ );
769
1157
  },
770
1158
  prepare: (name, storage) => {
771
1159
  const label = scopedNodeName(scope, name);
@@ -777,13 +1165,7 @@ function useGPUStorage(creatorOrScope, scope) {
777
1165
  return storage;
778
1166
  }
779
1167
  });
780
- let gpuStorage = created;
781
- if (creatorOrScope === void 0) {
782
- gpuStorage = storeStorage;
783
- } else if (typeof creatorOrScope === "string") {
784
- const scopeData = storeStorage[creatorOrScope];
785
- gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
786
- }
1168
+ const gpuStorage = isReader ? readWithStaged(store, "gpuStorage", storeStorage, creatorOrScope, isStorageLike) : created;
787
1169
  return {
788
1170
  ...gpuStorage,
789
1171
  removeStorage,
@@ -797,120 +1179,164 @@ function rebuildAllStorage(store, scope) {
797
1179
  }
798
1180
 
799
1181
  const NO_PASSES = Object.freeze({});
800
- function renderThroughPipeline(store) {
801
- return () => {
802
- const pipeline = store.getState().renderPipeline;
803
- if (pipeline?.render) pipeline.render();
804
- };
805
- }
806
- function useRenderPipeline(mainCB, setupCB) {
1182
+ function useRenderPipeline(mainCB, setupCB, options) {
807
1183
  const store = extension.useStore();
1184
+ const primary = usePrimaryStore();
1185
+ const view = React.useMemo(() => createResourceView(primary, store), [primary, store]);
808
1186
  const { scene, camera, renderer, isLegacy } = extension.useThree();
1187
+ const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
1188
+ const isOwner = mainCB !== void 0 || setupCB !== void 0;
809
1189
  const callbacksRanRef = React.useRef(false);
810
- const scenePassCacheRef = React.useRef(null);
811
1190
  const forceScenePassRebuildRef = React.useRef(false);
812
1191
  const mainCBRef = React.useRef(mainCB);
813
1192
  const setupCBRef = React.useRef(setupCB);
1193
+ const persistRef = React.useRef(options?.persist ?? false);
814
1194
  mainCBRef.current = mainCB;
815
1195
  setupCBRef.current = setupCB;
1196
+ persistRef.current = options?.persist ?? false;
816
1197
  const [rebuildVersion, setRebuildVersion] = React.useState(0);
817
- React.useEffect(() => {
1198
+ const rerun = React.useCallback((freshScenePass) => {
818
1199
  callbacksRanRef.current = false;
819
- forceScenePassRebuildRef.current = true;
1200
+ if (freshScenePass) forceScenePassRebuildRef.current = true;
1201
+ setRebuildVersion((v) => v + 1);
820
1202
  }, []);
1203
+ const ownerRef = React.useRef(null);
1204
+ const target = React.useCallback(() => (isOwner ? ownerRef.current : activeOwner(store)) ?? null, [isOwner, store]);
821
1205
  const clearPasses = React.useCallback(() => {
822
- store.setState({ passes: {} });
823
- }, [store]);
1206
+ const owner = target();
1207
+ if (!owner) return;
1208
+ owner.passes = {};
1209
+ if (activeOwner(store) === owner) store.setState({ passes: {} });
1210
+ }, [store, target]);
824
1211
  const reset = React.useCallback(() => {
825
- store.setState({
826
- renderPipeline: null,
827
- passes: {}
828
- });
829
- extension.setRenderOverride(store, null);
830
- scenePassCacheRef.current?.scenePass.dispose();
831
- callbacksRanRef.current = false;
832
- scenePassCacheRef.current = null;
833
- forceScenePassRebuildRef.current = false;
834
- }, [store]);
1212
+ const owner = target();
1213
+ if (!owner) return;
1214
+ releaseOwner(owner);
1215
+ if (owner === ownerRef.current) committedReads.current = null;
1216
+ }, [target]);
835
1217
  const rebuild = React.useCallback(() => {
836
- callbacksRanRef.current = false;
837
- forceScenePassRebuildRef.current = true;
838
- setRebuildVersion((v) => v + 1);
839
- }, []);
1218
+ if (isOwner) rerun(true);
1219
+ else activeOwner(store)?.rebuild();
1220
+ }, [isOwner, rerun, store]);
1221
+ const hmrVersionRef = React.useRef(hmrVersion);
1222
+ React.useEffect(() => {
1223
+ if (hmrVersionRef.current === hmrVersion) return;
1224
+ hmrVersionRef.current = hmrVersion;
1225
+ if (isOwner) rerun(false);
1226
+ }, [hmrVersion, isOwner, rerun]);
1227
+ const committedReads = React.useRef(null);
1228
+ const readsVersion = React.useRef(0);
1229
+ const getReadsVersion = () => {
1230
+ const tracker = committedReads.current;
1231
+ if (tracker && !tracker.stale && isTrackerStale(tracker, view)) readsVersion.current++;
1232
+ return readsVersion.current;
1233
+ };
1234
+ const subscribe = React.useCallback(
1235
+ (onChange) => {
1236
+ const unsubscribe = primary.subscribe(onChange);
1237
+ if (store === primary) return unsubscribe;
1238
+ const unsubscribeLocal = store.subscribe(onChange);
1239
+ return () => {
1240
+ unsubscribe();
1241
+ unsubscribeLocal();
1242
+ };
1243
+ },
1244
+ [primary, store]
1245
+ );
1246
+ const resourceVersion = React.useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
1247
+ const lastResourceVersion = React.useRef(resourceVersion);
840
1248
  React.useLayoutEffect(() => {
841
1249
  if (isLegacy) {
842
- throw new Error("useRenderPipeline is only available with WebGPU renderer. Set renderer prop on Canvas.");
1250
+ throw new Error(
1251
+ "useRenderPipeline needs WebGPU: import Canvas from @react-three/fiber, not @react-three/fiber/legacy."
1252
+ );
843
1253
  }
1254
+ if (!isOwner) return;
844
1255
  if (!renderer || !scene || !camera) return;
845
1256
  if (!("isWebGPURenderer" in renderer)) {
846
1257
  throw new Error("useRenderPipeline: renderer is not a WebGPURenderer but isLegacy is false.");
847
1258
  }
848
- const state = store.getState();
849
- const set = store.setState;
1259
+ let owner = ownerRef.current;
1260
+ if (owner && owner.store !== store) {
1261
+ releaseOwner(owner);
1262
+ owner = null;
1263
+ }
1264
+ if (!owner) owner = ownerRef.current = createOwner(store, () => rerun(true));
1265
+ claimOwnership(owner);
1266
+ const currentOwner = owner;
850
1267
  try {
851
- let pipeline = state.renderPipeline ?? null;
852
- let currentPasses = { ...state.passes };
853
1268
  let justCreatedPipeline = false;
854
- if (!pipeline) {
855
- pipeline = new THREE__namespace.RenderPipeline(renderer);
1269
+ if (!owner.pipeline || owner.pipeline.renderer !== renderer) {
1270
+ disposeOwnerResources(owner);
1271
+ owner.pipeline = new THREE__namespace.RenderPipeline(renderer);
856
1272
  justCreatedPipeline = true;
857
1273
  }
858
- const cacheValid = !forceScenePassRebuildRef.current && scenePassCacheRef.current && scenePassCacheRef.current.sceneUuid === scene.uuid && scenePassCacheRef.current.cameraUuid === camera.uuid;
1274
+ const pipeline = owner.pipeline;
1275
+ const cached = owner.scenePass;
1276
+ const cacheValid = !forceScenePassRebuildRef.current && cached !== null && cached.sceneUuid === scene.uuid && cached.cameraUuid === camera.uuid;
859
1277
  let outgoingScenePass = null;
860
1278
  let scenePass;
861
- if (cacheValid) {
862
- scenePass = scenePassCacheRef.current.scenePass;
1279
+ if (cacheValid && cached) {
1280
+ scenePass = cached.node;
863
1281
  } else {
864
- outgoingScenePass = scenePassCacheRef.current?.scenePass ?? null;
1282
+ outgoingScenePass = cached?.node ?? null;
865
1283
  scenePass = tsl.pass(scene, camera);
866
- scenePassCacheRef.current = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, scenePass };
1284
+ owner.scenePass = { sceneUuid: scene.uuid, cameraUuid: camera.uuid, node: scenePass };
867
1285
  forceScenePassRebuildRef.current = false;
868
1286
  }
869
- currentPasses.scenePass = scenePass;
870
1287
  let outputNodeChanged = false;
871
1288
  if (!pipeline.outputNode || justCreatedPipeline || pipeline.outputNode === outgoingScenePass) {
872
1289
  pipeline.outputNode = scenePass;
873
1290
  outputNodeChanged = true;
874
1291
  }
875
- set({ renderPipeline: pipeline, passes: currentPasses });
876
- extension.setRenderOverride(store, renderThroughPipeline(store));
877
- const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid;
1292
+ const readsChanged = lastResourceVersion.current !== resourceVersion;
1293
+ lastResourceVersion.current = resourceVersion;
1294
+ const shouldRunCallbacks = justCreatedPipeline || !callbacksRanRef.current || !cacheValid || readsChanged;
878
1295
  if (shouldRunCallbacks) {
879
- if (setupCBRef.current) {
880
- const freshState = Object.assign({}, store.getState(), {
881
- renderPipeline: pipeline,
882
- passes: { ...currentPasses, scenePass },
883
- renderer,
884
- isLegacy: false
885
- });
886
- const setupResult = setupCBRef.current(freshState);
887
- if (setupResult && typeof setupResult === "object") {
888
- currentPasses = { ...currentPasses, ...setupResult };
889
- set({ passes: currentPasses });
1296
+ let currentPasses = { scenePass };
1297
+ owner.passes = currentPasses;
1298
+ const tracker = createReadTracker("useRenderPipeline");
1299
+ const callbackState = () => Object.assign(
1300
+ createLazyCreatorState(store.getState(), primary, {
1301
+ reads: { observe: tracker.observe, nested: true },
1302
+ view
1303
+ }),
1304
+ { renderPipeline: pipeline, passes: { ...currentPasses, scenePass }, renderer, isLegacy: false }
1305
+ );
1306
+ try {
1307
+ if (setupCBRef.current) {
1308
+ const setupResult = setupCBRef.current(callbackState());
1309
+ if (setupResult && typeof setupResult === "object") {
1310
+ currentPasses = { ...currentPasses, ...setupResult };
1311
+ owner.passes = currentPasses;
1312
+ }
890
1313
  }
891
- }
892
- if (mainCBRef.current) {
893
- const freshState = Object.assign({}, store.getState(), {
894
- renderPipeline: pipeline,
895
- passes: { ...currentPasses, scenePass },
896
- renderer,
897
- isLegacy: false
898
- });
899
- const mainResult = mainCBRef.current(freshState);
900
- if (mainResult && typeof mainResult === "object") {
901
- currentPasses = { ...currentPasses, ...mainResult };
902
- set({ passes: currentPasses });
1314
+ if (mainCBRef.current) {
1315
+ const mainResult = mainCBRef.current(callbackState());
1316
+ if (mainResult && typeof mainResult === "object") {
1317
+ currentPasses = { ...currentPasses, ...mainResult };
1318
+ owner.passes = currentPasses;
1319
+ }
903
1320
  }
1321
+ } finally {
1322
+ tracker.closed = true;
1323
+ committedReads.current = tracker;
904
1324
  }
905
1325
  outputNodeChanged = true;
906
1326
  callbacksRanRef.current = true;
907
1327
  }
908
1328
  if (outputNodeChanged) pipeline.needsUpdate = true;
1329
+ publishOwner(owner);
909
1330
  if (outgoingScenePass && outgoingScenePass !== scenePass) outgoingScenePass.dispose();
910
1331
  } catch (error) {
1332
+ publishOwner(owner);
911
1333
  console.warn("[useRenderPipeline] Setup failed; the render pipeline was not configured:", error);
912
1334
  }
913
- }, [store, renderer, scene, camera, isLegacy, rebuildVersion]);
1335
+ return () => {
1336
+ if (persistRef.current) currentOwner.mounted = false;
1337
+ else scheduleRelease(currentOwner);
1338
+ };
1339
+ }, [store, primary, view, renderer, scene, camera, isLegacy, isOwner, rebuildVersion, resourceVersion, rerun]);
914
1340
  const passes = extension.useThree((s) => s.passes) ?? NO_PASSES;
915
1341
  const renderPipeline = extension.useThree((s) => s.renderPipeline) ?? null;
916
1342
  const actions = { passes, clearPasses, reset, rebuild };