@react-three/fiber 10.0.0-canary.c3fa45d → 10.0.0-canary.d6fdf72

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
@@ -801,8 +801,10 @@ interface RootState {
801
801
  buffers: BufferStore
802
802
  /** Global GPU storage (textures, etc.) - root-level storage + scoped sub-objects. Use useGPUStorage() hook */
803
803
  gpuStorage: StorageStore
804
- /** Global TSL texture nodes - use useTextures() hook for operations */
805
- textures: Map<string, any>
804
+ /** Global Texture registry (key → Texture, usually keyed by URL) - use useTextures() hook for access + lifecycle */
805
+ textures: Map<string, THREE$1.Texture>
806
+ /** Internal: refcount per texture key, driven by mounted useTexture consumers (registry enrollment is on by default) */
807
+ _textureRefs: Map<string, number>
806
808
  /** WebGPU RenderPipeline instance - use useRenderPipeline() hook */
807
809
  renderPipeline: any | null // THREE.PostProcessing (will be THREE.RenderPipeline in future Three.js release)
808
810
  /** Global TSL pass nodes for render pipeline - use useRenderPipeline() hook */
@@ -2573,11 +2575,16 @@ type UseTextureOptions<Url extends string[] | string | Record<string, string>> =
2573
2575
  /** Callback when texture(s) finish loading */
2574
2576
  onLoad?: (texture: MappedTextureType<Url>) => void;
2575
2577
  /**
2576
- * Cache the texture in R3F's global state for access via useTextures().
2577
- * When true:
2578
+ * Register the texture(s) in R3F's global texture registry for access via useTextures().
2579
+ * When true (the default):
2578
2580
  * - Textures persist until explicitly disposed
2579
- * - Returns existing cached textures if available (preserving modifications like colorSpace)
2580
- * @default false
2581
+ * - On mount, returns existing registered textures if available (preserving modifications like colorSpace)
2582
+ *
2583
+ * Reads are taken once at mount — a mounted `useTexture` does not re-render when the registry
2584
+ * changes. To react to registry swaps, use `useTextures(s => s.get(key))` instead.
2585
+ *
2586
+ * Pass `false` to opt out of registry enrollment entirely.
2587
+ * @default true
2581
2588
  */
2582
2589
  cache?: boolean;
2583
2590
  };
@@ -2601,15 +2608,18 @@ type UseTextureOptions<Url extends string[] | string | Record<string, string>> =
2601
2608
  * normal: '/normal.png'
2602
2609
  * })
2603
2610
  *
2604
- * // With caching - returns same texture object across components
2605
- * // Modifications (colorSpace, wrapS, etc.) are preserved
2606
- * const diffuse = useTexture('/diffuse.png', { cache: true })
2611
+ * // Textures are registered in the global registry by default — the same texture
2612
+ * // object is shared across components and modifications (colorSpace, wrapS, etc.) are preserved
2613
+ * const diffuse = useTexture('/diffuse.png')
2607
2614
  * diffuse.colorSpace = THREE.SRGBColorSpace
2608
2615
  *
2609
2616
  * // Another component gets the SAME texture with colorSpace already set
2610
- * const sameDiffuse = useTexture('/diffuse.png', { cache: true })
2617
+ * const sameDiffuse = useTexture('/diffuse.png')
2611
2618
  *
2612
- * // Access cache directly
2619
+ * // Opt out of registry enrollment for a one-off texture
2620
+ * const scratch = useTexture('/scratch.png', { cache: false })
2621
+ *
2622
+ * // Access the registry directly
2613
2623
  * const { get } = useTextures()
2614
2624
  * const cached = get('/diffuse.png')
