@react-three/tsl 10.0.0-canary.87166dc → 10.0.0-canary.908b65b

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.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { RootState, useStore } from '@react-three/fiber/extension';
2
2
  import * as three_webgpu from 'three/webgpu';
3
3
  import { Node, StorageTexture, Storage3DTexture, StorageArrayTexture, Data3DTexture, RenderPipeline, Vector2, Vector3, Vector4, Color, Matrix2, Matrix3, Matrix4 } from 'three/webgpu';
4
+ import { Fn } from 'three/tsl';
4
5
 
5
6
  //* Buffer Types (useBuffers) ========================================
6
7
 
@@ -57,6 +58,40 @@ type NodeRecord<T extends NodeLike = NodeLike> = Record<string, T>
57
58
  */
58
59
  type NodeStore = Record<string, NodeLike | NodeRecord>
59
60
 
61
+ /** What `Fn()` returns (its members are what matters here, not its signature). */
62
+ type FnNodeLike = ReturnType<typeof Fn>
63
+
64
+ /**
65
+ * What an unregistered node read is: `useNodes().wobble`, `useNodes('fx').blur`, a creator's
66
+ * `nodes.wobble`, `state.nodes.wobble` in `useFrame`.
67
+ *
68
+ * Why it is loose: the read goes through a string name, and TypeScript cannot know what the entry
69
+ * under that name holds: a float, a vec3, a texture sample, or an `Fn()` to call. three's types
70
+ * only give operator methods to a node whose node type is known, and only an `Fn()` can be called,
71
+ * so the broad `NodeLike` allowed neither and every read needed a cast.
72
+ *
73
+ * So this has every member that any of three's nodes or an `Fn()` has, each typed `any`, and can
74
+ * be called. `nodes.wobble.mul(2)`, `material.colorNode = nodes.wobble` and `nodes.rimLightFn()`
75
+ * compile, and a misspelt member is still an error. It is a deliberate simplification: results of
76
+ * its own methods and calls are `any`.
77
+ *
78
+ * A single `as` gives it an exact type: `nodes.wobble as Node<'vec3'>`, `nodes.fn as typeof fn`.
79
+ * For exact types without casts, register the nodes (`nodes` / `nodeScopes` in `Register`) or pass
80
+ * a schema to the reader: `useNodes<{ blur: Node<'vec4'> }>('fx')`, `nodes.scope<Schema>('fx')`.
81
+ */
82
+ interface LooseNode extends TSLLooseMembers<Node<TSLNodeTypeName> | FnNodeLike> {
83
+ (...args: any[]): any
84
+ }
85
+
86
+ /**
87
+ * A root entry of an unregistered node map: a node (its members) or a scope of nodes (any other
88
+ * name), since the type cannot tell which a name holds.
89
+ */
90
+ type LooseNodeEntry = LooseNode & { readonly [name: string]: LooseNode }
91
+
92
+ /** An unregistered node map read: `state.nodes`, `useNodes()`. */
93
+ type LooseNodeStore = Record<string, LooseNodeEntry>
94
+
60
95
  //* Storage Types (useGPUStorage) ========================================
61
96
 
62
97
  /**
@@ -93,17 +128,23 @@ type StorageStore = Record<string, StorageLike | StorageRecord>
93
128
  */
