@react-three/tsl 10.0.0-canary.e53bd54 → 10.0.0-canary.f50076a

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
@@ -177,6 +177,10 @@ function extractTSLValue(value) {
177
177
  if (node.isConstNode) {
178
178
  return node.value;
179
179
  }
180
+ if (node.isVarNode && node.node) {
181
+ const inner = extractTSLValue(node.node);
182
+ return inner === node.node ? value : inner;
183
+ }
180
184
  if ("value" in node) {
181
185
  let extractedValue = node.value;
182
186
  if (typeof node.traverse === "function") {
@@ -312,33 +316,129 @@ function ensureTSLExtension() {
312
316
  });
313
317
  }
314
318
 
319
+ const SCOPE = Symbol("readTracking.scope");
320
+ const LEAF_GUARDS = {
321
+ uniforms: isUniformNode,
322
+ nodes: isTSLNode,
323
+ buffers: isBufferLike,
324
+ gpuStorage: isStorageLike
325
+ };
326
+ function isScope(kind, value) {
327
+ return !!value && typeof value === "object" && !LEAF_GUARDS[kind](value);
328
+ }
329
+ function classify(kind, value) {
330
+ return kind !== "textures" && isScope(kind, value) ? SCOPE : value;
331
+ }
332
+ function resolveContainer(kind, root, scopePath) {
333
+ let container = root;
334
+ for (const key of scopePath) {
335
+ const next = container?.[key];
336
+ container = isScope(kind, next) ? next : void 0;
337
+ }
338
+ return container;
339
+ }
340
+ function observeNow(read, view) {
341
+ if (read.kind === "textures") {
342
+ const map = view("textures");
343
+ if (read.op === "keys") return [...map.keys()];
344
+ return read.op === "has" ? map.has(read.path[0]) : map.get(read.path[0]);
345
+ }
346
+ const root = view(read.kind);
347
+ if (read.op === "keys") return Object.keys(resolveContainer(read.kind, root, read.path) ?? {});
348
+ const container = resolveContainer(read.kind, root, read.path.slice(0, -1));
349
+ const key = read.path[read.path.length - 1];
350
+ if (read.op === "has") return !!container && key in container;
351
+ return classify(read.kind, container?.[key]);
352
+ }
353
+ function sameKeys(a, b) {
354
+ return a.length === b.length && a.every((key, i) => key === b[i]);
355
+ }
356
+ function hasChanged(read, view) {
357
+ const now = observeNow(read, view);
358
+ return read.op === "keys" ? !sameKeys(read.seen, now) : !Object.is(read.seen, now);
359
+ }
360
+ const warned = /* @__PURE__ */ new Set();
361
+ function warnOnce(message) {
362
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
363
+ if (warned.has(message)) return;
364
+ warned.add(message);
365
+ console.warn(message);
366
+ }
367
+ function describeRead(kind, path) {
368
+ if (kind === "textures") return `textures.get('${path[0]}')`;
369
+ const scopes = path.slice(0, -1).map((key) => `.scope('${key}')`);
370
+ return `${kind}${scopes.join("")}.${path[path.length - 1] ?? ""}`;
371
+ }
372
+ function createReadTracker(hookName) {
373
+ const tracker = {
374
+ reads: [],
375
+ closed: false,
376
+ stale: false,
377
+ observe: (read) => {
378
+ if (tracker.closed) {
379
+ warnOnce(
380
+ `[${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.`
381
+ );
382
+ return;
383
+ }
384
+ tracker.reads.push(read);
385
+ }
386
+ };
387
+ return tracker;
388
+ }
389
+ function isTrackerStale(tracker, view) {
390
+ if (!tracker.stale) tracker.stale = tracker.reads.some((read) => hasChanged(read, view));
391
+ return tracker.stale;
392
+ }
393
+ function warnMissingReads(hookName) {
394
+ return {
395
+ nested: false,
396
+ observe: (read) => {
397
+ if (read.op !== "get" || read.seen !== void 0) return;
398
+ warnOnce(
399
+ `[${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().`
400
+ );
401
+ }
402
+ };
403
+ }
404
+
315
405
  var __defProp = Object.defineProperty;
316
406
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
317
407
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
318
- var _a, _b;
408
+ var _a, _b, _c, _d;
319
409
  const INTERNAL_DATA = Symbol("ScopedStore.data");
320
410
  const INTERNAL_IS_LEAF = Symbol("ScopedStore.isLeaf");
321
- _b = INTERNAL_DATA, _a = INTERNAL_IS_LEAF;
411
+ const INTERNAL_READS = Symbol("ScopedStore.reads");
412
+ const INTERNAL_CHILDREN = Symbol("ScopedStore.children");
413
+ _d = INTERNAL_DATA, _c = INTERNAL_IS_LEAF, _b = INTERNAL_READS, _a = INTERNAL_CHILDREN;
322
414
  const _ScopedStore = class _ScopedStore {
323
- constructor(data, isLeaf) {
415
+ constructor(data, isLeaf, reads) {
416
+ /** @internal */
417
+ __publicField(this, _d);
418
+ /** @internal */
419
+ __publicField(this, _c);
420
+ /** @internal */
324
421
  __publicField(this, _b);
422
+ /** @internal */
325
423
  __publicField(this, _a);
326
424
  this[INTERNAL_DATA] = data;
327
425
  this[INTERNAL_IS_LEAF] = isLeaf;
426
+ this[INTERNAL_READS] = reads;
328
427
  return new Proxy(this, {
329
428
  get(target, prop, receiver) {
330
429
  if (typeof prop === "string") {
331
430
  if (prop === "scope" || prop === "has" || prop === "keys") {
332
431
  return Reflect.get(target, prop, receiver);
333
432
  }
334
- return target[INTERNAL_DATA][prop];
433
+ return readEntry(target, prop);
335
434
  }
336
435
  return Reflect.get(target, prop, receiver);
337
436
  },
338
437
  has(target, prop) {
339
- return typeof prop === "string" ? prop in target[INTERNAL_DATA] : Reflect.has(target, prop);
438
+ return typeof prop === "string" ? target.has(prop) : Reflect.has(target, prop);
340
439
  },
341
440
  ownKeys(target) {
441
+ observeKeys(target);
342
442
  return Reflect.ownKeys(target[INTERNAL_DATA]);
343
443
  },
344
444
  getOwnPropertyDescriptor(target, prop) {
@@ -360,54 +460,152 @@ const _ScopedStore = class _ScopedStore {
360
460
  scope(key) {
361
461
  const value = this[INTERNAL_DATA][key];
362
462
  const isLeaf = this[INTERNAL_IS_LEAF];
463
+ const reads = this[INTERNAL_READS];
363
464
  const scope = value && typeof value === "object" && !isLeaf(value) ? value : {};
364
- return new _ScopedStore(scope, isLeaf);
465
+ const child = reads ? { ...reads, path: [...reads.path, key] } : void 0;
466
+ return new _ScopedStore(scope, isLeaf, child);
365
467
  }
366
468
  /**
367
469
  * Check if a key exists in the store.
368
470
  */
369
471
  has(key) {
370
- return key in this[INTERNAL_DATA];
472
+ const found = key in this[INTERNAL_DATA];
473
+ const reads = this[INTERNAL_READS];
474
+ reads?.observe({ kind: reads.kind, op: "has", path: [...reads.path, key], seen: found });
475
+ return found;
371
476
  }
372
477
  /**
373
478
  * Get all keys in the store.
374
479
  */
375
480
  keys() {
481
+ observeKeys(this);
376
482
  return Object.keys(this[INTERNAL_DATA]);
377
483
  }
378
484
  };
379
485
  let ScopedStore = _ScopedStore;
486
+ function readEntry(target, key) {
487
+ const value = target[INTERNAL_DATA][key];
488
+ const reads = target[INTERNAL_READS];
489
+ if (!reads) return value;
490
+ const isLeaf = target[INTERNAL_IS_LEAF];
491
+ const isScope = !!value && typeof value === "object" && !isLeaf(value);
492
+ const path = [...reads.path, key];
493
+ reads.observe({ kind: reads.kind, op: "get", path, seen: isScope ? SCOPE : value });
494
+ if (!isScope || !reads.nested) return value;
495
+ const children = target[INTERNAL_CHILDREN] ?? (target[INTERNAL_CHILDREN] = /* @__PURE__ */ new Map());
496
+ let child = children.get(key);
497
+ if (!child) {
498
+ child = new ScopedStore(value, isLeaf, { ...reads, path });
499
+ children.set(key, child);
500
+ }
501
+ return child;
502
+ }
503
+ function observeKeys(target) {
504
+ const reads = target[INTERNAL_READS];
505
+ reads?.observe({ kind: reads.kind, op: "keys", path: reads.path, seen: Object.keys(target[INTERNAL_DATA]) });
506
+ }
380
507
  function createScopedStore(data, isLeaf) {
381
508
  return new ScopedStore(data, isLeaf);
382
509
  }
383
- function createLazyCreatorState(state, store) {
510
+ function observeTextures(map, observe) {
511
+ const observeKeys2 = () => observe({ kind: "textures", op: "keys", path: [], seen: [...map.keys()] });
512
+ const observeAll = () => {
513
+ observeKeys2();
514
+ for (const [url, texture] of map) observe({ kind: "textures", op: "get", path: [url], seen: texture });
515
+ };
516
+ return new Proxy(map, {
517
+ get(target, prop) {
518
+ switch (prop) {
519
+ case "get":
520
+ return (url) => {
521
+ const texture = target.get(url);
522
+ observe({ kind: "textures", op: "get", path: [url], seen: texture });
523
+ return texture;
524
+ };
525
+ case "has":
526
+ return (url) => {
527
+ const found = target.has(url);
528
+ observe({ kind: "textures", op: "has", path: [url], seen: found });
529
+ return found;
530
+ };
531
+ case "size":
532
+ observeKeys2();
533
+ return target.size;
534
+ case "keys":
535
+ return () => {
536
+ observeKeys2();
537
+ return target.keys();
538
+ };
539
+ case "values":
540
+ return () => {
541
+ observeAll();
542
+ return target.values();
543
+ };
544
+ case "entries":
545
+ case Symbol.iterator:
546
+ return () => {
547
+ observeAll();
548
+ return target.entries();
549
+ };
550
+ case "forEach":
551
+ return (callback, thisArg) => {
552
+ observeAll();
553
+ target.forEach(callback, thisArg);
554
+ };
555
+ }
556
+ const value = Reflect.get(target, prop, target);
557
+ return typeof value === "function" ? value.bind(target) : value;
558
+ }
559
+ });
560
+ }
561
+ function createResourceView(primary, local = primary) {
562
+ return ((kind) => kind === "textures" ? extension.getTextureView(local) : withStagedOverlay(primary, kind, primary.getState()[kind]));
563
+ }
564
+ function createLazyCreatorState(state, store, options = {}) {
565
+ const { reads } = options;
566
+ const view = options.view ?? (store ? createResourceView(store) : void 0);
384
567
  let _uniforms = null;
385
568
  let _nodes = null;
386
569
  let _buffers = null;
387
570
  let _gpuStorage = null;
388
- const view = (kind) => store ? withStagedOverlay(store, kind, state[kind]) : state[kind];
389
- return Object.create(state, {
571
+ let _textures = null;
572
+ const read = (kind) => view ? view(kind) : state[kind];
573
+ const wrap = (kind, isLeaf) => new ScopedStore(
574
+ read(kind),
575
+ isLeaf,
576
+ reads && { ...reads, kind, path: [] }
577
+ );
578
+ const properties = {
390
579
  uniforms: {
391
580
  get() {
392
- return _uniforms ?? (_uniforms = createScopedStore(view("uniforms"), isUniformNode));
581
+ return _uniforms ?? (_uniforms = wrap("uniforms", isUniformNode));
393
582
  }
394
583
  },
395
584
  nodes: {
396
585
  get() {
397
- return _nodes ?? (_nodes = createScopedStore(view("nodes"), isTSLNode));
586
+ return _nodes ?? (_nodes = wrap("nodes", isTSLNode));
398
587
  }
399
588
  },
400
589
  buffers: {
401
590
  get() {
402
- return _buffers ?? (_buffers = createScopedStore(view("buffers"), isBufferLike));
591
+ return _buffers ?? (_buffers = wrap("buffers", isBufferLike));
403
592
  }
404
593
  },
405
594
  gpuStorage: {
406
595
  get() {
407
- return _gpuStorage ?? (_gpuStorage = createScopedStore(view("gpuStorage"), isStorageLike));
596
+ return _gpuStorage ?? (_gpuStorage = wrap("gpuStorage", isStorageLike));
408
597
  }
409
598
  }
410
- });
599
+ };
600
+ if (options.view) {
601
+ properties.textures = {
602
+ get() {
603
+ const textures = options.view("textures");
604
+ return _textures ?? (_textures = reads ? observeTextures(textures, reads.observe) : textures);
605
+ }
606
+ };
607
+ }
608
+ return Object.create(state, properties);
411
609
  }
412
610
 
413
611
  function resolvePrimary(local) {
@@ -483,7 +681,7 @@ function useUniforms(creatorOrScope, scope) {
483
681
  const processedInput = React.useMemo(() => {
484
682
  let raw = creatorOrScope;
485
683
  if (typeof creatorOrScope === "function") {
486
- const wrappedState = createLazyCreatorState(store.getState(), store);
684
+ const wrappedState = createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useUniforms") });
487
685
  raw = creatorOrScope(wrappedState);
488
686
  }
489
687
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
@@ -565,18 +763,20 @@ function isSameThreeType(a, b) {
565
763
  const uniformOf = tsl.uniform;
566
764
  function useUniform(name, value) {
567
765
  const store = usePrimaryStore();
766
+ const memoizedInput = useCompareMemoize(value === void 0 ? name : { [name]: normalizeValue(value) });
568
767
  const registered = useScopedResource({
569
768
  store,
570
769
  kind: "uniforms",
571
770
  scope: void 0,
572
771
  isLeaf: isUniformNode,
573
- input: name,
772
+ input: memoizedInput,
574
773
  create: () => value === void 0 ? {} : { [name]: value },
575
774
  prepare: createNamedUniform,
576
775
  reconcile: (existing2) => {
577
- if (value !== void 0 && !isTSLNode(value) && !isUniformNode(value)) {
578
- existing2.value = typeof value === "string" ? new THREE.Color(value) : value;
579
- }
776
+ if (value === void 0 || isUniformNode(value)) return;
777
+ const next = normalizeValue(value);
778
+ if (isTSLNode(next)) return;
779
+ existing2.value = next;
580
780
  }
581
781
  });
582
782
  if (registered[name]) return registered[name];
@@ -586,6 +786,9 @@ function useUniform(name, value) {
586
786
  `[useUniform] Uniform "${name}" not found. Create it first with: useUniform('${name}', initialValue)`
587
787
  );
588
788
  }
789
+ function normalizeValue(value) {
790
+ return typeof value === "string" ? new THREE.Color(value) : vectorize(value);
791
+ }
589
792
  function createNamedUniform(name, value) {
590
793
  let node;
591
794
  if (isUniformNode(value)) {
@@ -626,7 +829,16 @@ function useNodes(creatorOrScope, scope) {
626
829
  isLeaf: isTSLNode,
627
830
  create: () => {
628
831
  if (isReader) return {};
629
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
832
+ const nodes2 = creatorOrScope(
833
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useNodes") })
834
+ );
835
+ if (nodes2 == null) {
836
+ warnOnce(
837
+ "[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."
838
+ );
839
+ return {};
840
+ }
841
+ return nodes2;
630
842
  },
631
843
  prepare: (name, node) => {
632
844
  const setName = Reflect.get(node, "setName");
@@ -646,16 +858,100 @@ function useNodes(creatorOrScope, scope) {
646
858
  function rebuildAllNodes(store, scope) {
647
859
  rebuildResource(store, "nodes", scope);
648
860
  }
649
- function useLocalNodes(creator) {
861
+ function areDepsEqual(next, prev) {
862
+ if (next.length !== prev.length) return false;
863
+ for (let i = 0; i < next.length; i++) if (!Object.is(next[i], prev[i])) return false;
864
+ return true;
865
+ }
866
+ function useDependencyToken(deps) {
867
+ const previous = React.useRef(null);
868
+ const record = previous.current;
869
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && record) {
870
+ const hadDeps = record.deps !== void 0;
871
+ const hasDeps = deps !== void 0;
872
+ if (hadDeps !== hasDeps) {
873
+ console.warn(
874
+ `[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.`
875
+ );
876
+ } else if (hasDeps && record.deps.length !== deps.length) {
877
+ console.warn(
878
+ `[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.`
879
+ );
880
+ }
881
+ }
882
+ if (deps === void 0) {
883
+ previous.current = { deps: void 0, token: {} };
884
+ } else if (!record || record.deps === void 0 || !areDepsEqual(deps, record.deps)) {
885
+ previous.current = { deps, token: {} };
886
+ } else if (record.deps !== deps) {
887
+ previous.current = { deps, token: record.token };
888
+ }
889
+ return previous.current.token;
890
+ }
891
+ function useLocalNodes(creator, deps) {
892
+ const local = extension.useStore();
650
893
  const store = usePrimaryStore();
651
- const uniforms = usePrimaryThree((s) => s.uniforms);
652
- const nodes = usePrimaryThree((s) => s.nodes);
653
- const textures = usePrimaryThree((s) => s.textures);
894
+ const view = React.useMemo(() => createResourceView(store, local), [store, local]);
654
895
  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]);
896
+ const depsToken = useDependencyToken(deps);
897
+ const committedReads = React.useRef(null);
898
+ const readsVersion = React.useRef(0);
899
+ const getReadsVersion = () => {
900
+ const tracker2 = committedReads.current;
901
+ if (tracker2 && !tracker2.stale && isTrackerStale(tracker2, view)) readsVersion.current++;
902
+ return readsVersion.current;
903
+ };
904
+ const subscribe = React.useCallback(
905
+ (onChange) => {
906
+ const unsubscribe = store.subscribe(onChange);
907
+ if (local === store) return unsubscribe;
908
+ const unsubscribeLocal = local.subscribe(onChange);
909
+ return () => {
910
+ unsubscribe();
911
+ unsubscribeLocal();
912
+ };
913
+ },
914
+ [store, local]
915
+ );
916
+ const resourceVersion = React.useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
917
+ const evaluation = React.useMemo(() => {
918
+ const tracker2 = createReadTracker("useLocalNodes");
919
+ const reads = { observe: tracker2.observe, nested: true };
920
+ const value2 = creator(createLazyCreatorState(local.getState(), store, { reads, view }));
921
+ tracker2.closed = true;
922
+ return { value: value2, tracker: tracker2 };
923
+ }, [view, hmrVersion, depsToken, resourceVersion]);
924
+ const { value, tracker } = evaluation;
925
+ const installing = typeof value === "function";
926
+ useModeDiagnostics(value, installing);
927
+ useIsomorphicLayoutEffect(() => {
928
+ committedReads.current = tracker;
929
+ if (!installing) return;
930
+ tracker.closed = false;
931
+ let cleanup;
932
+ try {
933
+ cleanup = value();
934
+ } finally {
935
+ tracker.closed = true;
936
+ }
937
+ return typeof cleanup === "function" ? cleanup : void 0;
938
+ }, [evaluation]);
939
+ return installing ? void 0 : value;
940
+ }
941
+ function useModeDiagnostics(value, installing) {
942
+ const previous = React.useRef(null);
943
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
944
+ if (value === void 0 || value === null) {
945
+ warnOnce(
946
+ "[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."
947
+ );
948
+ }
949
+ if (previous.current !== null && previous.current !== installing) {
950
+ warnOnce(
951
+ "[useLocalNodes] The creator switched between returning a record and returning an install function. Keep one form for a mounted component."
952
+ );
953
+ }
954
+ previous.current = installing;
659
955
  }
660
956
 
661
957
  const disposeBuffer = (buffer) => {
@@ -701,7 +997,9 @@ function useBuffers(creatorOrScope, scope) {
701
997
  isLeaf: isBufferLike,
702
998
  create: () => {
703
999
  if (isReader) return {};
704
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
1000
+ return creatorOrScope(
1001
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useBuffers") })
1002
+ );
705
1003
  },
706
1004
  prepare: (name, buffer) => {
707
1005
  const setName = Reflect.get(buffer, "setName");
@@ -765,7 +1063,9 @@ function useGPUStorage(creatorOrScope, scope) {
765
1063
  isLeaf: isStorageLike,
766
1064
  create: () => {
767
1065
  if (isReader) return {};
768
- return creatorOrScope(createLazyCreatorState(store.getState(), store));
1066
+ return creatorOrScope(
1067
+ createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useGPUStorage") })
1068
+ );
769
1069
  },
770
1070
  prepare: (name, storage) => {
771
1071
  const label = scopedNodeName(scope, name);