@react-three/tsl 10.0.0-canary.87166dc → 10.0.0-canary.88b1ca7

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.
@@ -314,13 +418,18 @@ declare global {
314
418
  /**
315
419
  * State passed to pipeline callbacks after the active pipeline has been created.
316
420
  *
421
+ * The same state `useNodes` / `useLocalNodes` creators get: `uniforms`, `nodes`, `buffers`,
422
+ * `gpuStorage` and `textures` are the wrapped views, entries staged earlier in the render
423
+ * included. Reads through them are tracked, so a read resource that appears, is replaced or is
424
+ * removed re-runs the callbacks.
425
+ *
317
426
  * `passes.scenePass` is required here: the hook installs the default scene pass before either
318
427
  * callback runs, so callbacks can use it without a guard or a cast.
319
428
  *
320
429
  * `renderer` is narrowed to `WebGPURenderer` and `isLegacy` to `false`: the hook throws under
321
430
  * the legacy renderer before either callback can run, so a callback never sees WebGL.
322
431
  */
323
- type RenderPipelineCallbackState = RootState & {
432
+ type RenderPipelineCallbackState = CreatorState & {
324
433
  renderPipeline: ThreeRenderPipeline
325
434
  passes: PassRecord & { scenePass: ScenePassNode }
326
435
  renderer: three_webgpu.WebGPURenderer
@@ -331,7 +440,7 @@ declare global {
331
440
  * What a callback may return to register entries into `state.passes`.
332
441
  *
333
442
  * `scenePass` is reserved. The hook owns that entry and its lifecycle: it caches the pass it
334
- * created, and `rebuild()` / `reset()` dispose that cached pass. A callback overwriting the
443
+ * created, and `rebuild()`, `reset()` and unmount dispose that cached pass. A callback overwriting the
335
444
  * store entry would leave the store pointing at a node the hook never disposes, and the hook
336
445
  * disposing a pass nothing references. So returning it is a type error.
337
446
  */
@@ -343,15 +452,31 @@ declare global {
343
452
  /** Main callback - runs second to configure outputNode, create effect passes */
344
453
  type RenderPipelineMainCallback = (state: RenderPipelineCallbackState) => RegisteredPasses | void
345
454
 
455
+ /** Options for a configuring useRenderPipeline call */
456
+ interface RenderPipelineOptions {
457
+ /**
458
+ * Keep the pipeline rendering after the component unmounts, until another component configures
459
+ * the canvas's pipeline, `reset()`, or the canvas unmounts. Off by default: an owner frees its
460
+ * pipeline and scene pass when it unmounts. A remount is a new owner with a new pipeline either way.
461
+ */
462
+ persist?: boolean
463
+ }
464
+
346
465
  /** The imperative half of useRenderPipeline's return value, present in every state */
347
466
  interface UseRenderPipelineActions {
348
467
  /** Current passes from state */
349
468
  passes: PassRecord
350
- /** Clear all passes from state */
469
+ /** Clear the passes record. From a reader, acts on the pipeline that is rendering. */
351
470
  clearPasses: () => void
352
- /** Reset RenderPipeline entirely (clears PP + passes) */
471
+ /**
472
+ * Free the pipeline and its scene pass now. The previous owner takes over, or the canvas
473
+ * goes back to plain rendering. From a reader, acts on the pipeline that is rendering.
474
+ */
353
475
  reset: () => void
354
- /** Re-run setup/main callbacks with current closure values */
476
+ /**
477
+ * Re-run setup/main callbacks with current closure values, against a fresh scene pass. From
478
+ * a reader, rebuilds the pipeline that is rendering.
479
+ */
355
480
  rebuild: () => void
356
481
  }
357
482
 
@@ -371,9 +496,9 @@ declare global {
371
496
  renderPipeline: ThreeRenderPipeline
372
497
  }
373
498
  | {
374
- /** False until the pipeline has been created */
499
+ /** False until a configuring call has created the pipeline */
375
500
  isReady: false
376
- /** Not initialized yet, or torn down by reset() */
501
+ /** No owner yet, or torn down by reset() or the owner unmounting */
377
502
  renderPipeline: null
378
503
  }
379
504
  )
@@ -395,13 +520,14 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
395
520
  *
396
521
  * @example
397
522
  * ```tsx
398
- * // With uniforms registered (see `Register` and the Typed Uniforms guide), reads are typed by name
523
+ * // With uniforms registered (see `Register` and the Typed Resources guide), reads are typed by name
399
524
  * useLocalNodes(({ uniforms }) => ({
400
525
  * wobble: sin(uniforms.uTime.mul(2)),
401
526
  * playerHealth: uniforms.scope('player').uHealth, // or uniforms.player.uHealth
402
527
  * }), [])
403
528
  *
404
- * // Without registration, give a scope its schema
529
+ * // Without registration a read is loose: TSL takes it as is. A scope schema makes it exact.
530
+ * useLocalNodes(({ uniforms }) => ({ wobble: sin(uniforms.uTime.mul(2)) }), [])
405
531
  * useLocalNodes(({ uniforms }) => {
406
532
  * const player = uniforms.scope<{ uHealth: UniformNode<'float', number> }>('player')
407
533
  * return { damage: player.uHealth.mul(2) }
@@ -414,16 +540,19 @@ type ResourceLeafGuard<T> = (value: unknown) => value is T;
414
540
  * Property access returns `T` (assumes leaf node).
415
541
  * Use `.scope(key)` for nested object access.
416
542
  *
543
+ * `TLeaf` is what a scope schema must hold (`scope<{ uHealth: UniformNode<'float', number> }>()`),
544
+ * `TRead` what a leaf reads as without one. They differ for uniforms and nodes, whose unregistered
545
+ * reads are the loose `LooseUniformNode` / `LooseNode` (see those types for why).
417
546
  */
418
- type ScopedStoreMethods<TLeaf> = {
547
+ type ScopedStoreMethods<TLeaf, TRead extends TLeaf = TLeaf> = {
419
548
  /** 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>;
549
+ scope<TScope extends Record<string, TLeaf> = Record<string, TRead>>(key: string): ScopedStoreType<TLeaf, TScope, TRead>;
421
550
  /** Check if a key exists in the store */
422
551
  has(key: string): boolean;
423
552
  /** Get all keys in the store */
424
553
  keys(): string[];
425
554
  };
426
- type ScopedStoreType<TLeaf, TEntries extends Record<string, TLeaf> = Record<string, TLeaf>> = Readonly<TEntries> & ScopedStoreMethods<TLeaf>;
555
+ type ScopedStoreType<TLeaf, TEntries extends Record<string, TLeaf> = Record<string, TLeaf>, TRead extends TLeaf = TLeaf> = Readonly<TEntries> & ScopedStoreMethods<TLeaf, TRead>;
427
556
  interface ScopedStoreData<TLeaf> {
428
557
  [key: string]: TLeaf | ScopedStoreData<TLeaf>;
429
558
  }
@@ -440,18 +569,18 @@ declare function createScopedStore<TLeaf>(data: ScopedStoreData<TLeaf>, isLeaf:
440
569
  type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStorage'> & {
441
570
  /** Type-safe uniform access - property access returns UniformNode, typed by name when registered */
442
571
  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>;
572
+ /** Node access: each read a `LooseNode` (callable, every operator), typed by name when registered */
573
+ nodes: CreatorNodes;
574
+ /** Buffer access: typed by name when registered, BufferLike (typed arrays, attributes, nodes) otherwise */
575
+ buffers: CreatorBuffers;
576
+ /** GPU storage access: typed by name when registered, StorageLike (storage textures, nodes) otherwise */
577
+ gpuStorage: CreatorGPUStorage;
449
578
  };
450
579
 
451
580
  /**
452
- * @fileoverview Typed uniforms by registration (the TanStack Router `Register` pattern).
581
+ * @fileoverview Typed TSL resources by registration (the TanStack Router `Register` pattern).
453
582
  *
454
- * Nothing registered: every type below falls back to the loose shapes the hooks have always had.
583
+ * Nothing registered: every type below falls back to loose reads (`LooseUniformNode`, `LooseNode`).
455
584
  * Registered once, anywhere in the app:
456
585
  *
457
586
  * ```ts
@@ -471,14 +600,25 @@ type CreatorState = Omit<RootState, 'uniforms' | 'nodes' | 'buffers' | 'gpuStora
471
600
  *
472
601
  * then `useUniforms().uTime`, `useUniforms('player').uHealth`, `useUniform('uTime')`,
473
602
  * `useFrame(({ uniforms }) => uniforms.uTime)`, `useThree((s) => s.uniforms.uTime)` and a creator's
474
- * `({ uniforms }) => uniforms.uTime` are typed by name, with no generics.
603
+ * `({ uniforms }) => uniforms.uTime` are typed by name, with no generics. Nodes register the same
604
+ * way, by their node types: `nodes: { wobble: Node<'float'>; rimLight: typeof rimLightFn }` and
605
+ * `nodeScopes: { fx: { blur: Node<'vec4'> } }`, and so do buffers (`buffers` / `bufferScopes`) and
606
+ * GPU storage (`gpuStorage` / `gpuStorageScopes`). Docs: docs/webgpu/typed-resources.mdx.
607
+ *
608
+ * Unregistered reads are loose: `LooseUniformNode` / `LooseNode`, which every operator accepts and
609
+ * a single `as` makes exact (see those types for why).
475
610
  */
476
611
 
477
612
  /**
478
- * Augment this to type uniforms by name. Recognised keys:
613
+ * Augment this to type TSL resources by name. Recognised keys:
479
614
  * - `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)
615
+ * - `scopes`: named uniform scopes and their inputs (e.g. `{ player: typeof playerUniforms }`)
616
+ * - `nodes`: the root-level nodes by node type (e.g. `{ wobble: Node<'float'>; rim: typeof rimFn }`)
617
+ * - `nodeScopes`: named node scopes and their nodes (e.g. `{ fx: { blur: Node<'vec4'> } }`)
618
+ * - `buffers` / `bufferScopes`: buffers by name (e.g. `{ positions: StorageBufferNode<'vec3'> }`)
619
+ * - `gpuStorage` / `gpuStorageScopes`: GPU storage by name (e.g. `{ heightMap: StorageTexture }`)
620
+ * - `strict`: `false` makes unregistered root keys loose instead of errors (default: strict), for
621
+ * every resource family alike
482
622
  */
483
623
  interface Register {
484
624
  }
@@ -503,9 +643,11 @@ type TypedUniforms = UniformNodesFor<RegisteredUniforms> & {
503
643
  };
504
644
  /**
505
645
  * 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.
646
+ * is an error), or the loose `LooseUniformStore` when nothing is registered. With `strict: false`
647
+ * an unregistered root key reads loose too. A loose read is a `LooseUniformNode`: every operator and
648
+ * material slot accepts it, and a single `as` gives it an exact type (see that type for why).
507
649
  */
508
- type AppUniforms = IsRegistered extends true ? IsStrict extends true ? TypedUniforms : TypedUniforms & UniformStore : UniformStore;
650
+ type AppUniforms = IsRegistered extends true ? IsStrict extends true ? TypedUniforms : TypedUniforms & LooseUniformStore : LooseUniformStore;
509
651
  /** Creator input at root level: new keys are free, registered keys must keep their registered type. */
510
652
  type RootUniformInput = Partial<RegisteredUniforms> & UniformInputRecord;
511
653
  /** Creator input for scope `S`: checked against the registered scope, free for any other scope. */
@@ -515,13 +657,101 @@ type RegisteredScopeUniforms<S extends keyof RegisteredScopes> = UniformNodesFor
515
657
  /** A registered root uniform's node. */
516
658
  type RegisteredUniform<K extends keyof RegisteredUniforms> = UniformNodesFor<RegisteredUniforms>[K];
517
659
  /**
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
660
+ * `uniforms` in a creator (`useNodes(({ uniforms }) => ...)`). Unregistered: any name reads as a
661
+ * `LooseUniformNode` (or, by dot access, a scope of them), and `.scope(name)` as a record of them
662
+ * unless given a schema. Registered: typed by name like `state.uniforms`, and `.scope(name)` is
520
663
  * typed for registered scopes.
521
664
  */
522
665
  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>;
666
+ scope<S extends keyof RegisteredScopes & string>(key: S): ScopedStoreType<UniformNode, RegisteredScopeUniforms<S>, LooseUniformNode>;
667
+ } & Readonly<AppUniforms> & ScopedStoreMethods<UniformNode, LooseUniformNode> : ScopedStoreType<UniformNode, LooseUniformStore, LooseUniformNode>;
668
+ /** The registered root-level nodes, or `{}` when none are registered. */
669
+ type RegisteredNodes = Register extends {
670
+ nodes: infer N extends NodeRecord;
671
+ } ? N : {};
672
+ /** The registered node scopes and their nodes, or `{}` when none are registered. */
673
+ type RegisteredNodeScopes = Register extends {
674
+ nodeScopes: infer S extends Record<string, NodeRecord>;
675
+ } ? S : {};
676
+ type IsNodesRegistered = Register extends {
677
+ nodes: any;
678
+ } | {
679
+ nodeScopes: any;
680
+ } ? true : false;
681
+ type TypedNodes = RegisteredNodes & RegisteredNodeScopes;
682
+ /**
683
+ * What `state.nodes` and `useNodes()` are: the registered shape (strict by default, so a typo is an
684
+ * error), or the loose `LooseNodeStore` when no nodes are registered. With `strict: false` an
685
+ * unregistered root key reads loose too.
686
+ */
687
+ type AppNodes = IsNodesRegistered extends true ? IsStrict extends true ? TypedNodes : TypedNodes & LooseNodeStore : LooseNodeStore;
688
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
689
+ type RootNodeInput = Partial<RegisteredNodes> & NodeRecord;
690
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
691
+ type ScopeNodeInput<S extends string> = S extends keyof RegisteredNodeScopes ? Partial<RegisteredNodeScopes[S]> : unknown;
692
+ /**
693
+ * `nodes` in a creator. Unregistered: any name reads as a `LooseNode` (or, by dot access, a scope of
694
+ * them). Registered: typed by name like `state.nodes`, and `.scope(name)` is typed for registered
695
+ * scopes.
696
+ */
697
+ type CreatorNodes = IsNodesRegistered extends true ? {
698
+ scope<S extends keyof RegisteredNodeScopes & string>(key: S): ScopedStoreType<NodeLike, RegisteredNodeScopes[S], LooseNode>;
699
+ } & Readonly<AppNodes> & ScopedStoreMethods<NodeLike, LooseNode> : ScopedStoreType<NodeLike, LooseNodeStore, LooseNode>;
700
+ /** A node reader's result for an explicit schema `T`, or loose nodes when none was given. */
701
+ type ReadNodes<T extends NodeRecord> = NodeRecord extends T ? Record<string, LooseNode> : T;
702
+ /** The registered root-level buffers, or `{}` when none are registered. */
703
+ type RegisteredBuffers = Register extends {
704
+ buffers: infer B extends BufferRecord;
705
+ } ? B : {};
706
+ /** The registered buffer scopes and their buffers, or `{}` when none are registered. */
707
+ type RegisteredBufferScopes = Register extends {
708
+ bufferScopes: infer S extends Record<string, BufferRecord>;
709
+ } ? S : {};
710
+ type IsBuffersRegistered = Register extends {
711
+ buffers: any;
712
+ } | {
713
+ bufferScopes: any;
714
+ } ? true : false;
715
+ type TypedBuffers = RegisteredBuffers & RegisteredBufferScopes;
716
+ /** What `state.buffers` and `useBuffers()` are: the registered shape, or `BufferStore` when none are. */
717
+ type AppBuffers = IsBuffersRegistered extends true ? IsStrict extends true ? TypedBuffers : TypedBuffers & BufferStore : BufferStore;
718
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
719
+ type RootBufferInput = Partial<RegisteredBuffers> & BufferRecord;
720
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
721
+ type ScopeBufferInput<S extends string> = S extends keyof RegisteredBufferScopes ? Partial<RegisteredBufferScopes[S]> : unknown;
722
+ /** `buffers` in a creator: typed by name when registered, `BufferLike` reads otherwise. */
723
+ type CreatorBuffers = IsBuffersRegistered extends true ? {
724
+ scope<S extends keyof RegisteredBufferScopes & string>(key: S): ScopedStoreType<BufferLike, RegisteredBufferScopes[S]>;
725
+ } & Readonly<AppBuffers> & ScopedStoreMethods<BufferLike> : ScopedStoreType<BufferLike>;
726
+ /** The registered root-level GPU storage, or `{}` when none is registered. */
727
+ type RegisteredGPUStorage = Register extends {
728
+ gpuStorage: infer G extends StorageRecord;
729
+ } ? G : {};
730
+ /** The registered GPU storage scopes and their entries, or `{}` when none are registered. */
731
+ type RegisteredGPUStorageScopes = Register extends {
732
+ gpuStorageScopes: infer S extends Record<string, StorageRecord>;
733
+ } ? S : {};
734
+ type IsGPUStorageRegistered = Register extends {
735
+ gpuStorage: any;
736
+ } | {
737
+ gpuStorageScopes: any;
738
+ } ? true : false;
739
+ type TypedGPUStorage = RegisteredGPUStorage & RegisteredGPUStorageScopes;
740
+ /** What `state.gpuStorage` and `useGPUStorage()` are: the registered shape, or `StorageStore`. */
741
+ type AppGPUStorage = IsGPUStorageRegistered extends true ? IsStrict extends true ? TypedGPUStorage : TypedGPUStorage & StorageStore : StorageStore;
742
+ /** Creator output at root level: new keys are free, registered keys must keep their registered type. */
743
+ type RootGPUStorageInput = Partial<RegisteredGPUStorage> & StorageRecord;
744
+ /** Creator output for scope `S`: checked against the registered scope, free for any other scope. */
745
+ type ScopeGPUStorageInput<S extends string> = S extends keyof RegisteredGPUStorageScopes ? Partial<RegisteredGPUStorageScopes[S]> : unknown;
746
+ /** `gpuStorage` in a creator: typed by name when registered, `StorageLike` reads otherwise. */
747
+ type CreatorGPUStorage = IsGPUStorageRegistered extends true ? {
748
+ scope<S extends keyof RegisteredGPUStorageScopes & string>(key: S): ScopedStoreType<StorageLike, RegisteredGPUStorageScopes[S]>;
749
+ } & Readonly<AppGPUStorage> & ScopedStoreMethods<StorageLike> : ScopedStoreType<StorageLike>;
750
+ /**
751
+ * A reader's result for an explicit schema `T`, or loose uniforms when none was given (`T` is then
752
+ * its constraint, `UniformInputRecord`): `useUniforms<{ blur: number }>('fx')` vs `useUniforms('fx')`.
753
+ */
754
+ type ReadUniforms<T extends UniformInputRecord> = UniformInputRecord extends T ? Record<string, LooseUniformNode> : UniformNodesFor<T>;
525
755
 
526
756
  /** Uniforms that exist on every primary canvas from the start. See configureTSL. */
527
757
  interface TSLConfig {
@@ -565,7 +795,7 @@ declare function useUniforms<T extends RootUniformInput>(uniforms: T): UniformsW
565
795
  declare function useUniforms<T extends UniformInputRecord, S extends string>(uniforms: T & ScopeUniformInput<S>, scope: S): UniformsWithUtils<UniformNodesFor<T>>;
566
796
  declare function useUniforms(): UniformsWithUtils<AppUniforms>;
567
797
  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>>;
798
+ declare function useUniforms<T extends UniformInputRecord>(scope?: string): UniformsWithUtils<ReadUniforms<T>>;
569
799
  /**
570
800
  * Global rebuildUniforms function for HMR integration.
571
801
  * Invalidates cached uniforms and increments _hmrVersion to trigger re-creation.
@@ -587,8 +817,10 @@ declare function rebuildAllUniforms(store: ReturnType<typeof useStore>, scope?:
587
817
  * - UniformNode: existing uniforms (reused as-is)
588
818
  */
589
819
  type UniformValue = number | boolean | string | Vector2 | Vector3 | Vector4 | Color | Matrix2 | Matrix3 | Matrix4 | Node | UniformNode;
820
+ /** `useUniform<T>(name)`'s result: exact for an explicit `T`, a `LooseUniformNode` without one. */
821
+ type ReadUniform<T extends UniformValue> = UniformValue extends T ? LooseUniformNode : UniformNodeFor<T>;
590
822
  declare function useUniform<K extends keyof RegisteredUniforms & string>(name: K): RegisteredUniform<K>;
591
- declare function useUniform<T extends UniformValue = UniformValue>(name: string): UniformNodeFor<T>;
823
+ declare function useUniform<T extends UniformValue = UniformValue>(name: string): ReadUniform<T>;
592
824
  declare function useUniform<T extends UniformValue>(name: string, value: T): UniformNodeFor<T>;
593
825
 
594
826
  /**
@@ -618,12 +850,12 @@ type NodesWithUtils<T extends Record<string, unknown> = NodeRecord> = T & {
618
850
  clearNodes: ClearNodesFn;
619
851
  rebuildNodes: RebuildNodesFn;
620
852
  };
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>>;
853
+ declare function useNodes(): NodesWithUtils<AppNodes>;
854
+ declare function useNodes<S extends keyof RegisteredNodeScopes & string>(scope: S): NodesWithUtils<RegisteredNodeScopes[S]>;
855
+ declare function useNodes<T extends NodeRecord>(scope?: string): NodesWithUtils<ReadNodes<T>>;
856
+ declare function useNodes<T extends RootNodeInput>(creator: NodeCreator<T>): NodesWithUtils<T>;
857
+ declare function useNodes<T extends NodeRecord, S extends string>(creator: (state: CreatorState) => T & ScopeNodeInput<S>, scope: S): NodesWithUtils<T>;
858
+ declare function useNodes<S extends string | undefined>(creatorOrScope: NodeCreator<NodeRecord> | string | undefined, scope: S & (string extends S ? unknown : never)): NodesWithUtils<Record<string, unknown>>;
627
859
  /**
628
860
  * Global rebuildNodes function for HMR integration.
629
861
  * Invalidates cached nodes and increments _hmrVersion to trigger re-creation.
@@ -694,8 +926,8 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
694
926
  *
695
927
  * @example
696
928
  * ```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.
929
+ * // Resource-driven composition: no surrounding JS inputs. `uniforms.uTime` is typed by name when
930
+ * // the app registers its uniforms (see `Register`), and a loose node TSL accepts as is otherwise.
699
931
  * const { wobble, uTime } = useLocalNodes(({ uniforms, nodes }) => ({
700
932
  * wobble: sin(uniforms.uTime.mul(2)),
701
933
  * uTime: uniforms.uTime, // can return uniforms too
@@ -706,10 +938,10 @@ type LocalNodeInstaller = (state: CreatorState) => LocalNodeInstall;
706
938
  * result: pattern === 'noise' ? nodes.noise : nodes.stripes,
707
939
  * }), [pattern])
708
940
  *
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) }
941
+ * // An unregistered uniform works as is; one `as` gives its exact type where that matters
942
+ * const { colorNode, facing } = useLocalNodes(({ uniforms }) => {
943
+ * const uDir = uniforms.uDir as UniformNode<'vec3', Vector3> // cross() wants a vec3
944
+ * return { colorNode: mix(colorA, colorB, uniforms.myUniform), facing: cross(uDir, normalView) }
713
945
  * }, [])
714
946
  * ```
715
947
  */
@@ -740,11 +972,13 @@ type BuffersWithUtils<T extends BufferRecordType | BufferStore = BufferRecordTyp
740
972
  rebuildBuffers: RebuildBuffersFn;
741
973
  disposeBuffers: DisposeBuffersFn;
742
974
  };
743
- declare function useBuffers(): BuffersWithUtils<BufferStore>;
975
+ declare function useBuffers(): BuffersWithUtils<AppBuffers>;
976
+ declare function useBuffers<S extends keyof RegisteredBufferScopes & string>(scope: S): BuffersWithUtils<RegisteredBufferScopes[S]>;
744
977
  declare function useBuffers(scope: string): BuffersWithUtils<Record<string, BufferLike>>;
745
978
  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>;
979
+ declare function useBuffers<T extends RootBufferInput>(creator: BufferCreator<T>): BuffersWithUtils<T>;
980
+ declare function useBuffers<T extends Record<string, BufferLike>, S extends string>(creator: (state: CreatorState) => T & ScopeBufferInput<S>, scope: S): BuffersWithUtils<T>;
981
+ declare function useBuffers<S extends string | undefined>(creator: BufferCreator<BufferRecord>, scope: S & (string extends S ? unknown : never)): BuffersWithUtils<BufferRecord>;
748
982
  /**
749
983
  * Global rebuildBuffers function for HMR integration.
750
984
  * Invalidates cached buffers and increments _hmrVersion to trigger re-creation.
@@ -782,11 +1016,13 @@ type StorageWithUtils<T extends StorageRecordType | StorageStore = StorageRecord
782
1016
  rebuildStorage: RebuildStorageFn;
783
1017
  disposeStorage: DisposeStorageFn;
784
1018
  };
785
- declare function useGPUStorage(): StorageWithUtils<StorageStore>;
1019
+ declare function useGPUStorage(): StorageWithUtils<AppGPUStorage>;
1020
+ declare function useGPUStorage<S extends keyof RegisteredGPUStorageScopes & string>(scope: S): StorageWithUtils<RegisteredGPUStorageScopes[S]>;
786
1021
  declare function useGPUStorage(scope: string): StorageWithUtils<Record<string, StorageLike>>;
787
1022
  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>;
1023
+ declare function useGPUStorage<T extends RootGPUStorageInput>(creator: StorageCreator<T>): StorageWithUtils<T>;
1024
+ declare function useGPUStorage<T extends Record<string, StorageLike>, S extends string>(creator: (state: CreatorState) => T & ScopeGPUStorageInput<S>, scope: S): StorageWithUtils<T>;
1025
+ declare function useGPUStorage<S extends string | undefined>(creator: StorageCreator<StorageRecord>, scope: S & (string extends S ? unknown : never)): StorageWithUtils<StorageRecord>;
790
1026
  /**
791
1027
  * Global rebuildStorage function for HMR integration.
792
1028
  * Invalidates cached storage and increments _hmrVersion to trigger re-creation.
@@ -804,11 +1040,15 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
804
1040
  * Hook for managing WebGPU RenderPipeline with automatic scenePass setup.
805
1041
  *
806
1042
  * Features:
807
- * - Creates RenderPipeline instance if not exists
808
- * - Creates default scenePass (no MRT) automatically
809
- * - Callbacks receive full RootState for flexibility
810
- * - No auto-cleanup on unmount - use reset() for explicit cleanup
811
- * - Scene/camera changes trigger scenePass recreation
1043
+ * - A call with a callback owns the canvas's pipeline: it creates the pipeline and a default
1044
+ * scenePass (no MRT), and frees both when it unmounts (opt out with `persist`)
1045
+ * - A call without callbacks only reads: it never creates, keeps alive or frees the pipeline
1046
+ * - The most recently mounted owner renders; when it unmounts, the previous owner takes over again
1047
+ * - StrictMode and Fast Refresh remounts reuse the graph instead of rebuilding it
1048
+ * - Callbacks receive the same state as creators (`uniforms`, `nodes`, ... wrapped, including
1049
+ * entries staged earlier in the render), and their reads are tracked as in useLocalNodes: a read
1050
+ * resource that appears, is replaced or is removed re-runs them
1051
+ * - Callbacks re-run on a hot update (`_hmrVersion`), on scene/camera change and on rebuild()
812
1052
  * - A replaced scenePass is disposed once the new graph is installed, so rebuild() does not
813
1053
  * strand its render target (and MRT attachments) on the GPU
814
1054
  * - Any run that changes the output node sets RenderPipeline.needsUpdate, so rebuild() actually
@@ -816,6 +1056,7 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
816
1056
  *
817
1057
  * @param mainCB - Main callback to configure outputNode and create effect passes
818
1058
  * @param setupCB - Optional setup callback to configure MRT on scenePass
1059
+ * @param options - `persist: true` keeps the pipeline after the component unmounts
819
1060
  * @returns { passes, renderPipeline, clearPasses, reset, rebuild }
820
1061
  *
821
1062
  * @example
@@ -841,7 +1082,7 @@ declare function rebuildAllStorage(store: ReturnType<typeof useStore>, scope?: s
841
1082
  * const { renderPipeline, passes } = useRenderPipeline()
842
1083
  * ```
843
1084
  */
844
- declare function useRenderPipeline(mainCB?: RenderPipelineMainCallback, setupCB?: RenderPipelineSetupCallback): UseRenderPipelineReturn;
1085
+ declare function useRenderPipeline(mainCB?: RenderPipelineMainCallback, setupCB?: RenderPipelineSetupCallback, options?: RenderPipelineOptions): UseRenderPipelineReturn;
845
1086
 
846
1087
  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 };
1088
+ 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 };