@react-three/tsl 10.0.0-canary.490e4992 → 10.0.0-canary.590ca48
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 +318 -27
- package/dist/index.d.cts +76 -20
- package/dist/index.d.mts +76 -20
- package/dist/index.d.ts +76 -20
- package/dist/index.mjs +320 -29
- package/package.json +3 -3
- package/readme.md +3 -3
package/dist/index.cjs
CHANGED
|
@@ -312,33 +312,129 @@ function ensureTSLExtension() {
|
|
|
312
312
|
});
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
const SCOPE = Symbol("readTracking.scope");
|
|
316
|
+
const LEAF_GUARDS = {
|
|
317
|
+
uniforms: isUniformNode,
|
|
318
|
+
nodes: isTSLNode,
|
|
319
|
+
buffers: isBufferLike,
|
|
320
|
+
gpuStorage: isStorageLike
|
|
321
|
+
};
|
|
322
|
+
function isScope(kind, value) {
|
|
323
|
+
return !!value && typeof value === "object" && !LEAF_GUARDS[kind](value);
|
|
324
|
+
}
|
|
325
|
+
function classify(kind, value) {
|
|
326
|
+
return kind !== "textures" && isScope(kind, value) ? SCOPE : value;
|
|
327
|
+
}
|
|
328
|
+
function resolveContainer(kind, root, scopePath) {
|
|
329
|
+
let container = root;
|
|
330
|
+
for (const key of scopePath) {
|
|
331
|
+
const next = container?.[key];
|
|
332
|
+
container = isScope(kind, next) ? next : void 0;
|
|
333
|
+
}
|
|
334
|
+
return container;
|
|
335
|
+
}
|
|
336
|
+
function observeNow(read, view) {
|
|
337
|
+
if (read.kind === "textures") {
|
|
338
|
+
const map = view("textures");
|
|
339
|
+
if (read.op === "keys") return [...map.keys()];
|
|
340
|
+
return read.op === "has" ? map.has(read.path[0]) : map.get(read.path[0]);
|
|
341
|
+
}
|
|
342
|
+
const root = view(read.kind);
|
|
343
|
+
if (read.op === "keys") return Object.keys(resolveContainer(read.kind, root, read.path) ?? {});
|
|
344
|
+
const container = resolveContainer(read.kind, root, read.path.slice(0, -1));
|
|
345
|
+
const key = read.path[read.path.length - 1];
|
|
346
|
+
if (read.op === "has") return !!container && key in container;
|
|
347
|
+
return classify(read.kind, container?.[key]);
|
|
348
|
+
}
|
|
349
|
+
function sameKeys(a, b) {
|
|
350
|
+
return a.length === b.length && a.every((key, i) => key === b[i]);
|
|
351
|
+
}
|
|
352
|
+
function hasChanged(read, view) {
|
|
353
|
+
const now = observeNow(read, view);
|
|
354
|
+
return read.op === "keys" ? !sameKeys(read.seen, now) : !Object.is(read.seen, now);
|
|
355
|
+
}
|
|
356
|
+
const warned = /* @__PURE__ */ new Set();
|
|
357
|
+
function warnOnce(message) {
|
|
358
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
359
|
+
if (warned.has(message)) return;
|
|
360
|
+
warned.add(message);
|
|
361
|
+
console.warn(message);
|
|
362
|
+
}
|
|
363
|
+
function describeRead(kind, path) {
|
|
364
|
+
if (kind === "textures") return `textures.get('${path[0]}')`;
|
|
365
|
+
const scopes = path.slice(0, -1).map((key) => `.scope('${key}')`);
|
|
366
|
+
return `${kind}${scopes.join("")}.${path[path.length - 1] ?? ""}`;
|
|
367
|
+
}
|
|
368
|
+
function createReadTracker(hookName) {
|
|
369
|
+
const tracker = {
|
|
370
|
+
reads: [],
|
|
371
|
+
closed: false,
|
|
372
|
+
stale: false,
|
|
373
|
+
observe: (read) => {
|
|
374
|
+
if (tracker.closed) {
|
|
375
|
+
warnOnce(
|
|
376
|
+
`[${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.`
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
tracker.reads.push(read);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
return tracker;
|
|
384
|
+
}
|
|
385
|
+
function isTrackerStale(tracker, view) {
|
|
386
|
+
if (!tracker.stale) tracker.stale = tracker.reads.some((read) => hasChanged(read, view));
|
|
387
|
+
return tracker.stale;
|
|
388
|
+
}
|
|
389
|
+
function warnMissingReads(hookName) {
|
|
390
|
+
return {
|
|
391
|
+
nested: false,
|
|
392
|
+
observe: (read) => {
|
|
393
|
+
if (read.op !== "get" || read.seen !== void 0) return;
|
|
394
|
+
warnOnce(
|
|
395
|
+
`[${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().`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
315
401
|
var __defProp = Object.defineProperty;
|
|
316
402
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
317
403
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
318
|
-
var _a, _b;
|
|
404
|
+
var _a, _b, _c, _d;
|
|
319
405
|
const INTERNAL_DATA = Symbol("ScopedStore.data");
|
|
320
406
|
const INTERNAL_IS_LEAF = Symbol("ScopedStore.isLeaf");
|
|
321
|
-
|
|
407
|
+
const INTERNAL_READS = Symbol("ScopedStore.reads");
|
|
408
|
+
const INTERNAL_CHILDREN = Symbol("ScopedStore.children");
|
|
409
|
+
_d = INTERNAL_DATA, _c = INTERNAL_IS_LEAF, _b = INTERNAL_READS, _a = INTERNAL_CHILDREN;
|
|
322
410
|
const _ScopedStore = class _ScopedStore {
|
|
323
|
-
constructor(data, isLeaf) {
|
|
411
|
+
constructor(data, isLeaf, reads) {
|
|
412
|
+
/** @internal */
|
|
413
|
+
__publicField(this, _d);
|
|
414
|
+
/** @internal */
|
|
415
|
+
__publicField(this, _c);
|
|
416
|
+
/** @internal */
|
|
324
417
|
__publicField(this, _b);
|
|
418
|
+
/** @internal */
|
|
325
419
|
__publicField(this, _a);
|
|
326
420
|
this[INTERNAL_DATA] = data;
|
|
327
421
|
this[INTERNAL_IS_LEAF] = isLeaf;
|
|
422
|
+
this[INTERNAL_READS] = reads;
|
|
328
423
|
return new Proxy(this, {
|
|
329
424
|
get(target, prop, receiver) {
|
|
330
425
|
if (typeof prop === "string") {
|
|
331
426
|
if (prop === "scope" || prop === "has" || prop === "keys") {
|
|
332
427
|
return Reflect.get(target, prop, receiver);
|
|
333
428
|
}
|
|
334
|
-
return target
|
|
429
|
+
return readEntry(target, prop);
|
|
335
430
|
}
|
|
336
431
|
return Reflect.get(target, prop, receiver);
|
|
337
432
|
},
|
|
338
433
|
has(target, prop) {
|
|
339
|
-
return typeof prop === "string" ? prop
|
|
434
|
+
return typeof prop === "string" ? target.has(prop) : Reflect.has(target, prop);
|
|
340
435
|
},
|
|
341
436
|
ownKeys(target) {
|
|
437
|
+
observeKeys(target);
|
|
342
438
|
return Reflect.ownKeys(target[INTERNAL_DATA]);
|
|
343
439
|
},
|
|
344
440
|
getOwnPropertyDescriptor(target, prop) {
|
|
@@ -360,54 +456,152 @@ const _ScopedStore = class _ScopedStore {
|
|
|
360
456
|
scope(key) {
|
|
361
457
|
const value = this[INTERNAL_DATA][key];
|
|
362
458
|
const isLeaf = this[INTERNAL_IS_LEAF];
|
|
459
|
+
const reads = this[INTERNAL_READS];
|
|
363
460
|
const scope = value && typeof value === "object" && !isLeaf(value) ? value : {};
|
|
364
|
-
|
|
461
|
+
const child = reads ? { ...reads, path: [...reads.path, key] } : void 0;
|
|
462
|
+
return new _ScopedStore(scope, isLeaf, child);
|
|
365
463
|
}
|
|
366
464
|
/**
|
|
367
465
|
* Check if a key exists in the store.
|
|
368
466
|
*/
|
|
369
467
|
has(key) {
|
|
370
|
-
|
|
468
|
+
const found = key in this[INTERNAL_DATA];
|
|
469
|
+
const reads = this[INTERNAL_READS];
|
|
470
|
+
reads?.observe({ kind: reads.kind, op: "has", path: [...reads.path, key], seen: found });
|
|
471
|
+
return found;
|
|
371
472
|
}
|
|
372
473
|
/**
|
|
373
474
|
* Get all keys in the store.
|
|
374
475
|
*/
|
|
375
476
|
keys() {
|
|
477
|
+
observeKeys(this);
|
|
376
478
|
return Object.keys(this[INTERNAL_DATA]);
|
|
377
479
|
}
|
|
378
480
|
};
|
|
379
481
|
let ScopedStore = _ScopedStore;
|
|
482
|
+
function readEntry(target, key) {
|
|
483
|
+
const value = target[INTERNAL_DATA][key];
|
|
484
|
+
const reads = target[INTERNAL_READS];
|
|
485
|
+
if (!reads) return value;
|
|
486
|
+
const isLeaf = target[INTERNAL_IS_LEAF];
|
|
487
|
+
const isScope = !!value && typeof value === "object" && !isLeaf(value);
|
|
488
|
+
const path = [...reads.path, key];
|
|
489
|
+
reads.observe({ kind: reads.kind, op: "get", path, seen: isScope ? SCOPE : value });
|
|
490
|
+
if (!isScope || !reads.nested) return value;
|
|
491
|
+
const children = target[INTERNAL_CHILDREN] ?? (target[INTERNAL_CHILDREN] = /* @__PURE__ */ new Map());
|
|
492
|
+
let child = children.get(key);
|
|
493
|
+
if (!child) {
|
|
494
|
+
child = new ScopedStore(value, isLeaf, { ...reads, path });
|
|
495
|
+
children.set(key, child);
|
|
496
|
+
}
|
|
497
|
+
return child;
|
|
498
|
+
}
|
|
499
|
+
function observeKeys(target) {
|
|
500
|
+
const reads = target[INTERNAL_READS];
|
|
501
|
+
reads?.observe({ kind: reads.kind, op: "keys", path: reads.path, seen: Object.keys(target[INTERNAL_DATA]) });
|
|
502
|
+
}
|
|
380
503
|
function createScopedStore(data, isLeaf) {
|
|
381
504
|
return new ScopedStore(data, isLeaf);
|
|
382
505
|
}
|
|
383
|
-
function
|
|
506
|
+
function observeTextures(map, observe) {
|
|
507
|
+
const observeKeys2 = () => observe({ kind: "textures", op: "keys", path: [], seen: [...map.keys()] });
|
|
508
|
+
const observeAll = () => {
|
|
509
|
+
observeKeys2();
|
|
510
|
+
for (const [url, texture] of map) observe({ kind: "textures", op: "get", path: [url], seen: texture });
|
|
511
|
+
};
|
|
512
|
+
return new Proxy(map, {
|
|
513
|
+
get(target, prop) {
|
|
514
|
+
switch (prop) {
|
|
515
|
+
case "get":
|
|
516
|
+
return (url) => {
|
|
517
|
+
const texture = target.get(url);
|
|
518
|
+
observe({ kind: "textures", op: "get", path: [url], seen: texture });
|
|
519
|
+
return texture;
|
|
520
|
+
};
|
|
521
|
+
case "has":
|
|
522
|
+
return (url) => {
|
|
523
|
+
const found = target.has(url);
|
|
524
|
+
observe({ kind: "textures", op: "has", path: [url], seen: found });
|
|
525
|
+
return found;
|
|
526
|
+
};
|
|
527
|
+
case "size":
|
|
528
|
+
observeKeys2();
|
|
529
|
+
return target.size;
|
|
530
|
+
case "keys":
|
|
531
|
+
return () => {
|
|
532
|
+
observeKeys2();
|
|
533
|
+
return target.keys();
|
|
534
|
+
};
|
|
535
|
+
case "values":
|
|
536
|
+
return () => {
|
|
537
|
+
observeAll();
|
|
538
|
+
return target.values();
|
|
539
|
+
};
|
|
540
|
+
case "entries":
|
|
541
|
+
case Symbol.iterator:
|
|
542
|
+
return () => {
|
|
543
|
+
observeAll();
|
|
544
|
+
return target.entries();
|
|
545
|
+
};
|
|
546
|
+
case "forEach":
|
|
547
|
+
return (callback, thisArg) => {
|
|
548
|
+
observeAll();
|
|
549
|
+
target.forEach(callback, thisArg);
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const value = Reflect.get(target, prop, target);
|
|
553
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function createResourceView(primary, local = primary) {
|
|
558
|
+
return ((kind) => kind === "textures" ? extension.getTextureView(local) : withStagedOverlay(primary, kind, primary.getState()[kind]));
|
|
559
|
+
}
|
|
560
|
+
function createLazyCreatorState(state, store, options = {}) {
|
|
561
|
+
const { reads } = options;
|
|
562
|
+
const view = options.view ?? (store ? createResourceView(store) : void 0);
|
|
384
563
|
let _uniforms = null;
|
|
385
564
|
let _nodes = null;
|
|
386
565
|
let _buffers = null;
|
|
387
566
|
let _gpuStorage = null;
|
|
388
|
-
|
|
389
|
-
|
|
567
|
+
let _textures = null;
|
|
568
|
+
const read = (kind) => view ? view(kind) : state[kind];
|
|
569
|
+
const wrap = (kind, isLeaf) => new ScopedStore(
|
|
570
|
+
read(kind),
|
|
571
|
+
isLeaf,
|
|
572
|
+
reads && { ...reads, kind, path: [] }
|
|
573
|
+
);
|
|
574
|
+
const properties = {
|
|
390
575
|
uniforms: {
|
|
391
576
|
get() {
|
|
392
|
-
return _uniforms ?? (_uniforms =
|
|
577
|
+
return _uniforms ?? (_uniforms = wrap("uniforms", isUniformNode));
|
|
393
578
|
}
|
|
394
579
|
},
|
|
395
580
|
nodes: {
|
|
396
581
|
get() {
|
|
397
|
-
return _nodes ?? (_nodes =
|
|
582
|
+
return _nodes ?? (_nodes = wrap("nodes", isTSLNode));
|
|
398
583
|
}
|
|
399
584
|
},
|
|
400
585
|
buffers: {
|
|
401
586
|
get() {
|
|
402
|
-
return _buffers ?? (_buffers =
|
|
587
|
+
return _buffers ?? (_buffers = wrap("buffers", isBufferLike));
|
|
403
588
|
}
|
|
404
589
|
},
|
|
405
590
|
gpuStorage: {
|
|
406
591
|
get() {
|
|
407
|
-
return _gpuStorage ?? (_gpuStorage =
|
|
592
|
+
return _gpuStorage ?? (_gpuStorage = wrap("gpuStorage", isStorageLike));
|
|
408
593
|
}
|
|
409
594
|
}
|
|
410
|
-
}
|
|
595
|
+
};
|
|
596
|
+
if (options.view) {
|
|
597
|
+
properties.textures = {
|
|
598
|
+
get() {
|
|
599
|
+
const textures = options.view("textures");
|
|
600
|
+
return _textures ?? (_textures = reads ? observeTextures(textures, reads.observe) : textures);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return Object.create(state, properties);
|
|
411
605
|
}
|
|
412
606
|
|
|
413
607
|
function resolvePrimary(local) {
|
|
@@ -483,7 +677,7 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
483
677
|
const processedInput = React.useMemo(() => {
|
|
484
678
|
let raw = creatorOrScope;
|
|
485
679
|
if (typeof creatorOrScope === "function") {
|
|
486
|
-
const wrappedState = createLazyCreatorState(store.getState(), store);
|
|
680
|
+
const wrappedState = createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useUniforms") });
|
|
487
681
|
raw = creatorOrScope(wrappedState);
|
|
488
682
|
}
|
|
489
683
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
@@ -626,7 +820,16 @@ function useNodes(creatorOrScope, scope) {
|
|
|
626
820
|
isLeaf: isTSLNode,
|
|
627
821
|
create: () => {
|
|
628
822
|
if (isReader) return {};
|
|
629
|
-
|
|
823
|
+
const nodes2 = creatorOrScope(
|
|
824
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useNodes") })
|
|
825
|
+
);
|
|
826
|
+
if (nodes2 == null) {
|
|
827
|
+
warnOnce(
|
|
828
|
+
"[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."
|
|
829
|
+
);
|
|
830
|
+
return {};
|
|
831
|
+
}
|
|
832
|
+
return nodes2;
|
|
630
833
|
},
|
|
631
834
|
prepare: (name, node) => {
|
|
632
835
|
const setName = Reflect.get(node, "setName");
|
|
@@ -646,16 +849,100 @@ function useNodes(creatorOrScope, scope) {
|
|
|
646
849
|
function rebuildAllNodes(store, scope) {
|
|
647
850
|
rebuildResource(store, "nodes", scope);
|
|
648
851
|
}
|
|
649
|
-
function
|
|
852
|
+
function areDepsEqual(next, prev) {
|
|
853
|
+
if (next.length !== prev.length) return false;
|
|
854
|
+
for (let i = 0; i < next.length; i++) if (!Object.is(next[i], prev[i])) return false;
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
function useDependencyToken(deps) {
|
|
858
|
+
const previous = React.useRef(null);
|
|
859
|
+
const record = previous.current;
|
|
860
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && record) {
|
|
861
|
+
const hadDeps = record.deps !== void 0;
|
|
862
|
+
const hasDeps = deps !== void 0;
|
|
863
|
+
if (hadDeps !== hasDeps) {
|
|
864
|
+
console.warn(
|
|
865
|
+
`[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.`
|
|
866
|
+
);
|
|
867
|
+
} else if (hasDeps && record.deps.length !== deps.length) {
|
|
868
|
+
console.warn(
|
|
869
|
+
`[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.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (deps === void 0) {
|
|
874
|
+
previous.current = { deps: void 0, token: {} };
|
|
875
|
+
} else if (!record || record.deps === void 0 || !areDepsEqual(deps, record.deps)) {
|
|
876
|
+
previous.current = { deps, token: {} };
|
|
877
|
+
} else if (record.deps !== deps) {
|
|
878
|
+
previous.current = { deps, token: record.token };
|
|
879
|
+
}
|
|
880
|
+
return previous.current.token;
|
|
881
|
+
}
|
|
882
|
+
function useLocalNodes(creator, deps) {
|
|
883
|
+
const local = extension.useStore();
|
|
650
884
|
const store = usePrimaryStore();
|
|
651
|
-
const
|
|
652
|
-
const nodes = usePrimaryThree((s) => s.nodes);
|
|
653
|
-
const textures = usePrimaryThree((s) => s.textures);
|
|
885
|
+
const view = React.useMemo(() => createResourceView(store, local), [store, local]);
|
|
654
886
|
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
887
|
+
const depsToken = useDependencyToken(deps);
|
|
888
|
+
const committedReads = React.useRef(null);
|
|
889
|
+
const readsVersion = React.useRef(0);
|
|
890
|
+
const getReadsVersion = () => {
|
|
891
|
+
const tracker2 = committedReads.current;
|
|
892
|
+
if (tracker2 && !tracker2.stale && isTrackerStale(tracker2, view)) readsVersion.current++;
|
|
893
|
+
return readsVersion.current;
|
|
894
|
+
};
|
|
895
|
+
const subscribe = React.useCallback(
|
|
896
|
+
(onChange) => {
|
|
897
|
+
const unsubscribe = store.subscribe(onChange);
|
|
898
|
+
if (local === store) return unsubscribe;
|
|
899
|
+
const unsubscribeLocal = local.subscribe(onChange);
|
|
900
|
+
return () => {
|
|
901
|
+
unsubscribe();
|
|
902
|
+
unsubscribeLocal();
|
|
903
|
+
};
|
|
904
|
+
},
|
|
905
|
+
[store, local]
|
|
906
|
+
);
|
|
907
|
+
const resourceVersion = React.useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
|
|
908
|
+
const evaluation = React.useMemo(() => {
|
|
909
|
+
const tracker2 = createReadTracker("useLocalNodes");
|
|
910
|
+
const reads = { observe: tracker2.observe, nested: true };
|
|
911
|
+
const value2 = creator(createLazyCreatorState(local.getState(), store, { reads, view }));
|
|
912
|
+
tracker2.closed = true;
|
|
913
|
+
return { value: value2, tracker: tracker2 };
|
|
914
|
+
}, [view, hmrVersion, depsToken, resourceVersion]);
|
|
915
|
+
const { value, tracker } = evaluation;
|
|
916
|
+
const installing = typeof value === "function";
|
|
917
|
+
useModeDiagnostics(value, installing);
|
|
918
|
+
useIsomorphicLayoutEffect(() => {
|
|
919
|
+
committedReads.current = tracker;
|
|
920
|
+
if (!installing) return;
|
|
921
|
+
tracker.closed = false;
|
|
922
|
+
let cleanup;
|
|
923
|
+
try {
|
|
924
|
+
cleanup = value();
|
|
925
|
+
} finally {
|
|
926
|
+
tracker.closed = true;
|
|
927
|
+
}
|
|
928
|
+
return typeof cleanup === "function" ? cleanup : void 0;
|
|
929
|
+
}, [evaluation]);
|
|
930
|
+
return installing ? void 0 : value;
|
|
931
|
+
}
|
|
932
|
+
function useModeDiagnostics(value, installing) {
|
|
933
|
+
const previous = React.useRef(null);
|
|
934
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
935
|
+
if (value === void 0 || value === null) {
|
|
936
|
+
warnOnce(
|
|
937
|
+
"[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."
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (previous.current !== null && previous.current !== installing) {
|
|
941
|
+
warnOnce(
|
|
942
|
+
"[useLocalNodes] The creator switched between returning a record and returning an install function. Keep one form for a mounted component."
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
previous.current = installing;
|
|
659
946
|
}
|
|
660
947
|
|
|
661
948
|
const disposeBuffer = (buffer) => {
|
|
@@ -701,7 +988,9 @@ function useBuffers(creatorOrScope, scope) {
|
|
|
701
988
|
isLeaf: isBufferLike,
|
|
702
989
|
create: () => {
|
|
703
990
|
if (isReader) return {};
|
|
704
|
-
return creatorOrScope(
|
|
991
|
+
return creatorOrScope(
|
|
992
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useBuffers") })
|
|
993
|
+
);
|
|
705
994
|
},
|
|
706
995
|
prepare: (name, buffer) => {
|
|
707
996
|
const setName = Reflect.get(buffer, "setName");
|
|
@@ -765,7 +1054,9 @@ function useGPUStorage(creatorOrScope, scope) {
|
|
|
765
1054
|
isLeaf: isStorageLike,
|
|
766
1055
|
create: () => {
|
|
767
1056
|
if (isReader) return {};
|
|
768
|
-
return creatorOrScope(
|
|
1057
|
+
return creatorOrScope(
|
|
1058
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useGPUStorage") })
|
|
1059
|
+
);
|
|
769
1060
|
},
|
|
770
1061
|
prepare: (name, storage) => {
|
|
771
1062
|
const label = scopedNodeName(scope, name);
|
package/dist/index.d.cts
CHANGED
|
@@ -112,8 +112,8 @@ interface TSLRootState {
|
|
|
112
112
|
passes?: PassRecord
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
115
|
+
// Every fiber entry shares one core, and so one RootState: augmenting it through the root entry
|
|
116
|
+
// reaches /legacy, /webgpu and /extension as well.
|
|
117
117
|
declare module '@react-three/fiber' {
|
|
118
118
|
interface RootState extends TSLRootState {}
|
|
119
119
|
}
|
|
@@ -381,10 +381,17 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
|
|
|
381
381
|
*
|
|
382
382
|
* @example
|
|
383
383
|
* ```tsx
|
|
384
|
+
* // With uniforms registered (see `Register` and the Typed Uniforms guide), reads are typed by name
|
|
384
385
|
* useLocalNodes(({ uniforms }) => ({
|
|
385
|
-
* wobble: sin(uniforms.uTime.mul(2)),
|
|
386
|
-
* playerHealth: uniforms.scope('player').uHealth //
|
|
387
|
-
* }))
|
|
386
|
+
* wobble: sin(uniforms.uTime.mul(2)),
|
|
387
|
+
* playerHealth: uniforms.scope('player').uHealth, // or uniforms.player.uHealth
|
|
388
|
+
* }), [])
|
|
389
|
+
*
|
|
390
|
+
* // Without registration, give a scope its schema
|
|
391
|
+
* useLocalNodes(({ uniforms }) => {
|
|
392
|
+
* const player = uniforms.scope<{ uHealth: UniformNode<'float', number> }>('player')
|
|
393
|
+
* return { damage: player.uHealth.mul(2) }
|
|
394
|
+
* }, [])
|
|
388
395
|
* ```
|
|
389
396
|
*/
|
|
390
397
|
|
|
@@ -619,32 +626,81 @@ declare function rebuildAllNodes(store: ReturnType<typeof useStore>, scope?: str
|
|
|
619
626
|
/** Creator receives CreatorState with ScopedStore wrappers for type-safe access. Returns any record. */
|
|
620
627
|
type LocalNodeCreator<T extends Record<string, unknown>> = (state: CreatorState) => T;
|
|
621
628
|
/**
|
|
622
|
-
*
|
|
629
|
+
* The install step an install-form creator returns: it runs after commit (as a layout effect) and
|
|
630
|
+
* may return a cleanup, which runs before the next install and on unmount.
|
|
631
|
+
*/
|
|
632
|
+
type LocalNodeInstall = () => void | (() => void);
|
|
633
|
+
/** A creator that builds during render and returns an install step instead of a record. */
|
|
634
|
+
type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
|
|
635
|
+
/**
|
|
636
|
+
* Creates component-local values from the rendering context and the shared TSL resources.
|
|
637
|
+
*
|
|
638
|
+
* Unlike `useNodes`, this does NOT register to the global store — nothing is published during
|
|
639
|
+
* render or commit. The creator runs in the render phase and is pure computation.
|
|
640
|
+
*
|
|
641
|
+
* **When the creator re-runs** is controlled by the optional `deps` array, mirroring `useMemo`:
|
|
642
|
+
*
|
|
643
|
+
* | Call | Ordinary component renders |
|
|
644
|
+
* | -------------------------------- | ----------------------------------------------------------- |
|
|
645
|
+
* | `useLocalNodes(creator)` | Re-evaluate every render, even with a `useCallback` creator |
|
|
646
|
+
* | `useLocalNodes(creator, [])` | Reuse the result |
|
|
647
|
+
* | `useLocalNodes(creator, [a, b])` | Reuse until a declared dependency changes by `Object.is` |
|
|
648
|
+
*
|
|
649
|
+
* Independently of `deps`, three things re-run the creator: a change to a shared resource the
|
|
650
|
+
* creator READ (replaced, removed, or appearing where it read nothing), a change of the owning
|
|
651
|
+
* (primary) store, and an HMR / `rebuild*` invalidation. Registrations the creator did not read
|
|
652
|
+
* change nothing, and writing `.value` on a uniform it read is not a change. `[]` therefore means
|
|
653
|
+
* "no JavaScript construction inputs", not "never rebuild". Whenever it re-runs, the creator from
|
|
654
|
+
* the CURRENT render is used; creator identity itself is never a rebuild trigger once an array is
|
|
655
|
+
* supplied.
|
|
623
656
|
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
657
|
+
* Only reads the creator makes before it returns are tracked. Inside `Fn(() => …)` the body runs
|
|
658
|
+
* later, while three builds the shader, so read the resource in the creator and close over it.
|
|
659
|
+
*
|
|
660
|
+
* `[]` is the normal case. A value that changes (a color prop, a slider) belongs in a uniform: the
|
|
661
|
+
* graph references the `UniformNode`, so updating its `.value` needs no rebuild and must not be a
|
|
662
|
+
* dependency. Declare only inputs that decide the graph's structure and cannot be uniforms: which
|
|
663
|
+
* node to use, a loop count, whether a branch exists.
|
|
664
|
+
*
|
|
665
|
+
* **Install form.** To put a node onto a Three object (`scene.fogNode`, `scene.backgroundNode`),
|
|
666
|
+
* return a function instead of a record. The creator still builds during render; the returned
|
|
667
|
+
* function runs after commit and may return a cleanup, which runs before the next install and on
|
|
668
|
+
* unmount. Mutating a Three object inside the creator itself is unsafe: React can discard a
|
|
669
|
+
* render, and StrictMode renders twice. The hook returns nothing in this form.
|
|
670
|
+
*
|
|
671
|
+
* ```tsx
|
|
672
|
+
* useLocalNodes(({ scene, uniforms }) => {
|
|
673
|
+
* const fogNode = fog(uniforms.fogColor, rangeFogFactor(uniforms.near, uniforms.far))
|
|
674
|
+
* return () => {
|
|
675
|
+
* scene.fogNode = fogNode
|
|
676
|
+
* return () => { scene.fogNode = null }
|
|
677
|
+
* }
|
|
678
|
+
* }, [])
|
|
679
|
+
* ```
|
|
626
680
|
*
|
|
627
681
|
* @example
|
|
628
682
|
* ```tsx
|
|
629
|
-
* //
|
|
683
|
+
* // Resource-driven composition: no surrounding JS inputs. `uniforms.uTime` is typed when the
|
|
684
|
+
* // app registers its uniforms (see `Register`); otherwise give a scope its schema or cast.
|
|
630
685
|
* const { wobble, uTime } = useLocalNodes(({ uniforms, nodes }) => ({
|
|
631
686
|
* wobble: sin(uniforms.uTime.mul(2)),
|
|
632
|
-
* uTime: uniforms.uTime,
|
|
633
|
-
* }))
|
|
687
|
+
* uTime: uniforms.uTime, // can return uniforms too
|
|
688
|
+
* }), [])
|
|
634
689
|
*
|
|
635
|
-
* //
|
|
636
|
-
* const {
|
|
637
|
-
*
|
|
638
|
-
* }))
|
|
690
|
+
* // `pattern` picks which node the graph is built from: a structural input.
|
|
691
|
+
* const { result } = useLocalNodes(({ nodes }) => ({
|
|
692
|
+
* result: pattern === 'noise' ? nodes.noise : nodes.stripes,
|
|
693
|
+
* }), [pattern])
|
|
639
694
|
*
|
|
640
|
-
* //
|
|
695
|
+
* // An unregistered uniform, cast to its type
|
|
641
696
|
* const { colorNode } = useLocalNodes(({ uniforms }) => {
|
|
642
|
-
* const uValue = uniforms.myUniform as UniformNode<number>
|
|
697
|
+
* const uValue = uniforms.myUniform as UniformNode<'float', number>
|
|
643
698
|
* return { colorNode: mix(colorA, colorB, uValue) }
|
|
644
|
-
* })
|
|
699
|
+
* }, [])
|
|
645
700
|
* ```
|
|
646
701
|
*/
|
|
647
|
-
declare function useLocalNodes
|
|
702
|
+
declare function useLocalNodes(creator: LocalNodeInstaller, deps?: React.DependencyList): void;
|
|
703
|
+
declare function useLocalNodes<T extends Record<string, unknown>>(creator: LocalNodeCreator<T>, deps?: React.DependencyList): T;
|
|
648
704
|
|
|
649
705
|
/**
|
|
650
706
|
* A record of buffer-like values - allows mixed types (TypedArrays, BufferAttributes, TSL nodes)
|
|
@@ -774,4 +830,4 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
|
|
|
774
830
|
declare function useRenderPipeline(mainCB?: RenderPipelineMainCallback, setupCB?: RenderPipelineSetupCallback): UseRenderPipelineReturn;
|
|
775
831
|
|
|
776
832
|
export { configureTSL, createScopedStore, rebuildAllBuffers, rebuildAllNodes, rebuildAllStorage, rebuildAllUniforms, useBuffers, useGPUStorage, useLocalNodes, useNodes, useRenderPipeline, useUniform, useUniforms };
|
|
777
|
-
export type { AppUniforms, BufferCreator, BufferLike, BufferRecord, BufferStore, BuffersWithUtils, ClearBuffersFn, ClearNodesFn, ClearStorageFn, ClearUniformsFn, CreatorState, DisposeBuffersFn, DisposeStorageFn, LocalNodeCreator, NodeCreator, NodeLike, NodeRecord, NodeStore, NodesWithUtils, RebuildBuffersFn, RebuildNodesFn, RebuildStorageFn, RebuildUniformsFn, Register, RegisteredScopeUniforms, RegisteredScopes, RegisteredUniform, RegisteredUniforms, RemoveBuffersFn, RemoveNodesFn, RemoveStorageFn, RemoveUniformsFn, RootUniformInput, ScopeUniformInput, ScopedStoreType, StorageCreator, StorageLike, StorageRecord, StorageStore, StorageWithUtils, TSLConfig, TSLNode, TSLNodeLike, TSLRootState, UniformCreator, UniformValue, UniformsWithUtils };
|
|
833
|
+
export type { AppUniforms, BufferCreator, BufferLike, BufferRecord, BufferStore, BuffersWithUtils, ClearBuffersFn, ClearNodesFn, ClearStorageFn, ClearUniformsFn, CreatorState, DisposeBuffersFn, DisposeStorageFn, LocalNodeCreator, LocalNodeInstall, LocalNodeInstaller, NodeCreator, NodeLike, NodeRecord, NodeStore, NodesWithUtils, RebuildBuffersFn, RebuildNodesFn, RebuildStorageFn, RebuildUniformsFn, Register, RegisteredScopeUniforms, RegisteredScopes, RegisteredUniform, RegisteredUniforms, RemoveBuffersFn, RemoveNodesFn, RemoveStorageFn, RemoveUniformsFn, RootUniformInput, ScopeUniformInput, ScopedStoreType, StorageCreator, StorageLike, StorageRecord, StorageStore, StorageWithUtils, TSLConfig, TSLNode, TSLNodeLike, TSLRootState, UniformCreator, UniformValue, UniformsWithUtils };
|