@react-three/tsl 10.0.0-canary.e3891fb → 10.0.0-canary.e78793c

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.cts 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. */
@@ -149,6 +190,16 @@ declare global {
149
190
  /** Node type accepted by broad dynamic node-store readers. */
150
191
  type TSLNodeType = three_webgpu.Node | CallableTSLNode
151
192
 
193
+ /**
194
+ * What TSL's constant constructors return: `int(1)`, `vec2(0, 1)`, `color('red')` and friends
195
+ * wrap a ConstNode in a VarNode. three's `uniform()` unwraps exactly this shape to
196
+ * `UniformNode<TNodeType, TValue>`, and so do the mappings below.
197
+ */
198
+ type TSLConstNode<TNodeType, TValue> = three_webgpu.VarNode<
199
+ TNodeType,
200
+ three_webgpu.ConstNode<TNodeType, TValue>
201
+ >
202
+
152
203
  /**
153
204
  * Derive Three's shader node type from an existing node or a supported raw input.
154
205
  * Existing InputNode generics take precedence over structural value normalization.
@@ -159,44 +210,48 @@ declare global {
159
210
  ? TNodeType
160
211
  : T extends three_webgpu.InputNode<infer TNodeType, infer _TValue>
161
212
  ? TNodeType
162
- : T extends number
163
- ? 'float'
164
- : T extends boolean
165
- ? 'bool'
166
- : T extends string | three_webgpu.Color | { r: number; g: number; b: number }
167
- ? 'color'
168
- : T extends three_webgpu.Vector4 | { x: number; y: number; z: number; w: number }
169
- ? 'vec4'
170
- : T extends three_webgpu.Vector3 | { x: number; y: number; z: number }
171
- ? 'vec3'
172
- : T extends three_webgpu.Vector2 | { x: number; y: number }
173
- ? 'vec2'
174
- : T extends three_webgpu.Matrix4
175
- ? 'mat4'
176
- : T extends three_webgpu.Matrix3
177
- ? 'mat3'
178
- : T extends three_webgpu.Matrix2
179
- ? 'mat2'
180
- : unknown
213
+ : T extends TSLConstNode<infer TNodeType, infer _TValue>
214
+ ? TNodeType
215
+ : T extends number
216
+ ? 'float'
217
+ : T extends boolean
218
+ ? 'bool'
219
+ : T extends string | three_webgpu.Color | { r: number; g: number; b: number }
220
+ ? 'color'
221
+ : T extends three_webgpu.Vector4 | { x: number; y: number; z: number; w: number }
222
+ ? 'vec4'
223
+ : T extends three_webgpu.Vector3 | { x: number; y: number; z: number }
224
+ ? 'vec3'
225
+ : T extends three_webgpu.Vector2 | { x: number; y: number }
226
+ ? 'vec2'
227
+ : T extends three_webgpu.Matrix4
228
+ ? 'mat4'
229
+ : T extends three_webgpu.Matrix3
230
+ ? 'mat3'
231
+ : T extends three_webgpu.Matrix2
232
+ ? 'mat2'
233
+ : unknown
181
234
 
182
235
  /** Derive the normalized JavaScript value stored by a uniform node. */
183
236
  type UniformNodeValue<T> = T extends three_webgpu.UniformNode<infer _TNodeType, infer TValue>
184
237
  ? TValue
185
238
  : T extends three_webgpu.InputNode<infer _TNodeType, infer TValue>
186
239
  ? TValue
187
- : T extends string | { r: number; g: number; b: number }
188
- ? three_webgpu.Color
189
- : T extends { x: number; y: number; z: number; w: number }
190
- ? three_webgpu.Vector4
191
- : T extends { x: number; y: number; z: number }
192
- ? three_webgpu.Vector3
193
- : T extends { x: number; y: number }
194
- ? three_webgpu.Vector2
195
- : T extends number
196
- ? number
197
- : T extends boolean
198
- ? boolean
199
- : T
240
+ : T extends TSLConstNode<infer _TNodeType, infer TValue>
241
+ ? TValue
242
+ : T extends string | { r: number; g: number; b: number }
243
+ ? three_webgpu.Color
244
+ : T extends { x: number; y: number; z: number; w: number }
245
+ ? three_webgpu.Vector4
246
+ : T extends { x: number; y: number; z: number }
247
+ ? three_webgpu.Vector3
248
+ : T extends { x: number; y: number }
249
+ ? three_webgpu.Vector2
250
+ : T extends number
251
+ ? number
252
+ : T extends boolean
253
+ ? boolean
254
+ : T
200
255
 
201
256
  /** Three's exact UniformNode with shader and value generics derived from an input. */
202
257
  type UniformNodeFor<T> = three_webgpu.UniformNode<UniformNodeType<T>, UniformNodeValue<T>>
@@ -219,6 +274,69 @@ declare global {
219
274
  */
220
275
  type UniformStore = Record<string, UniformNode | UniformRecord>
221
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
+
222
340
  /**
223
341
  * Helper to safely access a uniform node from the store.
224
342
  * Use this when accessing state.uniforms to get proper typing.
@@ -381,13 +499,14 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
381
499
  *
382
500
  * @example
383
501
  * ```tsx
384
- * // 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
385
503
  * useLocalNodes(({ uniforms }) => ({
386
504
  * wobble: sin(uniforms.uTime.mul(2)),
387
505
  * playerHealth: uniforms.scope('player').uHealth, // or uniforms.player.uHealth
388
506
  * }), [])
389
507
  *
390
- * // 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)) }), [])
391
510
  * useLocalNodes(({ uniforms }) => {
392
511
  * const player = uniforms.scope<{ uHealth: UniformNode<'float', number> }>('player')
393
512
  * return { damage: player.uHealth.mul(2) }
@@ -400,16 +519,19 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
400
519
  * Property access returns `T` (assumes leaf node).
401
520
  * Use `.scope(key)` for nested object access.
402
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).
403
525
  */
404
- type ScopedStoreMethods<TLeaf> = {
526
+ type ScopedStoreMethods<TLeaf, TRead extends TLeaf = TLeaf> = {
405
527
  /** Access a nested scope by key. Returns empty wrapper if scope doesn't exist. */
406
- 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>;
407
529
  /** Check if a key exists in the store */
408
530
  has(key: string): boolean;
409
531
  /** Get all keys in the store */
410
532
  keys(): string[];
411
533
  };
412
- 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>;
413
535
  interface ScopedStoreData<TLeaf> {
414
536
  [key: string]: TLeaf | ScopedStoreData<TLeaf>;
415
537
  }
@@ -426,18 +548,18 @@ declare function createScopedStore<TLeaf>(data: ScopedStoreData<TLeaf>, isLeaf:
426
548
  type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStorage'> & {
427
549
  /** Type-safe uniform access - property access returns UniformNode, typed by name when registered */
428
550
  uniforms: CreatorUniforms;
429
- /** Type-safe node access for real, callable, and legacy structural nodes */
430
- nodes: ScopedStoreType<NodeLike>;
431
- /** Type-safe buffer access - property access returns BufferLike (TypedArrays, BufferAttributes, TSL nodes) */
432
- buffers: ScopedStoreType<BufferLike>;
433
- /** Type-safe GPU storage access - property access returns StorageLike (StorageTexture, TSL nodes) */
434
- 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;
435
557
  };
436
558
 
437
559
  /**
438
- * @fileoverview Typed uniforms by registration (the TanStack Router `Register` pattern).
560
+ * @fileoverview Typed TSL resources by registration (the TanStack Router `Register` pattern).
439
561
  *
440
- * 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`).
441
563
  * Registered once, anywhere in the app:
442
564
  *
443
565
  * ```ts
@@ -457,14 +579,25 @@ type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStora
457
579
  *
458
580
  * then `useUniforms().uTime`, `useUniforms('player').uHealth`, `useUniform('uTime')`,
459
581
  * `useFrame(({ uniforms }) => uniforms.uTime)`, `useThree((s) => s.uniforms.uTime)` and a creator's
460
- * `({ 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).
461
589
  */
462
590
 
463
591
  /**
464
- * Augment this to type uniforms by name. Recognised keys:
592
+ * Augment this to type TSL resources by name. Recognised keys:
465
593
  * - `uniforms`: the root-level uniform inputs (e.g. `typeof globalUniforms`)
466
- * - `scopes`: named scopes and their inputs (e.g. `{ player: typeof playerUniforms }`)
467
- * - `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
468
601
  */
469
602
  interface Register {
470
603
  }
@@ -489,9 +622,11 @@ type TypedUniforms = UniformNodesFor<RegisteredUniforms> & {
489
622
  };
490
623
  /**
491
624
  * What `state.uniforms` and `useUniforms()` are: the registered shape (strict by default, so a typo
492
- * 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).
493
628
  */
494
- 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;
495
630
  /** Creator input at root level: new keys are free, registered keys must keep their registered type. */
496
631
  type RootUniformInput = Partial<RegisteredUniforms> & UniformInputRecord;
497
632
  /** Creator input for scope `S`: checked against the registered scope, free for any other scope. */
@@ -501,13 +636,101 @@ type RegisteredScopeUniforms<S extends keyof RegisteredScopes> = UniformNodesFor
501
636
  /** A registered root uniform's node. */
502
637
  type RegisteredUniform<K extends keyof RegisteredUniforms> = UniformNodesFor<RegisteredUniforms>[K];
503
638
  /**
504
- * `uniforms` in a creator (`useNodes(({ uniforms }) => ...)`). Unregistered: any name is a
505
- * `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
506
642
  * typed for registered scopes.
507
643
  */
508
644
  type CreatorUniforms = IsRegistered extends true ? {
509
- scope<S extends keyof RegisteredScopes & string>(key: S): ScopedStoreType<UniformNode, RegisteredScopeUniforms<S>>;
510
- } & 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>;
511
734
 
512
735
  /** Uniforms that exist on every primary canvas from the start. See configureTSL. */
513
736
  interface TSLConfig {
@@ -551,7 +774,7 @@ declare function useUniforms<T extends RootUniformInput>(uniforms: T): UniformsW
551
774
  declare function useUniforms<T extends UniformInputRecord, S extends string>(uniforms: T & ScopeUniformInput<S>, scope: S): UniformsWithUtils<UniformNodesFor<T>>;
552
775
  declare function useUniforms(): UniformsWithUtils<AppUniforms>;
553
776
  declare function useUniforms<S extends keyof RegisteredScopes & string>(scope: S): UniformsWithUtils<RegisteredScopeUniforms<S>>;
554
- declare function useUniforms<T extends UniformInputRecord>(scope?: string): UniformsWithUtils<UniformNodesFor<T>>;
777
+ declare function useUniforms<T extends UniformInputRecord>(scope?: string): UniformsWithUtils<ReadUniforms<T>>;
555
778
  /**
556
779
  * Global rebuildUniforms function for HMR integration.
557
780
  * Invalidates cached uniforms and increments _hmrVersion to trigger re-creation.
@@ -573,8 +796,10 @@ declare function rebuildAllUniforms(store: ReturnType<typeof useStore>, scope?:
573
796
  * - UniformNode: existing uniforms (reused as-is)
574
797
  */
575
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>;
576
801
  declare function useUniform<K extends keyof RegisteredUniforms & string>(name: K): RegisteredUniform<K>;
577
- declare function useUniform<T extends UniformValue = UniformValue>(name: string): UniformNodeFor<T>;
802
+ declare function useUniform<T extends UniformValue = UniformValue>(name: string): ReadUniform<T>;
578
803
  declare function useUniform<T extends UniformValue>(name: string, value: T): UniformNodeFor<T>;
579
804
 
580
805
  /**
@@ -604,12 +829,12 @@ type NodesWithUtils<T extends Record<string, unknown> = NodeRecord> = T & {
604
829
  clearNodes: ClearNodesFn;
605
830
  rebuildNodes: RebuildNodesFn;
606
831
  };
607
- declare function useNodes(): NodesWithUtils<NodeStore>;
608
- declare function useNodes(scope: string): NodesWithUtils<NodeRecord>;
609
- declare function useNodes<T extends NodeRecord>(scope?: string): NodesWithUtils<T>;
610
- declare function useNodes<T extends NodeRecord>(creator: NodeCreator<T>): NodesWithUtils<T>;
611
- declare function useNodes<T extends NodeRecord>(creator: NodeCreator<T>, scope: string): NodesWithUtils<T>;
612
- 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>>;
613
838
  /**
614
839
  * Global rebuildNodes function for HMR integration.
615
840
  * Invalidates cached nodes and increments _hmrVersion to trigger re-creation.
@@ -680,8 +905,8 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
680
905
  *
681
906
  * @example
682
907
  * ```tsx
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.
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.
685
910
  * const { wobble, uTime } = useLocalNodes(({ uniforms, nodes }) => ({
686
911
  * wobble: sin(uniforms.uTime.mul(2)),
687
912
  * uTime: uniforms.uTime, // can return uniforms too
@@ -692,10 +917,10 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
692
917
  * result: pattern === 'noise' ? nodes.noise : nodes.stripes,
693
918
  * }), [pattern])
694
919
  *
695
- * // An unregistered uniform, cast to its type
696
- * const { colorNode } = useLocalNodes(({ uniforms }) => {
697
- * const uValue = uniforms.myUniform as UniformNode<'float', number>
698
- * 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) }
699
924
  * }, [])
700
925
  * ```
701
926
  */
@@ -726,11 +951,13 @@ type BuffersWithUtils<T extends BufferRecordType | BufferStore = BufferRecordTyp
726
951
  rebuildBuffers: RebuildBuffersFn;
727
952
  disposeBuffers: DisposeBuffersFn;
728
953
  };
729
- 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]>;
730
956
  declare function useBuffers(scope: string): BuffersWithUtils<Record<string, BufferLike>>;
731
957
  declare function useBuffers<T extends Record<string, BufferLike>>(scope?: string): BuffersWithUtils<T>;
732
- declare function useBuffers<T extends Record<string, BufferLike>>(creator: BufferCreator<T>): BuffersWithUtils<T>;
733
- 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>;
734
961
  /**
735
962
  * Global rebuildBuffers function for HMR integration.
736
963
  * Invalidates cached buffers and increments _hmrVersion to trigger re-creation.
@@ -768,11 +995,13 @@ type StorageWithUtils<T extends StorageRecordType | StorageStore = StorageRecord
768
995
  rebuildStorage: RebuildStorageFn;
769
996
  disposeStorage: DisposeStorageFn;
770
997
  };
771
- 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]>;
772
1000
  declare function useGPUStorage(scope: string): StorageWithUtils<Record<string, StorageLike>>;
773
1001
  declare function useGPUStorage<T extends Record<string, StorageLike>>(scope?: string): StorageWithUtils<T>;
774
- declare function useGPUStorage<T extends Record<string, StorageLike>>(creator: StorageCreator<T>): StorageWithUtils<T>;
775
- 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>;
776
1005
  /**
777
1006
  * Global rebuildStorage function for HMR integration.
778
1007
  * Invalidates cached storage and increments _hmrVersion to trigger re-creation.
@@ -830,4 +1059,4 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
830
1059
  declare function useRenderPipeline(mainCB?: RenderPipelineMainCallback, setupCB?: RenderPipelineSetupCallback): UseRenderPipelineReturn;
831
1060
 
832
1061
  export { configureTSL, createScopedStore, rebuildAllBuffers, rebuildAllNodes, rebuildAllStorage, rebuildAllUniforms, useBuffers, useGPUStorage, useLocalNodes, useNodes, useRenderPipeline, useUniform, useUniforms };
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 };
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 };