94
129
  interface TSLRootState {
95
130
  /**
96
- * TSL uniform nodes - root-level uniforms + scoped sub-objects. Use useUniforms(). Typed by name
97
- * once you register them (see `Register`), so `useFrame(({ uniforms }) => uniforms.uTime.value)`
98
- * needs no cast.
131
+ * TSL uniform nodes - root-level uniforms + scoped sub-objects. Use useUniforms(). Each read is a
132
+ * `LooseUniformNode`, usable without casts, or typed by name once you register them (see
133
+ * `Register`), so `useFrame(({ uniforms }) => uniforms.uTime.value)` needs no cast either way.
99
134
  */
100
135
  uniforms: AppUniforms
101
- /** TSL nodes - root-level nodes + scoped sub-objects. Use useNodes() */
102
- nodes: NodeStore
103
- /** Buffers - root-level buffers + scoped sub-objects. Use useBuffers() */
104
- buffers: BufferStore
105
- /** GPU storage (textures, etc.) - root-level storage + scoped sub-objects. Use useGPUStorage() */
106
- gpuStorage: StorageStore
136
+ /**
137
+ * TSL nodes - root-level nodes + scoped sub-objects. Use useNodes(). Each read is a `LooseNode`,
138
+ * usable without casts, or typed by name once you register your nodes (see `Register`).
139
+ */
140
+ nodes: AppNodes
141
+ /** Buffers - root-level buffers + scoped sub-objects. Use useBuffers(). Typed by name once registered */
142
+ buffers: AppBuffers
143
+ /**
144
+ * GPU storage (textures, etc.) - root-level storage + scoped sub-objects. Use useGPUStorage().
145
+ * Typed by name once registered
146
+ */
147
+ gpuStorage: AppGPUStorage
107
148
  /** Internal: bumped by hot reloads and rebuilds to re-run creators */
108
149
  _hmrVersion: number
109
150
  /** The canvas's RenderPipeline, once useRenderPipeline created one. Per canvas, not shared. */
@@ -233,6 +274,69 @@ declare global {
233
274
  */
234
275
  type UniformStore = Record<string, UniformNode | UniformRecord>
235
276
 
277
+ //* Loose reads ==============================
278
+
279
+ /** Every shader node type three's TSL declarations type separately. */
280
+ type TSLNodeTypeName =
281
+ | 'float'
282
+ | 'int'
283
+ | 'uint'
284
+ | 'bool'
285
+ | 'vec2'
286
+ | 'vec3'
287
+ | 'vec4'
288
+ | 'ivec2'
289
+ | 'ivec3'
290
+ | 'ivec4'
291
+ | 'uvec2'
292
+ | 'uvec3'
293
+ | 'uvec4'
294
+ | 'bvec2'
295
+ | 'bvec3'
296
+ | 'bvec4'
297
+ | 'color'
298
+ | 'mat2'
299
+ | 'mat3'
300
+ | 'mat4'
301
+
302
+ /** Every member of every constituent of `T`, each typed `any`. */
303
+ type TSLLooseMembers<T> = { [K in T extends unknown ? keyof T : never]: any }
304
+
305
+ /**
306
+ * What an unregistered uniform read is: `useUniforms().uTime`, `useUniforms('fx').blur`,
307
+ * `useUniform('uTime')`, a creator's `uniforms.uTime`, `state.uniforms.uTime` in `useFrame`.
308
+ *
309
+ * Why it is loose: the read goes through a string name, and TypeScript cannot know what a
310
+ * runtime entry under that name holds. three's types only give the operator methods (`.mul`,
311
+ * `.add`, swizzles, `.toVar()`, ...) to a node whose node type is known, so `UniformNode<unknown>`
312
+ * had none of them and every read needed a cast.
313
+ *
314
+ * So this has every member that any of three's uniform nodes has (float, vec3, color, mat4, ...),
315
+ * each typed `any`. Every operator, TSL function and material slot accepts it, and a misspelt member
316
+ * (`.valeu`) is still an error. It is a deliberate simplification: results of its own methods are
317
+ * `any`, and nothing checks that `cross()` gets a vec3.
318
+ *
319
+ * It converts to an exact type with a single `as`, since every member is `any`:
320
+ * `uniforms.uColor as UniformNode<'color', Color>`, or `as Node<'vec3'>`. A node typed as a float
321
+ * uniform would cover the operators with exact result types, but three's node types are not
322
+ * comparable to one another, so it could only be cast through `unknown`.
323
+ *
324
+ * Exact types without casts: register the uniforms (`Register` in `@react-three/tsl`, which also
325
+ * catches typos in the name), or pass a schema to the reader (`useUniforms<{ blur: number }>('fx')`,
326
+ * `useUniform<Color>('uColor')`, `uniforms.scope<{ uHealth: UniformNode<'float', number> }>('player')`).
327
+ */
328
+ interface LooseUniformNode extends TSLLooseMembers<three_webgpu.UniformNode<TSLNodeTypeName, any>> {}
329
+
330
+ /**
331
+ * A root entry of an unregistered uniform map: a uniform (its members) or a scope of uniforms (any
332
+ * other name), since the type cannot tell which a name holds. `uniforms.uTime.mul(2)` and
333
+ * `uniforms.player.uHealth` both read.
334
+ */
335
+ type LooseUniformEntry = LooseUniformNode & { readonly [name: string]: LooseUniformNode }
336
+
337
+ /** An unregistered uniform map read: `state.uniforms`, `useUniforms()`. */
338
+ type LooseUniformStore = Record<string, LooseUniformEntry>
339
+
236
340
  /**
237
341
  * Helper to safely access a uniform node from the store.
238
342
  * Use this when accessing state.uniforms to get proper typing.
@@ -395,13 +499,14 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
395
499
  *
396
500
  * @example
397
501
  * ```tsx
398
- * // With uniforms registered (see `Register` and the Typed Uniforms guide), reads are typed by name
502
+ * // With uniforms registered (see `Register` and the Typed Resources guide), reads are typed by name
399
503
  * useLocalNodes(({ uniforms }) => ({
400
504
  * wobble: sin(uniforms.uTime.mul(2)),
401
505
  * playerHealth: uniforms.scope('player').uHealth, // or uniforms.player.uHealth
402
506
  * }), [])
403
507
  *
404
- * // Without registration, give a scope its schema
508
+ * // Without registration a read is loose: TSL takes it as is. A scope schema makes it exact.
509
+ * useLocalNodes(({ uniforms }) => ({ wobble: sin(uniforms.uTime.mul(2)) }), [])
405
510
  * useLocalNodes(({ uniforms }) => {
406
511
  * const player = uniforms.scope<{ uHealth: UniformNode<'float', number> }>('player')
407
512
  * return { damage: player.uHealth.mul(2) }
@@ -414,16 +519,19 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
414
519
  * Property access returns `T` (assumes leaf node).
415
520
  * Use `.scope(key)` for nested object access.
416
521
  *
522
+ * `TLeaf` is what a scope schema must hold (`scope<{ uHealth: UniformNode<'float', number> }>()`),
523
+ * `TRead` what a leaf reads as without one. They differ for uniforms and nodes, whose unregistered
524
+ * reads are the loose `LooseUniformNode` / `LooseNode` (see those types for why).
417
525
  */
418
- type ScopedStoreMethods<TLeaf> = {
526
+ type ScopedStoreMethods<TLeaf, TRead extends TLeaf = TLeaf> = {
419
527
  /** Access a nested scope by key. Returns empty wrapper if scope doesn't exist. */
420
- scope<TScope extends Record<string, TLeaf> = Record<string, TLeaf>>(key: string): ScopedStoreType<TLeaf, TScope>;
528
+ scope<TScope extends Record<string, TLeaf> = Record<string, TRead>>(key: string): ScopedStoreType<TLeaf, TScope, TRead>;
421
529
  /** Check if a key exists in the store */
422
530
  has(key: string): boolean;
423
531
  /** Get all keys in the store */
424
532
  keys(): string[];
425
533
  };
426
- type ScopedStoreType<TLeaf, TEntries extends Record<string, TLeaf> = Record<string, TLeaf>> = Readonly<TEntries> & ScopedStoreMethods<TLeaf>;
534
+ type ScopedStoreType<TLeaf, TEntries extends Record<string, TLeaf> = Record<string, TLeaf>, TRead extends TLeaf = TLeaf> = Readonly<TEntries> & ScopedStoreMethods<TLeaf, TRead>;
427
535
  interface ScopedStoreData<TLeaf> {
428
536
  [key: string]: TLeaf | ScopedStoreData<TLeaf>;
429
537
  }
@@ -440,18 +548,18 @@ declare function createScopedStore<TLeaf>(data: ScopedStoreData<TLeaf>, isLeaf:
440
548
  type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStorage'> & {
441
549
  /** Type-safe uniform access - property access returns UniformNode, typed by name when registered */
442
550
  uniforms: CreatorUniforms;
443
- /** Type-safe node access for real, callable, and legacy structural nodes */
444
- nodes: ScopedStoreType<NodeLike>;
445
- /** Type-safe buffer access - property access returns BufferLike (TypedArrays, BufferAttributes, TSL nodes) */
446
- buffers: ScopedStoreType<BufferLike>;
447
- /** Type-safe GPU storage access - property access returns StorageLike (StorageTexture, TSL nodes) */
448
- gpuStorage: ScopedStoreType<StorageLike>;
551
+ /** Node access: each read a `LooseNode` (callable, every operator), typed by name when registered */
552
+ nodes: CreatorNodes;
553
+ /** Buffer access: typed by name when registered, BufferLike (typed arrays, attributes, nodes) otherwise */
554
+ buffers: CreatorBuffers;
555
+ /** GPU storage access: typed by name when registered, StorageLike (storage textures, nodes) otherwise */
556
+ gpuStorage: CreatorGPUStorage;
449
557
  };
450
558
 
451
559
  /**
452
- * @fileoverview Typed uniforms by registration (the TanStack Router `Register` pattern).
560
+ * @fileoverview Typed TSL resources by registration (the TanStack Router `Register` pattern).
453
561
  *
454
- * Nothing registered: every type below falls back to the loose shapes the hooks have always had.
562
+ * Nothing registered: every type below falls back to loose reads (`LooseUniformNode`, `LooseNode`).
455
563
  * Registered once, anywhere in the app:
456
564
  *
457
565
  * ```ts
@@ -471,14 +579,25 @@ type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStora
471
579
  *
472
580
  * then `useUniforms().uTime`, `useUniforms('player').uHealth`, `useUniform('uTime')`,
473
581
  * `useFrame(({ uniforms }) => uniforms.uTime)`, `useThree((s) => s.uniforms.uTime)` and a creator's
474
- * `({ uniforms }) => uniforms.uTime` are typed by name, with no generics.
582
+ * `({ uniforms }) => uniforms.uTime` are typed by name, with no generics. Nodes register the same
583
+ * way, by their node types: `nodes: { wobble: Node<'float'>; rimLight: typeof rimLightFn }` and
584
+ * `nodeScopes: { fx: { blur: Node<'vec4'> } }`, and so do buffers (`buffers` / `bufferScopes`) and
585
+ * GPU storage (`gpuStorage` / `gpuStorageScopes`). Docs: docs/webgpu/typed-resources.mdx.
586
+ *
587
+ * Unregistered reads are loose: `LooseUniformNode` / `LooseNode`, which every operator accepts and
588
+ * a single `as` makes exact (see those types for why).
475
589
  */
476
590
 
477
591
  /**
478
- * Augment this to type uniforms by name. Recognised keys:
592
+ * Augment this to type TSL resources by name. Recognised keys:
479
593
  * - `uniforms`: the root-level uniform inputs (e.g. `typeof globalUniforms`)
480
- * - `scopes`: named scopes and their inputs (e.g. `{ player: typeof playerUniforms }`)
481
- * - `strict`: `false` makes unregistered root keys loose instead of errors (default: strict)
594
+ * - `scopes`: named uniform scopes and their inputs (e.g. `{ player: typeof playerUniforms }`)
595
+ * - `nodes`: the root-level nodes by node type (e.g. `{ wobble: Node<'float'>; rim: typeof rimFn }`)
596
+ * - `nodeScopes`: named node scopes and their nodes (e.g. `{ fx: { blur: Node<'vec4'> } }`)
597
+ * - `buffers` / `bufferScopes`: buffers by name (e.g. `{ positions: StorageBufferNode<'vec3'> }`)
598
+ * - `gpuStorage` / `gpuStorageScopes`: GPU storage by name (e.g. `{ heightMap: StorageTexture }`)
599
+ * - `strict`: `false` makes unregistered root keys loose instead of errors (default: strict), for
600
+ * every resource family alike
482
601
  */
483
602
  interface Register {
484
603
  }
@@ -503,9 +622,11 @@ type TypedUniforms = UniformNodesFor<RegisteredUniforms> & {
503
622
  };
504
623
  /**
505
624
  * What `state.uniforms` and `useUniforms()` are: the registered shape (strict by default, so a typo
506
- * is an error), or today's loose `UniformStore` when nothing is registered.
625
+ * is an error), or the loose `LooseUniformStore` when nothing is registered. With `strict: false`
626
+ * an unregistered root key reads loose too. A loose read is a `LooseUniformNode`: every operator and
627
+ * material slot accepts it, and a single `as` gives it an exact type (see that type for why).
507
628
  */
508
- type AppUniforms = IsRegistered extends true ? IsStrict extends true ? TypedUniforms : TypedUniforms & UniformStore : UniformStore;
629
+ type AppUniforms = IsRegistered extends true ? IsStrict extends true ? TypedUniforms : TypedUniforms & LooseUniformStore : LooseUniformStore;
509
630
  /** Creator input at root level: new keys are free, registered keys must keep their registered type. */
510
631
  type RootUniformInput = Partial<RegisteredUniforms> & UniformInputRecord;
511
632
  /** Creator input for scope `S`: checked against the registered scope, free for any other scope. */
@@ -515,13 +636,101 @@ type RegisteredScopeUniforms<S extends keyof RegisteredScopes> = UniformNodesFor
515
636
  /** A registered root uniform's node. */
516
637
  type RegisteredUniform<K extends keyof RegisteredUniforms> = UniformNodesFor<RegisteredUniforms>[K];
517
638
  /**
518
- * `uniforms` in a creator (`useNodes(({ uniforms }) => ...)`). Unregistered: any name is a
519
- * `UniformNode`, as before. Registered: typed by name like `state.uniforms`, and `.scope(name)` is
639
+ * `uniforms` in a creator (`useNodes(({ uniforms }) => ...)`). Unregistered: any name reads as a
640
+ * `LooseUniformNode` (or, by dot access, a scope of them), and `.scope(name)` as a record of them
641
+ * unless given a schema. Registered: typed by name like `state.uniforms`, and `.scope(name)` is
520
642
  * typed for registered scopes.
521
643
  */
522
644
  type CreatorUniforms = IsRegistered extends true ? {
523
- scope<S extends keyof RegisteredScopes & string>(key: S): ScopedStoreType<UniformNode, RegisteredScopeUniforms<S>>;
524
- } & Readonly<AppUniforms> & ScopedStoreMethods<UniformNode> : ScopedStoreType<UniformNode>;
645
+ scope<S extends keyof RegisteredScopes & string>(key: S): ScopedStoreType<UniformNode, RegisteredScopeUniforms<S>, LooseUniformNode>;
646
+ } & Readonly<AppUniforms> & ScopedStoreMethods<UniformNode, LooseUniformNode> : ScopedStoreType<UniformNode, LooseUniformStore, LooseUniformNode>;
647
+ /** The registered root-level nodes, or `{}` when none are registered. */
648
+ type RegisteredNodes = Register extends {
649
+ nodes: infer N extends NodeRecord;
650
+ } ? N : {};
651
+ /** The registered node scopes and their nodes, or `{}` when none are registered. */
652
+ type RegisteredNodeScopes = Register extends {
653
+ nodeScopes: infer S extends Record<string, NodeRecord>;
654
+ } ? S : {};
655
+ type IsNodesRegistered = Register extends {
656
+ nodes: any;
657
+ } | {
658
+ nodeScopes: any;
659
+ } ? true : false;
660
+ type TypedNodes = RegisteredNodes & RegisteredNodeScopes;
661
+ /**
662
+ * What `state.nodes` and `useNodes()` are: the registered shape (strict by default, so a typo is an
663
+ * error), or the loose `LooseNodeStore` when no nodes are registered. With `strict: false` an
664
+ * unregistered root key reads loose too.
665
+ */
666
+ type AppNodes = IsNodesRegistered extends true ? IsStrict extends true ? TypedNodes : TypedNodes & LooseNodeStore : LooseNodeStore;
667
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
668
+ type RootNodeInput = Partial<RegisteredNodes> & NodeRecord;
669
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
670
+ type ScopeNodeInput<S extends string> = S extends keyof RegisteredNodeScopes ? Partial<RegisteredNodeScopes[S]> : unknown;
671
+ /**
672
+ * `nodes` in a creator. Unregistered: any name reads as a `LooseNode` (or, by dot access, a scope of
673
+ * them). Registered: typed by name like `state.nodes`, and `.scope(name)` is typed for registered
674
+ * scopes.
675
+ */
676
+ type CreatorNodes = IsNodesRegistered extends true ? {
677
+ scope<S extends keyof RegisteredNodeScopes & string>(key: S): ScopedStoreType<NodeLike, RegisteredNodeScopes[S], LooseNode>;
678
+ } & Readonly<AppNodes> & ScopedStoreMethods<NodeLike, LooseNode> : ScopedStoreType<NodeLike, LooseNodeStore, LooseNode>;
679
+ /** A node reader's result for an explicit schema `T`, or loose nodes when none was given. */
680
+ type ReadNodes<T extends NodeRecord> = NodeRecord extends T ? Record<string, LooseNode> : T;
681
+ /** The registered root-level buffers, or `{}` when none are registered. */
682
+ type RegisteredBuffers = Register extends {
683
+ buffers: infer B extends BufferRecord;
684
+ } ? B : {};
685
+ /** The registered buffer scopes and their buffers, or `{}` when none are registered. */
686
+ type RegisteredBufferScopes = Register extends {
687
+ bufferScopes: infer S extends Record<string, BufferRecord>;
688
+ } ? S : {};
689
+ type IsBuffersRegistered = Register extends {
690
+ buffers: any;
691
+ } | {
692
+ bufferScopes: any;
693
+ } ? true : false;
694
+ type TypedBuffers = RegisteredBuffers & RegisteredBufferScopes;
695
+ /** What `state.buffers` and `useBuffers()` are: the registered shape, or `BufferStore` when none are. */
696
+ type AppBuffers = IsBuffersRegistered extends true ? IsStrict extends true ? TypedBuffers : TypedBuffers & BufferStore : BufferStore;
697
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
698
+ type RootBufferInput = Partial<RegisteredBuffers> & BufferRecord;
699
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
700
+ type ScopeBufferInput<S extends string> = S extends keyof RegisteredBufferScopes ? Partial<RegisteredBufferScopes[S]> : unknown;
701
+ /** `buffers` in a creator: typed by name when registered, `BufferLike` reads otherwise. */
702
+ type CreatorBuffers = IsBuffersRegistered extends true ? {
703
+ scope<S extends keyof RegisteredBufferScopes & string>(key: S): ScopedStoreType<BufferLike, RegisteredBufferScopes[S]>;
704
+ } & Readonly<AppBuffers> & ScopedStoreMethods<BufferLike> : ScopedStoreType<BufferLike>;
705
+ /** The registered root-level GPU storage, or `{}` when none is registered. */
706
+ type RegisteredGPUStorage = Register extends {
707
+ gpuStorage: infer G extends StorageRecord;
708
+ } ? G : {};
709
+ /** The registered GPU storage scopes and their entries, or `{}` when none are registered. */
710
+ type RegisteredGPUStorageScopes = Register extends {
711
+ gpuStorageScopes: infer S extends Record<string, StorageRecord>;
712
+ } ? S : {};
713
+ type IsGPUStorageRegistered = Register extends {
714
+ gpuStorage: any;
715
+ } | {
716
+ gpuStorageScopes: any;
717
+ } ? true : false;
718
+ type TypedGPUStorage = RegisteredGPUStorage & RegisteredGPUStorageScopes;
719
+ /** What `state.gpuStorage` and `useGPUStorage()` are: the registered shape, or `StorageStore`. */
720
+ type AppGPUStorage = IsGPUStorageRegistered extends true ? IsStrict extends true ? TypedGPUStorage : TypedGPUStorage & StorageStore : StorageStore;
721
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
722
+ type RootGPUStorageInput = Partial<RegisteredGPUStorage> & StorageRecord;
723
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
724
+ type ScopeGPUStorageInput<S extends string> = S extends keyof RegisteredGPUStorageScopes ? Partial<RegisteredGPUStorageScopes[S]> : unknown;
725
+ /** `gpuStorage` in a creator: typed by name when registered, `StorageLike` reads otherwise. */
726
+ type CreatorGPUStorage = IsGPUStorageRegistered extends true ? {
727
+ scope<S extends keyof RegisteredGPUStorageScopes & string>(key: S): ScopedStoreType<StorageLike, RegisteredGPUStorageScopes[S]>;
728
+ } & Readonly<AppGPUStorage> & ScopedStoreMethods<StorageLike> : ScopedStoreType<StorageLike>;
729
+ /**
730
+ * A reader's result for an explicit schema `T`, or loose uniforms when none was given (`T` is then
731
+ * its constraint, `UniformInputRecord`): `useUniforms<{ blur: number }>('fx')` vs `useUniforms('fx')`.
732
+ */
733
+ type ReadUniforms<T extends UniformInputRecord> = UniformInputRecord extends T ? Record<string, LooseUniformNode> : UniformNodesFor<T>;
525
734
 
526
735
  /** Uniforms that exist on every primary canvas from the start. See configureTSL. */
527
736
  interface TSLConfig {
@@ -565,7 +774,7 @@ declare function useUniforms<T extends RootUniformInput>(uniforms: T): UniformsW
565
774
  declare function useUniforms<T extends UniformInputRecord, S extends string>(uniforms: T & ScopeUniformInput<S>, scope: S): UniformsWithUtils<UniformNodesFor<T>>;
566
775
  declare function useUniforms(): UniformsWithUtils<AppUniforms>;
567
776
  declare function useUniforms<S extends keyof RegisteredScopes & string>(scope: S): UniformsWithUtils<RegisteredScopeUniforms<S>>;
568
- declare function useUniforms<T extends UniformInputRecord>(scope?: string): UniformsWithUtils<UniformNodesFor<T>>;
777
+ declare function useUniforms<T extends UniformInputRecord>(scope?: string): UniformsWithUtils<ReadUniforms<T>>;
569
778
  /**
570
779
  * Global rebuildUniforms function for HMR integration.
571
780
  * Invalidates cached uniforms and increments _hmrVersion to trigger re-creation.
@@ -587,8 +796,10 @@ declare function rebuildAllUniforms(store: ReturnType<typeof useStore>, scope?:
587
796
  * - UniformNode: existing uniforms (reused as-is)
588
797
  */
589
798
  type UniformValue = number | boolean | string | Vector2 | Vector3 | Vector4 | Color | Matrix2 | Matrix3 | Matrix4 | Node | UniformNode;
799
+ /** `useUniform<T>(name)`'s result: exact for an explicit `T`, a `LooseUniformNode` without one. */
800
+ type ReadUniform<T extends UniformValue> = UniformValue extends T ? LooseUniformNode : UniformNodeFor<T>;
590
801
  declare function useUniform<K extends keyof RegisteredUniforms & string>(name: K): RegisteredUniform<K>;
591
- declare function useUniform<T extends UniformValue = UniformValue>(name: string): UniformNodeFor<T>;
802
+ declare function useUniform<T extends UniformValue = UniformValue>(name: string): ReadUniform<T>;
592
803
  declare function useUniform<T extends UniformValue>(name: string, value: T): UniformNodeFor<T>;
593
804
 
594
805
  /**
@@ -618,12 +829,12 @@ type NodesWithUtils<T extends Record<string, unknown> = NodeRecord> = T & {
618
829
  clearNodes: ClearNodesFn;
619
830
  rebuildNodes: RebuildNodesFn;
620
831
  };
621
- declare function useNodes(): NodesWithUtils<NodeStore>;
622
- declare function useNodes(scope: string): NodesWithUtils<NodeRecord>;
623
- declare function useNodes<T extends NodeRecord>(scope?: string): NodesWithUtils<T>;
624
- declare function useNodes<T extends NodeRecord>(creator: NodeCreator<T>): NodesWithUtils<T>;
625
- declare function useNodes<T extends NodeRecord>(creator: NodeCreator<T>, scope: string): NodesWithUtils<T>;
626
- declare function useNodes(creatorOrScope?: NodeCreator<NodeRecord> | string, scope?: string): NodesWithUtils<Record<string, unknown>>;
832
+ declare function useNodes(): NodesWithUtils<AppNodes>;
833
+ declare function useNodes<S extends keyof RegisteredNodeScopes & string>(scope: S): NodesWithUtils<RegisteredNodeScopes[S]>;
834
+ declare function useNodes<T extends NodeRecord>(scope?: string): NodesWithUtils<ReadNodes<T>>;
835
+ declare function useNodes<T extends RootNodeInput>(creator: NodeCreator<T>): NodesWithUtils<T>;
836
+ declare function useNodes<T extends NodeRecord, S extends string>(creator: (state: CreatorState) => T & ScopeNodeInput<S>, scope: S): NodesWithUtils<T>;
837
+ declare function useNodes<S extends string | undefined>(creatorOrScope: NodeCreator<NodeRecord> | string | undefined, scope: S & (string extends S ? unknown : never)): NodesWithUtils<Record<string, unknown>>;
627
838
  /**
628
839
  * Global rebuildNodes function for HMR integration.
629
840
  * Invalidates cached nodes and increments _hmrVersion to trigger re-creation.
@@ -694,8 +905,8 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
694
905
  *
695
906
  * @example
696
907
  * ```tsx
697
- * // Resource-driven composition: no surrounding JS inputs. `uniforms.uTime` is typed when the
698
- * // app registers its uniforms (see `Register`); otherwise give a scope its schema or cast.
908
+ * // Resource-driven composition: no surrounding JS inputs. `uniforms.uTime` is typed by name when
909
+ * // the app registers its uniforms (see `Register`), and a loose node TSL accepts as is otherwise.
699
910
  * const { wobble, uTime } = useLocalNodes(({ uniforms, nodes }) => ({
700
911
  * wobble: sin(uniforms.uTime.mul(2)),
701
912
  * uTime: uniforms.uTime, // can return uniforms too
@@ -706,10 +917,10 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
706
917
  * result: pattern === 'noise' ? nodes.noise : nodes.stripes,
707
918
  * }), [pattern])
708
919
  *
709
- * // An unregistered uniform, cast to its type
710
- * const { colorNode } = useLocalNodes(({ uniforms }) => {
711
- * const uValue = uniforms.myUniform as UniformNode<'float', number>
712
- * return { colorNode: mix(colorA, colorB, uValue) }
920
+ * // An unregistered uniform works as is; one `as` gives its exact type where that matters
921
+ * const { colorNode, facing } = useLocalNodes(({ uniforms }) => {
922
+ * const uDir = uniforms.uDir as UniformNode<'vec3', Vector3> // cross() wants a vec3
923
+ * return { colorNode: mix(colorA, colorB, uniforms.myUniform), facing: cross(uDir, normalView) }
713
924
  * }, [])
714
925
  * ```
715
926
  */
@@ -740,11 +951,13 @@ type BuffersWithUtils<T extends BufferRecordType | BufferStore = BufferRecordTyp
740
951
  rebuildBuffers: RebuildBuffersFn;
741
952
  disposeBuffers: DisposeBuffersFn;
742
953
  };
743
- declare function useBuffers(): BuffersWithUtils<BufferStore>;
954
+ declare function useBuffers(): BuffersWithUtils<AppBuffers>;
955
+ declare function useBuffers<S extends keyof RegisteredBufferScopes & string>(scope: S): BuffersWithUtils<RegisteredBufferScopes[S]>;
744
956
  declare function useBuffers(scope: string): BuffersWithUtils<Record<string, BufferLike>>;
745
957
  declare function useBuffers<T extends Record<string, BufferLike>>(scope?: string): BuffersWithUtils<T>;
746
- declare function useBuffers<T extends Record<string, BufferLike>>(creator: BufferCreator<T>): BuffersWithUtils<T>;
747
- declare function useBuffers<T extends Record<string, BufferLike>>(creator: BufferCreator<T>, scope: string): BuffersWithUtils<T>;
958
+ declare function useBuffers<T extends RootBufferInput>(creator: BufferCreator<T>): BuffersWithUtils<T>;
959
+ declare function useBuffers<T extends Record<string, BufferLike>, S extends string>(creator: (state: CreatorState) => T & ScopeBufferInput<S>, scope: S): BuffersWithUtils<T>;
960
+ declare function useBuffers<S extends string | undefined>(creator: BufferCreator<BufferRecord>, scope: S & (string extends S ? unknown : never)): BuffersWithUtils<BufferRecord>;
748
961
  /**
749
962
  * Global rebuildBuffers function for HMR integration.
750
963
  * Invalidates cached buffers and increments _hmrVersion to trigger re-creation.
@@ -782,11 +995,13 @@ type StorageWithUtils<T extends StorageRecordType | StorageStore = StorageRecord
782
995
  rebuildStorage: RebuildStorageFn;
783
996
  disposeStorage: DisposeStorageFn;
784
997
  };
785
- declare function useGPUStorage(): StorageWithUtils<StorageStore>;
998
+ declare function useGPUStorage(): StorageWithUtils<AppGPUStorage>;
999
+ declare function useGPUStorage<S extends keyof RegisteredGPUStorageScopes & string>(scope: S): StorageWithUtils<RegisteredGPUStorageScopes[S]>;
786
1000
  declare function useGPUStorage(scope: string): StorageWithUtils<Record<string, StorageLike>>;
787
1001
  declare function useGPUStorage<T extends Record<string, StorageLike>>(scope?: string): StorageWithUtils<T>;
788
- declare function useGPUStorage<T extends Record<string, StorageLike>>(creator: StorageCreator<T>): StorageWithUtils<T>;
789
- declare function useGPUStorage<T extends Record<string, StorageLike>>(creator: StorageCreator<T>, scope: string): StorageWithUtils<T>;
1002
+ declare function useGPUStorage<T extends RootGPUStorageInput>(creator: StorageCreator<T>): StorageWithUtils<T>;
1003
+ declare function useGPUStorage<T extends Record<string, StorageLike>, S extends string>(creator: (state: CreatorState) => T & ScopeGPUStorageInput<S>, scope: S): StorageWithUtils<T>;
1004
+ declare function useGPUStorage<S extends string | undefined>(creator: StorageCreator<StorageRecord>, scope: S & (string extends S ? unknown : never)): StorageWithUtils<StorageRecord>;
790
1005
  /**
791
1006
  * Global rebuildStorage function for HMR integration.
792
1007
  * Invalidates cached storage and increments _hmrVersion to trigger re-creation.
@@ -844,4 +1059,4 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
844
1059
  declare function useRenderPipeline(mainCB?: RenderPipelineMainCallback, setupCB?: RenderPipelineSetupCallback): UseRenderPipelineReturn;
845
1060
 
846
1061
  export { configureTSL, createScopedStore, rebuildAllBuffers, rebuildAllNodes, rebuildAllStorage, rebuildAllUniforms, useBuffers, useGPUStorage, useLocalNodes, useNodes, useRenderPipeline, useUniform, useUniforms };
847
- 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 };
1062
+ export type { AppBuffers, AppGPUStorage, AppNodes, AppUniforms, BufferCreator, BufferLike, BufferRecord, BufferStore, BuffersWithUtils, ClearBuffersFn, ClearNodesFn, ClearStorageFn, ClearUniformsFn, CreatorState, DisposeBuffersFn, DisposeStorageFn, FnNodeLike, LocalNodeCreator, LocalNodeInstall, LocalNodeInstaller, LooseNode, LooseNodeEntry, LooseNodeStore, NodeCreator, NodeLike, NodeRecord, NodeStore, NodesWithUtils, RebuildBuffersFn, RebuildNodesFn, RebuildStorageFn, RebuildUniformsFn, Register, RegisteredBufferScopes, RegisteredBuffers, RegisteredGPUStorage, RegisteredGPUStorageScopes, RegisteredNodeScopes, RegisteredNodes, RegisteredScopeUniforms, RegisteredScopes, RegisteredUniform, RegisteredUniforms, RemoveBuffersFn, RemoveNodesFn, RemoveStorageFn, RemoveUniformsFn, RootBufferInput, RootGPUStorageInput, RootNodeInput, RootUniformInput, ScopeBufferInput, ScopeGPUStorageInput, ScopeNodeInput, ScopeUniformInput, ScopedStoreType, StorageCreator, StorageLike, StorageRecord, StorageStore, StorageWithUtils, TSLConfig, TSLNode, TSLNodeLike, TSLRootState, UniformCreator, UniformValue, UniformsWithUtils };
package/dist/index.mjs CHANGED
@@ -58,6 +58,13 @@ function withStagedOverlay(store, kind, committed) {
58
58
  }
59
59
  return view;
60
60
  }
61
+ function readWithStaged(store, kind, committed, scope, isLeaf) {
62
+ if (scope === void 0) return withStagedOverlay(store, kind, committed);
63
+ const scopeData = committed[scope];
64
+ const current = scopeData && typeof scopeData === "object" && !isLeaf(scopeData) ? scopeData : {};
65
+ const staged = scope === ROOT_SCOPE ? void 0 : peekStaged(store, kind, scope);
66
+ return staged ? { ...current, ...staged } : current;
67
+ }
61
68
  function flushStagedScope(store, kind, scopeKey, isLeaf) {
62
69
  const registry = getKindRegistry(store, kind);
63
70
  const staged = registry.staged.get(scopeKey);
@@ -689,8 +696,9 @@ function useUniforms(creatorOrScope, scope) {
689
696
  input: memoizedInput,
690
697
  create: () => {
691
698
  if (isReader) return {};
692
- if (typeof processedInput.candidates !== "object" || processedInput.candidates === null)
699
+ if (typeof processedInput.candidates !== "object" || processedInput.candidates === null) {
693
700
  throw new Error("Invalid uniform input");
701
+ }
694
702
  return processedInput.candidates;
695
703
  },
696
704
  prepare: (name, candidate) => createUniform(name, candidate, scope),
@@ -708,13 +716,7 @@ function useUniforms(creatorOrScope, scope) {
708
716
  if (!equals) existing.value = newVal;
709
717
  }
710
718
  });
711
- let uniforms = created;
712
- if (memoizedInput === void 0) {
713
- uniforms = storeUniforms;
714
- } else if (typeof memoizedInput === "string") {
715
- const scopeData = storeUniforms[memoizedInput];
716
- uniforms = scopeData && !isUniformNode(scopeData) ? scopeData : {};
717
- }
719
+ const uniforms = isReader ? readWithStaged(store, "uniforms", storeUniforms, memoizedInput, isUniformNode) : created;
718
720
  return { ...uniforms, removeUniforms, clearUniforms, rebuildUniforms };
719
721
  }
720
722
  function rebuildAllUniforms(store, scope) {
@@ -831,13 +833,7 @@ function useNodes(creatorOrScope, scope) {
831
833
  return node;
832
834
  }
833
835
  });
834
- let nodes = created;
835
- if (creatorOrScope === void 0) {
836
- nodes = storeNodes;
837
- } else if (typeof creatorOrScope === "string") {
838
- const scopeData = storeNodes[creatorOrScope];
839
- nodes = scopeData && !isTSLNode(scopeData) ? scopeData : {};
840
- }
836
+ const nodes = isReader ? readWithStaged(store, "nodes", storeNodes, creatorOrScope, isTSLNode) : created;
841
837
  return { ...nodes, removeNodes, clearNodes, rebuildNodes };
842
838
  }
843
839
  function rebuildAllNodes(store, scope) {
@@ -992,13 +988,7 @@ function useBuffers(creatorOrScope, scope) {
992
988
  return buffer;
993
989
  }
994
990
  });
995
- let buffers = created;
996
- if (creatorOrScope === void 0) {
997
- buffers = storeBuffers;
998
- } else if (typeof creatorOrScope === "string") {
999
- const scopeData = storeBuffers[creatorOrScope];
1000
- buffers = scopeData && !isBufferLike(scopeData) ? scopeData : {};
1001
- }
991
+ const buffers = isReader ? readWithStaged(store, "buffers", storeBuffers, creatorOrScope, isBufferLike) : created;
1002
992
  return { ...buffers, removeBuffers, clearBuffers, rebuildBuffers, disposeBuffers };
1003
993
  }
1004
994
  function rebuildAllBuffers(store, scope) {
@@ -1062,13 +1052,7 @@ function useGPUStorage(creatorOrScope, scope) {
1062
1052
  return storage;
1063
1053
  }
1064
1054
  });
1065
- let gpuStorage = created;
1066
- if (creatorOrScope === void 0) {
1067
- gpuStorage = storeStorage;
1068
- } else if (typeof creatorOrScope === "string") {
1069
- const scopeData = storeStorage[creatorOrScope];
1070
- gpuStorage = scopeData && !isStorageLike(scopeData) ? scopeData : {};
1071
- }
1055
+ const gpuStorage = isReader ? readWithStaged(store, "gpuStorage", storeStorage, creatorOrScope, isStorageLike) : created;
1072
1056
  return {
1073
1057
  ...gpuStorage,
1074
1058
  removeStorage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/tsl",
3
- "version": "10.0.0-canary.87166dc",
3
+ "version": "10.0.0-canary.908b65b",
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",
@@ -56,7 +56,7 @@
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.87166dc"
59
+ "@react-three/fiber": "10.0.0-canary.908b65b"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "unbuild",