@react-three/tsl 10.0.0-canary.975e1e7 → 10.0.0-canary.a9bd42a
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 +74 -18
- package/dist/index.d.mts +74 -18
- package/dist/index.d.ts +74 -18
- package/dist/index.mjs +320 -29
- package/package.json +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
|
@@ -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 };
|
package/dist/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { registerRootExtension, useStore, useThree, setRenderOverride } from '@react-three/fiber/extension';
|
|
1
|
+
import { registerRootExtension, getTextureView, useStore, useThree, setRenderOverride } from '@react-three/fiber/extension';
|
|
2
2
|
import { uniform, pass } from 'three/tsl';
|
|
3
3
|
import * as THREE from 'three/webgpu';
|
|
4
4
|
import { Color } from 'three/webgpu';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
-
import { useMemo, useRef, useCallback, useState, useEffect, useLayoutEffect } from 'react';
|
|
6
|
+
import { useMemo, useRef, useCallback, useSyncExternalStore, useState, useEffect, useLayoutEffect } from 'react';
|
|
7
7
|
import { dequal } from 'dequal/lite';
|
|
8
8
|
import 'zustand/shallow';
|
|
9
9
|
|
|
@@ -297,33 +297,129 @@ function ensureTSLExtension() {
|
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
const SCOPE = Symbol("readTracking.scope");
|
|
301
|
+
const LEAF_GUARDS = {
|
|
302
|
+
uniforms: isUniformNode,
|
|
303
|
+
nodes: isTSLNode,
|
|
304
|
+
buffers: isBufferLike,
|
|
305
|
+
gpuStorage: isStorageLike
|
|
306
|
+
};
|
|
307
|
+
function isScope(kind, value) {
|
|
308
|
+
return !!value && typeof value === "object" && !LEAF_GUARDS[kind](value);
|
|
309
|
+
}
|
|
310
|
+
function classify(kind, value) {
|
|
311
|
+
return kind !== "textures" && isScope(kind, value) ? SCOPE : value;
|
|
312
|
+
}
|
|
313
|
+
function resolveContainer(kind, root, scopePath) {
|
|
314
|
+
let container = root;
|
|
315
|
+
for (const key of scopePath) {
|
|
316
|
+
const next = container?.[key];
|
|
317
|
+
container = isScope(kind, next) ? next : void 0;
|
|
318
|
+
}
|
|
319
|
+
return container;
|
|
320
|
+
}
|
|
321
|
+
function observeNow(read, view) {
|
|
322
|
+
if (read.kind === "textures") {
|
|
323
|
+
const map = view("textures");
|
|
324
|
+
if (read.op === "keys") return [...map.keys()];
|
|
325
|
+
return read.op === "has" ? map.has(read.path[0]) : map.get(read.path[0]);
|
|
326
|
+
}
|
|
327
|
+
const root = view(read.kind);
|
|
328
|
+
if (read.op === "keys") return Object.keys(resolveContainer(read.kind, root, read.path) ?? {});
|
|
329
|
+
const container = resolveContainer(read.kind, root, read.path.slice(0, -1));
|
|
330
|
+
const key = read.path[read.path.length - 1];
|
|
331
|
+
if (read.op === "has") return !!container && key in container;
|
|
332
|
+
return classify(read.kind, container?.[key]);
|
|
333
|
+
}
|
|
334
|
+
function sameKeys(a, b) {
|
|
335
|
+
return a.length === b.length && a.every((key, i) => key === b[i]);
|
|
336
|
+
}
|
|
337
|
+
function hasChanged(read, view) {
|
|
338
|
+
const now = observeNow(read, view);
|
|
339
|
+
return read.op === "keys" ? !sameKeys(read.seen, now) : !Object.is(read.seen, now);
|
|
340
|
+
}
|
|
341
|
+
const warned = /* @__PURE__ */ new Set();
|
|
342
|
+
function warnOnce(message) {
|
|
343
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
344
|
+
if (warned.has(message)) return;
|
|
345
|
+
warned.add(message);
|
|
346
|
+
console.warn(message);
|
|
347
|
+
}
|
|
348
|
+
function describeRead(kind, path) {
|
|
349
|
+
if (kind === "textures") return `textures.get('${path[0]}')`;
|
|
350
|
+
const scopes = path.slice(0, -1).map((key) => `.scope('${key}')`);
|
|
351
|
+
return `${kind}${scopes.join("")}.${path[path.length - 1] ?? ""}`;
|
|
352
|
+
}
|
|
353
|
+
function createReadTracker(hookName) {
|
|
354
|
+
const tracker = {
|
|
355
|
+
reads: [],
|
|
356
|
+
closed: false,
|
|
357
|
+
stale: false,
|
|
358
|
+
observe: (read) => {
|
|
359
|
+
if (tracker.closed) {
|
|
360
|
+
warnOnce(
|
|
361
|
+
`[${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.`
|
|
362
|
+
);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
tracker.reads.push(read);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return tracker;
|
|
369
|
+
}
|
|
370
|
+
function isTrackerStale(tracker, view) {
|
|
371
|
+
if (!tracker.stale) tracker.stale = tracker.reads.some((read) => hasChanged(read, view));
|
|
372
|
+
return tracker.stale;
|
|
373
|
+
}
|
|
374
|
+
function warnMissingReads(hookName) {
|
|
375
|
+
return {
|
|
376
|
+
nested: false,
|
|
377
|
+
observe: (read) => {
|
|
378
|
+
if (read.op !== "get" || read.seen !== void 0) return;
|
|
379
|
+
warnOnce(
|
|
380
|
+
`[${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().`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
300
386
|
var __defProp = Object.defineProperty;
|
|
301
387
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
302
388
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
303
|
-
var _a, _b;
|
|
389
|
+
var _a, _b, _c, _d;
|
|
304
390
|
const INTERNAL_DATA = Symbol("ScopedStore.data");
|
|
305
391
|
const INTERNAL_IS_LEAF = Symbol("ScopedStore.isLeaf");
|
|
306
|
-
|
|
392
|
+
const INTERNAL_READS = Symbol("ScopedStore.reads");
|
|
393
|
+
const INTERNAL_CHILDREN = Symbol("ScopedStore.children");
|
|
394
|
+
_d = INTERNAL_DATA, _c = INTERNAL_IS_LEAF, _b = INTERNAL_READS, _a = INTERNAL_CHILDREN;
|
|
307
395
|
const _ScopedStore = class _ScopedStore {
|
|
308
|
-
constructor(data, isLeaf) {
|
|
396
|
+
constructor(data, isLeaf, reads) {
|
|
397
|
+
/** @internal */
|
|
398
|
+
__publicField(this, _d);
|
|
399
|
+
/** @internal */
|
|
400
|
+
__publicField(this, _c);
|
|
401
|
+
/** @internal */
|
|
309
402
|
__publicField(this, _b);
|
|
403
|
+
/** @internal */
|
|
310
404
|
__publicField(this, _a);
|
|
311
405
|
this[INTERNAL_DATA] = data;
|
|
312
406
|
this[INTERNAL_IS_LEAF] = isLeaf;
|
|
407
|
+
this[INTERNAL_READS] = reads;
|
|
313
408
|
return new Proxy(this, {
|
|
314
409
|
get(target, prop, receiver) {
|
|
315
410
|
if (typeof prop === "string") {
|
|
316
411
|
if (prop === "scope" || prop === "has" || prop === "keys") {
|
|
317
412
|
return Reflect.get(target, prop, receiver);
|
|
318
413
|
}
|
|
319
|
-
return target
|
|
414
|
+
return readEntry(target, prop);
|
|
320
415
|
}
|
|
321
416
|
return Reflect.get(target, prop, receiver);
|
|
322
417
|
},
|
|
323
418
|
has(target, prop) {
|
|
324
|
-
return typeof prop === "string" ? prop
|
|
419
|
+
return typeof prop === "string" ? target.has(prop) : Reflect.has(target, prop);
|
|
325
420
|
},
|
|
326
421
|
ownKeys(target) {
|
|
422
|
+
observeKeys(target);
|
|
327
423
|
return Reflect.ownKeys(target[INTERNAL_DATA]);
|
|
328
424
|
},
|
|
329
425
|
getOwnPropertyDescriptor(target, prop) {
|
|
@@ -345,54 +441,152 @@ const _ScopedStore = class _ScopedStore {
|
|
|
345
441
|
scope(key) {
|
|
346
442
|
const value = this[INTERNAL_DATA][key];
|
|
347
443
|
const isLeaf = this[INTERNAL_IS_LEAF];
|
|
444
|
+
const reads = this[INTERNAL_READS];
|
|
348
445
|
const scope = value && typeof value === "object" && !isLeaf(value) ? value : {};
|
|
349
|
-
|
|
446
|
+
const child = reads ? { ...reads, path: [...reads.path, key] } : void 0;
|
|
447
|
+
return new _ScopedStore(scope, isLeaf, child);
|
|
350
448
|
}
|
|
351
449
|
/**
|
|
352
450
|
* Check if a key exists in the store.
|
|
353
451
|
*/
|
|
354
452
|
has(key) {
|
|
355
|
-
|
|
453
|
+
const found = key in this[INTERNAL_DATA];
|
|
454
|
+
const reads = this[INTERNAL_READS];
|
|
455
|
+
reads?.observe({ kind: reads.kind, op: "has", path: [...reads.path, key], seen: found });
|
|
456
|
+
return found;
|
|
356
457
|
}
|
|
357
458
|
/**
|
|
358
459
|
* Get all keys in the store.
|
|
359
460
|
*/
|
|
360
461
|
keys() {
|
|
462
|
+
observeKeys(this);
|
|
361
463
|
return Object.keys(this[INTERNAL_DATA]);
|
|
362
464
|
}
|
|
363
465
|
};
|
|
364
466
|
let ScopedStore = _ScopedStore;
|
|
467
|
+
function readEntry(target, key) {
|
|
468
|
+
const value = target[INTERNAL_DATA][key];
|
|
469
|
+
const reads = target[INTERNAL_READS];
|
|
470
|
+
if (!reads) return value;
|
|
471
|
+
const isLeaf = target[INTERNAL_IS_LEAF];
|
|
472
|
+
const isScope = !!value && typeof value === "object" && !isLeaf(value);
|
|
473
|
+
const path = [...reads.path, key];
|
|
474
|
+
reads.observe({ kind: reads.kind, op: "get", path, seen: isScope ? SCOPE : value });
|
|
475
|
+
if (!isScope || !reads.nested) return value;
|
|
476
|
+
const children = target[INTERNAL_CHILDREN] ?? (target[INTERNAL_CHILDREN] = /* @__PURE__ */ new Map());
|
|
477
|
+
let child = children.get(key);
|
|
478
|
+
if (!child) {
|
|
479
|
+
child = new ScopedStore(value, isLeaf, { ...reads, path });
|
|
480
|
+
children.set(key, child);
|
|
481
|
+
}
|
|
482
|
+
return child;
|
|
483
|
+
}
|
|
484
|
+
function observeKeys(target) {
|
|
485
|
+
const reads = target[INTERNAL_READS];
|
|
486
|
+
reads?.observe({ kind: reads.kind, op: "keys", path: reads.path, seen: Object.keys(target[INTERNAL_DATA]) });
|
|
487
|
+
}
|
|
365
488
|
function createScopedStore(data, isLeaf) {
|
|
366
489
|
return new ScopedStore(data, isLeaf);
|
|
367
490
|
}
|
|
368
|
-
function
|
|
491
|
+
function observeTextures(map, observe) {
|
|
492
|
+
const observeKeys2 = () => observe({ kind: "textures", op: "keys", path: [], seen: [...map.keys()] });
|
|
493
|
+
const observeAll = () => {
|
|
494
|
+
observeKeys2();
|
|
495
|
+
for (const [url, texture] of map) observe({ kind: "textures", op: "get", path: [url], seen: texture });
|
|
496
|
+
};
|
|
497
|
+
return new Proxy(map, {
|
|
498
|
+
get(target, prop) {
|
|
499
|
+
switch (prop) {
|
|
500
|
+
case "get":
|
|
501
|
+
return (url) => {
|
|
502
|
+
const texture = target.get(url);
|
|
503
|
+
observe({ kind: "textures", op: "get", path: [url], seen: texture });
|
|
504
|
+
return texture;
|
|
505
|
+
};
|
|
506
|
+
case "has":
|
|
507
|
+
return (url) => {
|
|
508
|
+
const found = target.has(url);
|
|
509
|
+
observe({ kind: "textures", op: "has", path: [url], seen: found });
|
|
510
|
+
return found;
|
|
511
|
+
};
|
|
512
|
+
case "size":
|
|
513
|
+
observeKeys2();
|
|
514
|
+
return target.size;
|
|
515
|
+
case "keys":
|
|
516
|
+
return () => {
|
|
517
|
+
observeKeys2();
|
|
518
|
+
return target.keys();
|
|
519
|
+
};
|
|
520
|
+
case "values":
|
|
521
|
+
return () => {
|
|
522
|
+
observeAll();
|
|
523
|
+
return target.values();
|
|
524
|
+
};
|
|
525
|
+
case "entries":
|
|
526
|
+
case Symbol.iterator:
|
|
527
|
+
return () => {
|
|
528
|
+
observeAll();
|
|
529
|
+
return target.entries();
|
|
530
|
+
};
|
|
531
|
+
case "forEach":
|
|
532
|
+
return (callback, thisArg) => {
|
|
533
|
+
observeAll();
|
|
534
|
+
target.forEach(callback, thisArg);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const value = Reflect.get(target, prop, target);
|
|
538
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function createResourceView(primary, local = primary) {
|
|
543
|
+
return ((kind) => kind === "textures" ? getTextureView(local) : withStagedOverlay(primary, kind, primary.getState()[kind]));
|
|
544
|
+
}
|
|
545
|
+
function createLazyCreatorState(state, store, options = {}) {
|
|
546
|
+
const { reads } = options;
|
|
547
|
+
const view = options.view ?? (store ? createResourceView(store) : void 0);
|
|
369
548
|
let _uniforms = null;
|
|
370
549
|
let _nodes = null;
|
|
371
550
|
let _buffers = null;
|
|
372
551
|
let _gpuStorage = null;
|
|
373
|
-
|
|
374
|
-
|
|
552
|
+
let _textures = null;
|
|
553
|
+
const read = (kind) => view ? view(kind) : state[kind];
|
|
554
|
+
const wrap = (kind, isLeaf) => new ScopedStore(
|
|
555
|
+
read(kind),
|
|
556
|
+
isLeaf,
|
|
557
|
+
reads && { ...reads, kind, path: [] }
|
|
558
|
+
);
|
|
559
|
+
const properties = {
|
|
375
560
|
uniforms: {
|
|
376
561
|
get() {
|
|
377
|
-
return _uniforms ?? (_uniforms =
|
|
562
|
+
return _uniforms ?? (_uniforms = wrap("uniforms", isUniformNode));
|
|
378
563
|
}
|
|
379
564
|
},
|
|
380
565
|
nodes: {
|
|
381
566
|
get() {
|
|
382
|
-
return _nodes ?? (_nodes =
|
|
567
|
+
return _nodes ?? (_nodes = wrap("nodes", isTSLNode));
|
|
383
568
|
}
|
|
384
569
|
},
|
|
385
570
|
buffers: {
|
|
386
571
|
get() {
|
|
387
|
-
return _buffers ?? (_buffers =
|
|
572
|
+
return _buffers ?? (_buffers = wrap("buffers", isBufferLike));
|
|
388
573
|
}
|
|
389
574
|
},
|
|
390
575
|
gpuStorage: {
|
|
391
576
|
get() {
|
|
392
|
-
return _gpuStorage ?? (_gpuStorage =
|
|
577
|
+
return _gpuStorage ?? (_gpuStorage = wrap("gpuStorage", isStorageLike));
|
|
393
578
|
}
|
|
394
579
|
}
|
|
395
|
-
}
|
|
580
|
+
};
|
|
581
|
+
if (options.view) {
|
|
582
|
+
properties.textures = {
|
|
583
|
+
get() {
|
|
584
|
+
const textures = options.view("textures");
|
|
585
|
+
return _textures ?? (_textures = reads ? observeTextures(textures, reads.observe) : textures);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return Object.create(state, properties);
|
|
396
590
|
}
|
|
397
591
|
|
|
398
592
|
function resolvePrimary(local) {
|
|
@@ -468,7 +662,7 @@ function useUniforms(creatorOrScope, scope) {
|
|
|
468
662
|
const processedInput = useMemo(() => {
|
|
469
663
|
let raw = creatorOrScope;
|
|
470
664
|
if (typeof creatorOrScope === "function") {
|
|
471
|
-
const wrappedState = createLazyCreatorState(store.getState(), store);
|
|
665
|
+
const wrappedState = createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useUniforms") });
|
|
472
666
|
raw = creatorOrScope(wrappedState);
|
|
473
667
|
}
|
|
474
668
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
@@ -611,7 +805,16 @@ function useNodes(creatorOrScope, scope) {
|
|
|
611
805
|
isLeaf: isTSLNode,
|
|
612
806
|
create: () => {
|
|
613
807
|
if (isReader) return {};
|
|
614
|
-
|
|
808
|
+
const nodes2 = creatorOrScope(
|
|
809
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useNodes") })
|
|
810
|
+
);
|
|
811
|
+
if (nodes2 == null) {
|
|
812
|
+
warnOnce(
|
|
813
|
+
"[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."
|
|
814
|
+
);
|
|
815
|
+
return {};
|
|
816
|
+
}
|
|
817
|
+
return nodes2;
|
|
615
818
|
},
|
|
616
819
|
prepare: (name, node) => {
|
|
617
820
|
const setName = Reflect.get(node, "setName");
|
|
@@ -631,16 +834,100 @@ function useNodes(creatorOrScope, scope) {
|
|
|
631
834
|
function rebuildAllNodes(store, scope) {
|
|
632
835
|
rebuildResource(store, "nodes", scope);
|
|
633
836
|
}
|
|
634
|
-
function
|
|
837
|
+
function areDepsEqual(next, prev) {
|
|
838
|
+
if (next.length !== prev.length) return false;
|
|
839
|
+
for (let i = 0; i < next.length; i++) if (!Object.is(next[i], prev[i])) return false;
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
function useDependencyToken(deps) {
|
|
843
|
+
const previous = useRef(null);
|
|
844
|
+
const record = previous.current;
|
|
845
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production" && record) {
|
|
846
|
+
const hadDeps = record.deps !== void 0;
|
|
847
|
+
const hasDeps = deps !== void 0;
|
|
848
|
+
if (hadDeps !== hasDeps) {
|
|
849
|
+
console.warn(
|
|
850
|
+
`[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.`
|
|
851
|
+
);
|
|
852
|
+
} else if (hasDeps && record.deps.length !== deps.length) {
|
|
853
|
+
console.warn(
|
|
854
|
+
`[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.`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (deps === void 0) {
|
|
859
|
+
previous.current = { deps: void 0, token: {} };
|
|
860
|
+
} else if (!record || record.deps === void 0 || !areDepsEqual(deps, record.deps)) {
|
|
861
|
+
previous.current = { deps, token: {} };
|
|
862
|
+
} else if (record.deps !== deps) {
|
|
863
|
+
previous.current = { deps, token: record.token };
|
|
864
|
+
}
|
|
865
|
+
return previous.current.token;
|
|
866
|
+
}
|
|
867
|
+
function useLocalNodes(creator, deps) {
|
|
868
|
+
const local = useStore();
|
|
635
869
|
const store = usePrimaryStore();
|
|
636
|
-
const
|
|
637
|
-
const nodes = usePrimaryThree((s) => s.nodes);
|
|
638
|
-
const textures = usePrimaryThree((s) => s.textures);
|
|
870
|
+
const view = useMemo(() => createResourceView(store, local), [store, local]);
|
|
639
871
|
const hmrVersion = usePrimaryThree((s) => s._hmrVersion);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
872
|
+
const depsToken = useDependencyToken(deps);
|
|
873
|
+
const committedReads = useRef(null);
|
|
874
|
+
const readsVersion = useRef(0);
|
|
875
|
+
const getReadsVersion = () => {
|
|
876
|
+
const tracker2 = committedReads.current;
|
|
877
|
+
if (tracker2 && !tracker2.stale && isTrackerStale(tracker2, view)) readsVersion.current++;
|
|
878
|
+
return readsVersion.current;
|
|
879
|
+
};
|
|
880
|
+
const subscribe = useCallback(
|
|
881
|
+
(onChange) => {
|
|
882
|
+
const unsubscribe = store.subscribe(onChange);
|
|
883
|
+
if (local === store) return unsubscribe;
|
|
884
|
+
const unsubscribeLocal = local.subscribe(onChange);
|
|
885
|
+
return () => {
|
|
886
|
+
unsubscribe();
|
|
887
|
+
unsubscribeLocal();
|
|
888
|
+
};
|
|
889
|
+
},
|
|
890
|
+
[store, local]
|
|
891
|
+
);
|
|
892
|
+
const resourceVersion = useSyncExternalStore(subscribe, getReadsVersion, getReadsVersion);
|
|
893
|
+
const evaluation = useMemo(() => {
|
|
894
|
+
const tracker2 = createReadTracker("useLocalNodes");
|
|
895
|
+
const reads = { observe: tracker2.observe, nested: true };
|
|
896
|
+
const value2 = creator(createLazyCreatorState(local.getState(), store, { reads, view }));
|
|
897
|
+
tracker2.closed = true;
|
|
898
|
+
return { value: value2, tracker: tracker2 };
|
|
899
|
+
}, [view, hmrVersion, depsToken, resourceVersion]);
|
|
900
|
+
const { value, tracker } = evaluation;
|
|
901
|
+
const installing = typeof value === "function";
|
|
902
|
+
useModeDiagnostics(value, installing);
|
|
903
|
+
useIsomorphicLayoutEffect(() => {
|
|
904
|
+
committedReads.current = tracker;
|
|
905
|
+
if (!installing) return;
|
|
906
|
+
tracker.closed = false;
|
|
907
|
+
let cleanup;
|
|
908
|
+
try {
|
|
909
|
+
cleanup = value();
|
|
910
|
+
} finally {
|
|
911
|
+
tracker.closed = true;
|
|
912
|
+
}
|
|
913
|
+
return typeof cleanup === "function" ? cleanup : void 0;
|
|
914
|
+
}, [evaluation]);
|
|
915
|
+
return installing ? void 0 : value;
|
|
916
|
+
}
|
|
917
|
+
function useModeDiagnostics(value, installing) {
|
|
918
|
+
const previous = useRef(null);
|
|
919
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
|
|
920
|
+
if (value === void 0 || value === null) {
|
|
921
|
+
warnOnce(
|
|
922
|
+
"[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."
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
if (previous.current !== null && previous.current !== installing) {
|
|
926
|
+
warnOnce(
|
|
927
|
+
"[useLocalNodes] The creator switched between returning a record and returning an install function. Keep one form for a mounted component."
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
previous.current = installing;
|
|
644
931
|
}
|
|
645
932
|
|
|
646
933
|
const disposeBuffer = (buffer) => {
|
|
@@ -686,7 +973,9 @@ function useBuffers(creatorOrScope, scope) {
|
|
|
686
973
|
isLeaf: isBufferLike,
|
|
687
974
|
create: () => {
|
|
688
975
|
if (isReader) return {};
|
|
689
|
-
return creatorOrScope(
|
|
976
|
+
return creatorOrScope(
|
|
977
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useBuffers") })
|
|
978
|
+
);
|
|
690
979
|
},
|
|
691
980
|
prepare: (name, buffer) => {
|
|
692
981
|
const setName = Reflect.get(buffer, "setName");
|
|
@@ -750,7 +1039,9 @@ function useGPUStorage(creatorOrScope, scope) {
|
|
|
750
1039
|
isLeaf: isStorageLike,
|
|
751
1040
|
create: () => {
|
|
752
1041
|
if (isReader) return {};
|
|
753
|
-
return creatorOrScope(
|
|
1042
|
+
return creatorOrScope(
|
|
1043
|
+
createLazyCreatorState(store.getState(), store, { reads: warnMissingReads("useGPUStorage") })
|
|
1044
|
+
);
|
|
754
1045
|
},
|
|
755
1046
|
prepare: (name, storage) => {
|
|
756
1047
|
const label = scopedNodeName(scope, name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-three/tsl",
|
|
3
|
-
"version": "10.0.0-canary.
|
|
3
|
+
"version": "10.0.0-canary.a9bd42a",
|
|
4
4
|
"description": "TSL resource hooks for react-three-fiber: uniforms, nodes, buffers, GPU storage and render pipelines on WebGPU",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@react-three/fiber": "^10.0.0-alpha.6",
|
|
52
|
-
"react": ">=19.0 <19.
|
|
52
|
+
"react": ">=19.0 <19.4",
|
|
53
53
|
"three": ">=0.185.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/three": "^0.185.0",
|
|
57
57
|
"@webgpu/types": "^0.1.64",
|
|
58
58
|
"three": ">=0.185.0",
|
|
59
|
-
"@react-three/fiber": "10.0.0-canary.
|
|
59
|
+
"@react-three/fiber": "10.0.0-canary.a9bd42a"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "unbuild",
|