2615
2625
  * ```
@@ -2626,63 +2636,80 @@ declare const Texture: ({ children, input, onLoad, cache, }: {
2626
2636
  cache?: boolean;
2627
2637
  }) => react_jsx_runtime.JSX.Element;
2628
2638
 
2629
- type TextureEntry = Texture$1 | {
2630
- value: Texture$1;
2631
- [key: string]: any;
2639
+ /** Accepted shapes for a registry read: a single key, a list of keys, or a name→key map. */
2640
+ type TextureInput = string | string[] | Record<string, string>;
2641
+ /** Result of a `get()` read, mirroring the input shape. */
2642
+ type TextureResult<Input extends TextureInput> = Input extends string ? Texture$1 | undefined : Input extends string[] ? (Texture$1 | undefined)[] : {
2643
+ [K in keyof Input]: Texture$1 | undefined;
2632
2644
  };
2645
+ /** Options for `dispose()`. */
2646
+ interface DisposeOptions {
2647
+ /** Dispose even if the texture still has active references (default false). */
2648
+ force?: boolean;
2649
+ }
2633
2650
  interface UseTexturesReturn {
2634
- /** Map of all textures currently in cache */
2635
- textures: Map<string, TextureEntry>;
2636
- /** Get a specific texture by key (usually URL) */
2637
- get: (key: string) => TextureEntry | undefined;
2638
- /** Check if a texture exists in cache */
2639
- has: (key: string) => boolean;
2640
- /** Add a texture to the cache */
2641
- add: (key: string, value: TextureEntry) => void;
2642
- /** Add multiple textures to the cache */
2643
- addMultiple: (items: Map<string, TextureEntry> | Record<string, TextureEntry>) => void;
2644
- /** Remove a texture from cache (does NOT dispose GPU resources) */
2645
- remove: (key: string) => void;
2646
- /** Remove multiple textures from cache */
2647
- removeMultiple: (keys: string[]) => void;
2648
- /** Dispose a texture's GPU resources and remove from cache */
2649
- dispose: (key: string) => void;
2650
- /** Dispose multiple textures */
2651
- disposeMultiple: (keys: string[]) => void;
2652
- /** Dispose ALL cached textures - use with caution */
2653
- disposeAll: () => void;
2651
+ /** The live registry Map (key Texture). Treat as read-only. */
2652
+ readonly all: Map<string, Texture$1>;
2653
+ /** Read one texture, an array of textures, or a name→texture record (mirrors the input shape). */
2654
+ get<Input extends TextureInput>(input: Input): TextureResult<Input>;
2655
+ /** Check whether a key exists in the registry. */
2656
+ has(key: string): boolean;
2657
+ /**
2658
+ * Register a texture (or a record of textures) that has no URL — e.g. a render
2659
+ * target or procedural texture. URL-loaded textures are registered automatically
2660
+ * by `useTexture` (registry enrollment is on by default).
2661
+ */
2662
+ add(key: string, texture: Texture$1): void;
2663
+ add(record: Record<string, Texture$1>): void;
2664
+ /**
2665
+ * Dispose a texture's GPU resources and remove it from the registry.
2666
+ * Refcount-aware: if the key is still in use by a mounted `useTexture` consumer it is
2667
+ * skipped (with a warning) unless `{ force: true }` is passed.
2668
+ * @returns true if disposed, false if skipped.
2669
+ */
2670
+ dispose(key: string, options?: DisposeOptions): boolean;
2671
+ /** Dispose every texture in the registry and clear it. Use on scene teardown. */
2672
+ disposeAll(): void;
2654
2673
  }
2655
2674
  /**
2656
- * Hook for managing the global texture cache in R3F state.
2675
+ * Reactive Texture registry load once with `useTexture`, reach the textures from anywhere.
2676
+ *
2677
+ * This is a registry of plain `Texture` objects (not TSL nodes), so the same texture can feed
2678
+ * a material map, a heightmap sampler, or anything else. It does **not** load — pair it with
2679
+ * `useTexture` for loading (registry enrollment is on by default); `useTextures` is for access and lifecycle.
2657
2680
  *
2658
- * Textures are stored in a Map with URL/path keys for efficient lookup.
2659
- * Useful for sharing texture references across materials and components.
2681
+ * The returned handle is **reactive**: the calling component re-renders when the registry
2682
+ * changes. Pass a selector to scope the subscription to a single read.
2660
2683
  *
2661
2684
  * @example
2662
2685
  * ```tsx
2663
- * const { textures, add, get, remove, has, dispose } = useTextures()
2664
- *
2665
- * // Check if texture is already cached
2666
- * if (!has('/textures/diffuse.png')) {
2667
- * // Load with useTexture and cache: true, or manually add
2668
- * const tex = useTexture('/textures/diffuse.png', { cache: true })
2669
- * }
2686
+ * // Load + register once (anywhere in the tree) registered by default
2687
+ * useTexture({ map: '/diffuse.jpg', normalMap: '/normal.jpg' })
2670
2688
  *
2671
- * // Access cached texture from anywhere
2672
- * const diffuse = get('/textures/diffuse.png')
2673
- * if (diffuse) material.map = diffuse
2689
+ * // Reach them from another component — record form lands straight into a material
2690
+ * const { map, normalMap } = useTextures().get({ map: '/diffuse.jpg', normalMap: '/normal.jpg' })
2691
+ * return <meshStandardMaterial map={map} normalMap={normalMap} />
2674
2692
  *
2675
- * // Remove from cache only (texture still in GPU memory)
2676
- * remove('/textures/old.png')
2693
+ * // A set of heightmaps at once
2694
+ * const [a, b, c] = useTextures().get(['/h0.png', '/h1.png', '/h2.png'])
2677
2695
  *
2678
- * // Fully dispose (frees GPU memory + removes from cache)
2679
- * dispose('/textures/unused.png')
2696
+ * // Register a non-URL texture (render target / procedural)
2697
+ * useTextures().add('rt-main', renderTarget.texture)
2680
2698
  *
2681
- * // Nuclear option - dispose everything
2682
- * disposeAll()
2699
+ * // Deliberate GPU cleanup (refcount-aware; force to override)
2700
+ * useTextures().dispose('/diffuse.jpg')
2701
+ * useTextures().disposeAll() // scene teardown
2683
2702
  * ```
2703
+ *
2704
+ * @remarks
2705
+ * **Future expansion:** an imperative async loader (e.g. `await useTextures().load('/x.jpg')`) is
2706
+ * intentionally not included yet. It would let non-React code (loading screens, dynamic streaming)
2707
+ * populate the registry without Suspense, at the cost of putting loading back into this hook. It
2708
+ * will be added only if a concrete consumer needs imperative loading; for now, loading lives in
2709
+ * `useTexture`.
2684
2710
  */
2685
2711
  declare function useTextures(): UseTexturesReturn;
2712
+ declare function useTextures<T>(selector: (registry: UseTexturesReturn) => T): T;
2686
2713
 
2687
2714
  /**
2688
2715
  * Creates a render target compatible with the current renderer.
@@ -4379,4 +4406,4 @@ declare function isOnce(value: unknown): value is {
4379
4406
  declare function Canvas(props: CanvasProps): react_jsx_runtime.JSX.Element;
4380
4407
 
4381
4408
  export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, three_d as ReactThreeFiber, Scheduler, Texture, _roots, act, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getScheduler, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, reconciler, registerPrimary, removeInteractivity, resolve, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useEnvironment, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree, waitForPrimary };
4382
- export type { Act, AddPhaseOptions, Args, ArgsProp, AttachFnType, AttachType, BackgroundConfig, BackgroundProp, BaseRendererProps, Bridge, BufferLike, BufferRecord, BufferStore, Camera, CameraProps, CanvasProps, CanvasSchedulerConfig, Catalogue, Color, ColorManagementConfig, ComputeFunction, ConstructorRepresentation, DefaultGLProps, DefaultRendererProps, Disposable, DomEvent, Dpr, ElementProps, EnvironmentLoaderProps, EnvironmentProps, EquConfig, Euler, EventHandlers, EventManager, EventProps, Events, Extensions, FiberRoot, FilterFunction, FrameCallback, FrameControls, FrameNextCallback, FrameNextControls, FrameNextState, FrameState, FrameTimingState, Frameloop, GLProps, GLTFLike, GeometryProps, GeometryTransformProps, GlobalEffectType, GlobalRenderCallback, HostConfig, InferLoadResult, InjectState, InputLike, Instance, InstanceProps, InternalState, Intersection, IntersectionEvent, IsAllOptional, IsOptional, Layers, LegacyInternalState, LegacyRenderer, LegacyRootState, LoaderInstance, LoaderLike, LoaderResult, MappedTextureType, MathProps, MathRepresentation, MathType, MathTypes, Matrix3, Matrix4, Mutable, MutableOrReadonlyParameters, NodeProps, NonFunctionKeys, ObjectMap, OffscreenCanvas$1 as OffscreenCanvas, Overwrite, Performance, PointerCaptureTarget, PointerState, PresetsType, PrimaryCanvasEntry, Properties, Quaternion, R3FRenderer, RaycastableRepresentation, ReactProps, ReconcilerRoot, RenderCallback, RenderProps, RenderTargetOptions, Renderer, RendererConfigExtended, RendererFactory, RendererProps, Root, RootOptions, RootState, RootStore, SchedulerApi, SetBlock, Size, StorageLike, StorageRecord, StorageStore, Subscription, TSLNodeInput, TextureEntry, ThreeCamera, ThreeElement, ThreeElements, ThreeElementsImpl, ThreeEvent, ThreeExports, ThreeToJSXElements, UnblockProps, UseFrameNextOptions, UseFrameOptions, UseTextureOptions, UseTexturesReturn, Vector2, Vector3, Vector4, VectorRepresentation, Viewport, VisibilityEntry, WebGLDefaultProps, WebGLProps, WebGLShadowConfig, XRManager, XRPointerConfig };
4409
+ export type { Act, AddPhaseOptions, Args, ArgsProp, AttachFnType, AttachType, BackgroundConfig, BackgroundProp, BaseRendererProps, Bridge, BufferLike, BufferRecord, BufferStore, Camera, CameraProps, CanvasProps, CanvasSchedulerConfig, Catalogue, Color, ColorManagementConfig, ComputeFunction, ConstructorRepresentation, DefaultGLProps, DefaultRendererProps, Disposable, DisposeOptions, DomEvent, Dpr, ElementProps, EnvironmentLoaderProps, EnvironmentProps, EquConfig, Euler, EventHandlers, EventManager, EventProps, Events, Extensions, FiberRoot, FilterFunction, FrameCallback, FrameControls, FrameNextCallback, FrameNextControls, FrameNextState, FrameState, FrameTimingState, Frameloop, GLProps, GLTFLike, GeometryProps, GeometryTransformProps, GlobalEffectType, GlobalRenderCallback, HostConfig, InferLoadResult, InjectState, InputLike, Instance, InstanceProps, InternalState, Intersection, IntersectionEvent, IsAllOptional, IsOptional, Layers, LegacyInternalState, LegacyRenderer, LegacyRootState, LoaderInstance, LoaderLike, LoaderResult, MappedTextureType, MathProps, MathRepresentation, MathType, MathTypes, Matrix3, Matrix4, Mutable, MutableOrReadonlyParameters, NodeProps, NonFunctionKeys, ObjectMap, OffscreenCanvas$1 as OffscreenCanvas, Overwrite, Performance, PointerCaptureTarget, PointerState, PresetsType, PrimaryCanvasEntry, Properties, Quaternion, R3FRenderer, RaycastableRepresentation, ReactProps, ReconcilerRoot, RenderCallback, RenderProps, RenderTargetOptions, Renderer, RendererConfigExtended, RendererFactory, RendererProps, Root, RootOptions, RootState, RootStore, SchedulerApi, SetBlock, Size, StorageLike, StorageRecord, StorageStore, Subscription, TSLNodeInput, TextureInput, TextureResult, ThreeCamera, ThreeElement, ThreeElements, ThreeElementsImpl, ThreeEvent, ThreeExports, ThreeToJSXElements, UnblockProps, UseFrameNextOptions, UseFrameOptions, UseTextureOptions, UseTexturesReturn, Vector2, Vector3, Vector4, VectorRepresentation, Viewport, VisibilityEntry, WebGLDefaultProps, WebGLProps, WebGLShadowConfig, XRManager, XRPointerConfig };
package/dist/index.mjs CHANGED
@@ -2800,6 +2800,7 @@ const createStore = (invalidate, advance) => {
2800
2800
  buffers: {},
2801
2801
  gpuStorage: {},
2802
2802
  textures: /* @__PURE__ */ new Map(),
2803
+ _textureRefs: /* @__PURE__ */ new Map(),
2803
2804
  renderPipeline: null,
2804
2805
  passes: {},
2805
2806
  _hmrVersion: 0,
@@ -3163,18 +3164,18 @@ function buildFromCache(input, textureCache) {
3163
3164
  function useTexture(input, optionsOrOnLoad) {
3164
3165
  const renderer = useThree((state) => state.internal.actualRenderer);
3165
3166
  const store = useStore();
3166
- const textureCache = useThree((state) => state.textures);
3167
3167
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
3168
- const { onLoad, cache = false } = options;
3168
+ const { onLoad, cache = true } = options;
3169
3169
  const onLoadRef = useRef(onLoad);
3170
3170
  onLoadRef.current = onLoad;
3171
3171
  const onLoadCalledForRef = useRef(null);
3172
3172
  const urls = useMemo(() => getUrls(input), [input]);
3173
3173
  const cachedResult = useMemo(() => {
3174
3174
  if (!cache) return null;
3175
- if (!allUrlsCached(urls, textureCache)) return null;
3176
- return buildFromCache(input, textureCache);
3177
- }, [cache, urls, textureCache, input]);
3175
+ const textures = store.getState().textures;
3176
+ if (!allUrlsCached(urls, textures)) return null;
3177
+ return buildFromCache(input, textures);
3178
+ }, [cache, urls, input, store]);
3178
3179
  const loadedTextures = useLoader(
3179
3180
  TextureLoader,
3180
3181
  IsObject(input) ? Object.values(input) : input
@@ -3218,8 +3219,6 @@ function useTexture(input, optionsOrOnLoad) {
3218
3219
  }, [input, loadedTextures, cachedResult]);
3219
3220
  useEffect(() => {
3220
3221
  if (!cache) return;
3221
- if (cachedResult) return;
3222
- const set = store.setState;
3223
3222
  const urlTextureMap = [];
3224
3223
  if (typeof input === "string") {
3225
3224
  urlTextureMap.push([input, mappedTextures]);
@@ -3233,18 +3232,32 @@ function useTexture(input, optionsOrOnLoad) {
3233
3232
  urlTextureMap.push([url, textureRecord[key]]);
3234
3233
  }
3235
3234
  }
3236
- set((state) => {
3237
- const newMap = new Map(state.textures);
3238
- let changed = false;
3235
+ store.setState((state) => {
3236
+ const refs = new Map(state._textureRefs);
3237
+ let textures = state.textures;
3238
+ let added = false;
3239
3239
  for (const [url, texture] of urlTextureMap) {
3240
- if (!newMap.has(url)) {
3241
- newMap.set(url, texture);
3242
- changed = true;
3240
+ if (!textures.has(url)) {
3241
+ if (!added) {
3242
+ textures = new Map(textures);
3243
+ added = true;
3244
+ }
3245
+ textures.set(url, texture);
3243
3246
  }
3247
+ refs.set(url, (refs.get(url) ?? 0) + 1);
3244
3248
  }
3245
- return changed ? { textures: newMap } : state;
3249
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
3246
3250
  });
3247
- }, [cache, input, mappedTextures, store, cachedResult]);
3251
+ return () => store.setState((state) => {
3252
+ const refs = new Map(state._textureRefs);
3253
+ for (const [url] of urlTextureMap) {
3254
+ const next = (refs.get(url) ?? 0) - 1;
3255
+ if (next <= 0) refs.delete(url);
3256
+ else refs.set(url, next);
3257
+ }
3258
+ return { _textureRefs: refs };
3259
+ });
3260
+ }, [cache, input, mappedTextures, store]);
3248
3261
  return mappedTextures;
3249
3262
  }
3250
3263
  useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
@@ -3260,96 +3273,63 @@ const Texture = ({
3260
3273
  return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
3261
3274
  };
3262
3275
 
3263
- function getTextureValue(entry) {
3264
- if (entry instanceof Texture$1) return entry;
3265
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof Texture$1) {
3266
- return entry.value;
3267
- }
3268
- return null;
3269
- }
3270
- function useTextures() {
3276
+ function useTextures(selector) {
3271
3277
  const store = useStore();
3272
- return useMemo(() => {
3273
- const set = store.setState;
3278
+ const registry = useMemo(() => {
3274
3279
  const getState = store.getState;
3275
- const add = (key, value) => {
3276
- set((state) => {
3277
- const newMap = new Map(state.textures);
3278
- newMap.set(key, value);
3279
- return { textures: newMap };
3280
- });
3281
- };
3282
- const addMultiple = (items) => {
3283
- set((state) => {
3284
- const newMap = new Map(state.textures);
3285
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
3286
- for (const [key, value] of entries) {
3287
- newMap.set(key, value);
3288
- }
3289
- return { textures: newMap };
3290
- });
3291
- };
3292
- const remove = (key) => {
3293
- set((state) => {
3294
- const newMap = new Map(state.textures);
3295
- newMap.delete(key);
3296
- return { textures: newMap };
3297
- });
3298
- };
3299
- const removeMultiple = (keys) => {
3300
- set((state) => {
3301
- const newMap = new Map(state.textures);
3302
- for (const key of keys) newMap.delete(key);
3303
- return { textures: newMap };
3304
- });
3305
- };
3306
- const dispose = (key) => {
3307
- const entry = getState().textures.get(key);
3308
- if (entry) {
3309
- const tex = getTextureValue(entry);
3310
- tex?.dispose();
3311
- }
3312
- remove(key);
3313
- };
3314
- const disposeMultiple = (keys) => {
3315
- const textures = getState().textures;
3316
- for (const key of keys) {
3317
- const entry = textures.get(key);
3318
- if (entry) {
3319
- const tex = getTextureValue(entry);
3320
- tex?.dispose();
3321
- }
3322
- }
3323
- removeMultiple(keys);
3324
- };
3325
- const disposeAll = () => {
3326
- const textures = getState().textures;
3327
- for (const entry of textures.values()) {
3328
- const tex = getTextureValue(entry);
3329
- tex?.dispose();
3330
- }
3331
- set({ textures: /* @__PURE__ */ new Map() });
3332
- };
3280
+ const setState = store.setState;
3281
+ const getOne = (key) => getState().textures.get(key);
3333
3282
  return {
3334
- // Getter for the textures Map (reactive via getState)
3335
- get textures() {
3283
+ get all() {
3336
3284
  return getState().textures;
3337
3285
  },
3338
- // Read
3339
- get: (key) => getState().textures.get(key),
3286
+ get(input) {
3287
+ if (typeof input === "string") return getOne(input);
3288
+ if (Array.isArray(input)) return input.map(getOne);
3289
+ const out = {};
3290
+ for (const name in input) out[name] = getOne(input[name]);
3291
+ return out;
3292
+ },
3340
3293
  has: (key) => getState().textures.has(key),
3341
- // Write
3342
- add,
3343
- addMultiple,
3344
- // Remove (cache only)
3345
- remove,
3346
- removeMultiple,
3347
- // Dispose (GPU + cache)
3348
- dispose,
3349
- disposeMultiple,
3350
- disposeAll
3294
+ add(keyOrRecord, texture) {
3295
+ setState((state) => {
3296
+ const textures = new Map(state.textures);
3297
+ if (typeof keyOrRecord === "string") {
3298
+ textures.set(keyOrRecord, texture);
3299
+ } else {
3300
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
3301
+ }
3302
+ return { textures };
3303
+ });
3304
+ },
3305
+ dispose(key, options) {
3306
+ const state = getState();
3307
+ const refs = state._textureRefs.get(key) ?? 0;
3308
+ if (refs > 0 && !options?.force) {
3309
+ console.warn(
3310
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
3311
+ );
3312
+ return false;
3313
+ }
3314
+ state.textures.get(key)?.dispose();
3315
+ setState((s) => {
3316
+ const textures = new Map(s.textures);
3317
+ textures.delete(key);
3318
+ const nextRefs = new Map(s._textureRefs);
3319
+ nextRefs.delete(key);
3320
+ return { textures, _textureRefs: nextRefs };
3321
+ });
3322
+ return true;
3323
+ },
3324
+ disposeAll() {
3325
+ for (const texture of getState().textures.values()) texture.dispose();
3326
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
3327
+ }
3351
3328
  };
3352
3329
  }, [store]);
3330
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
3331
+ const selected = useThree(subscribe);
3332
+ return selector ? selected : registry;
3353
3333
  }
3354
3334
 
3355
3335
  function useRenderTarget(widthOrOptions, heightOrOptions, options) {
package/dist/legacy.cjs CHANGED
@@ -2830,6 +2830,7 @@ const createStore = (invalidate, advance) => {
2830
2830
  buffers: {},
2831
2831
  gpuStorage: {},
2832
2832
  textures: /* @__PURE__ */ new Map(),
2833
+ _textureRefs: /* @__PURE__ */ new Map(),
2833
2834
  renderPipeline: null,
2834
2835
  passes: {},
2835
2836
  _hmrVersion: 0,
@@ -3193,18 +3194,18 @@ function buildFromCache(input, textureCache) {
3193
3194
  function useTexture(input, optionsOrOnLoad) {
3194
3195
  const renderer = useThree((state) => state.internal.actualRenderer);
3195
3196
  const store = useStore();
3196
- const textureCache = useThree((state) => state.textures);
3197
3197
  const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
3198
- const { onLoad, cache = false } = options;
3198
+ const { onLoad, cache = true } = options;
3199
3199
  const onLoadRef = React.useRef(onLoad);
3200
3200
  onLoadRef.current = onLoad;
3201
3201
  const onLoadCalledForRef = React.useRef(null);
3202
3202
  const urls = React.useMemo(() => getUrls(input), [input]);
3203
3203
  const cachedResult = React.useMemo(() => {
3204
3204
  if (!cache) return null;
3205
- if (!allUrlsCached(urls, textureCache)) return null;
3206
- return buildFromCache(input, textureCache);
3207
- }, [cache, urls, textureCache, input]);
3205
+ const textures = store.getState().textures;
3206
+ if (!allUrlsCached(urls, textures)) return null;
3207
+ return buildFromCache(input, textures);
3208
+ }, [cache, urls, input, store]);
3208
3209
  const loadedTextures = useLoader(
3209
3210
  three.TextureLoader,
3210
3211
  IsObject(input) ? Object.values(input) : input
@@ -3248,8 +3249,6 @@ function useTexture(input, optionsOrOnLoad) {
3248
3249
  }, [input, loadedTextures, cachedResult]);
3249
3250
  React.useEffect(() => {
3250
3251
  if (!cache) return;
3251
- if (cachedResult) return;
3252
- const set = store.setState;
3253
3252
  const urlTextureMap = [];
3254
3253
  if (typeof input === "string") {
3255
3254
  urlTextureMap.push([input, mappedTextures]);
@@ -3263,18 +3262,32 @@ function useTexture(input, optionsOrOnLoad) {
3263
3262
  urlTextureMap.push([url, textureRecord[key]]);
3264
3263
  }
3265
3264
  }
3266
- set((state) => {
3267
- const newMap = new Map(state.textures);
3268
- let changed = false;
3265
+ store.setState((state) => {
3266
+ const refs = new Map(state._textureRefs);
3267
+ let textures = state.textures;
3268
+ let added = false;
3269
3269
  for (const [url, texture] of urlTextureMap) {
3270
- if (!newMap.has(url)) {
3271
- newMap.set(url, texture);
3272
- changed = true;
3270
+ if (!textures.has(url)) {
3271
+ if (!added) {
3272
+ textures = new Map(textures);
3273
+ added = true;
3274
+ }
3275
+ textures.set(url, texture);
3273
3276
  }
3277
+ refs.set(url, (refs.get(url) ?? 0) + 1);
3274
3278
  }
3275
- return changed ? { textures: newMap } : state;
3279
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
3276
3280
  });
3277
- }, [cache, input, mappedTextures, store, cachedResult]);
3281
+ return () => store.setState((state) => {
3282
+ const refs = new Map(state._textureRefs);
3283
+ for (const [url] of urlTextureMap) {
3284
+ const next = (refs.get(url) ?? 0) - 1;
3285
+ if (next <= 0) refs.delete(url);
3286
+ else refs.set(url, next);
3287
+ }
3288
+ return { _textureRefs: refs };
3289
+ });
3290
+ }, [cache, input, mappedTextures, store]);
3278
3291
  return mappedTextures;
3279
3292
  }
3280
3293
  useTexture.preload = (url) => useLoader.preload(three.TextureLoader, url);
@@ -3290,96 +3303,63 @@ const Texture = ({
3290
3303
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children?.(ret) });
3291
3304
  };
3292
3305
 
3293
- function getTextureValue(entry) {
3294
- if (entry instanceof three.Texture) return entry;
3295
- if (entry && typeof entry === "object" && "value" in entry && entry.value instanceof three.Texture) {
3296
- return entry.value;
3297
- }
3298
- return null;
3299
- }
3300
- function useTextures() {
3306
+ function useTextures(selector) {
3301
3307
  const store = useStore();
3302
- return React.useMemo(() => {
3303
- const set = store.setState;
3308
+ const registry = React.useMemo(() => {
3304
3309
  const getState = store.getState;
3305
- const add = (key, value) => {
3306
- set((state) => {
3307
- const newMap = new Map(state.textures);
3308
- newMap.set(key, value);
3309
- return { textures: newMap };
3310
- });
3311
- };
3312
- const addMultiple = (items) => {
3313
- set((state) => {
3314
- const newMap = new Map(state.textures);
3315
- const entries = items instanceof Map ? items.entries() : Object.entries(items);
3316
- for (const [key, value] of entries) {
3317
- newMap.set(key, value);
3318
- }
3319
- return { textures: newMap };
3320
- });
3321
- };
3322
- const remove = (key) => {
3323
- set((state) => {
3324
- const newMap = new Map(state.textures);
3325
- newMap.delete(key);
3326
- return { textures: newMap };
3327
- });
3328
- };
3329
- const removeMultiple = (keys) => {
3330
- set((state) => {
3331
- const newMap = new Map(state.textures);
3332
- for (const key of keys) newMap.delete(key);
3333
- return { textures: newMap };
3334
- });
3335
- };
3336
- const dispose = (key) => {
3337
- const entry = getState().textures.get(key);
3338
- if (entry) {
3339
- const tex = getTextureValue(entry);
3340
- tex?.dispose();
3341
- }
3342
- remove(key);
3343
- };
3344
- const disposeMultiple = (keys) => {
3345
- const textures = getState().textures;
3346
- for (const key of keys) {
3347
- const entry = textures.get(key);
3348
- if (entry) {
3349
- const tex = getTextureValue(entry);
3350
- tex?.dispose();
3351
- }
3352
- }
3353
- removeMultiple(keys);
3354
- };
3355
- const disposeAll = () => {
3356
- const textures = getState().textures;
3357
- for (const entry of textures.values()) {
3358
- const tex = getTextureValue(entry);
3359
- tex?.dispose();
3360
- }
3361
- set({ textures: /* @__PURE__ */ new Map() });
3362
- };
3310
+ const setState = store.setState;
3311
+ const getOne = (key) => getState().textures.get(key);
3363
3312
  return {
3364
- // Getter for the textures Map (reactive via getState)
3365
- get textures() {
3313
+ get all() {
3366
3314
  return getState().textures;
3367
3315
  },
3368
- // Read
3369
- get: (key) => getState().textures.get(key),
3316
+ get(input) {
3317
+ if (typeof input === "string") return getOne(input);
3318
+ if (Array.isArray(input)) return input.map(getOne);
3319
+ const out = {};
3320
+ for (const name in input) out[name] = getOne(input[name]);
3321
+ return out;
3322
+ },
3370
3323
  has: (key) => getState().textures.has(key),
3371
- // Write
3372
- add,
3373
- addMultiple,
3374
- // Remove (cache only)
3375
- remove,
3376
- removeMultiple,
3377
- // Dispose (GPU + cache)
3378
- dispose,
3379
- disposeMultiple,
3380
- disposeAll
3324
+ add(keyOrRecord, texture) {
3325
+ setState((state) => {
3326
+ const textures = new Map(state.textures);
3327
+ if (typeof keyOrRecord === "string") {
3328
+ textures.set(keyOrRecord, texture);
3329
+ } else {
3330
+ for (const key in keyOrRecord) textures.set(key, keyOrRecord[key]);
3331
+ }
3332
+ return { textures };
3333
+ });
3334
+ },
3335
+ dispose(key, options) {
3336
+ const state = getState();
3337
+ const refs = state._textureRefs.get(key) ?? 0;
3338
+ if (refs > 0 && !options?.force) {
3339
+ console.warn(
3340
+ `[useTextures] "${key}" still has ${refs} active reference(s); skipping dispose. Pass { force: true } to override.`
3341
+ );
3342
+ return false;
3343
+ }
3344
+ state.textures.get(key)?.dispose();
3345
+ setState((s) => {
3346
+ const textures = new Map(s.textures);
3347
+ textures.delete(key);
3348
+ const nextRefs = new Map(s._textureRefs);
3349
+ nextRefs.delete(key);
3350
+ return { textures, _textureRefs: nextRefs };
3351
+ });
3352
+ return true;
3353
+ },
3354
+ disposeAll() {
3355
+ for (const texture of getState().textures.values()) texture.dispose();
3356
+ setState({ textures: /* @__PURE__ */ new Map(), _textureRefs: /* @__PURE__ */ new Map() });
3357
+ }
3381
3358
  };
3382
3359
  }, [store]);
3360
+ const subscribe = selector ? () => selector(registry) : (state) => state.textures;
3361
+ const selected = useThree(subscribe);
3362
+ return selector ? selected : registry;
3383
3363
  }
3384
3364
 
3385
3365
  function useRenderTarget(widthOrOptions, heightOrOptions, options) {