@needle-tools/gltf-progressive 3.4.0-next.dbcee85 → 3.5.0-canary.4d42385

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.
@@ -42,8 +42,31 @@ type TextureLODsMinMaxInfo = {
42
42
  export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
43
43
  /** The name of the extension */
44
44
  get name(): string;
45
+ /**
46
+ * Get the progressive mesh LOD extension data associated with a geometry.
47
+ * Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
48
+ *
49
+ * @param geo - The buffer geometry to look up.
50
+ * @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
51
+ */
45
52
  static getMeshLODExtension(geo: BufferGeometry): NEEDLE_ext_progressive_mesh | null;
53
+ /**
54
+ * Get the glTF primitive index for a geometry within its parent mesh.
55
+ * A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
56
+ *
57
+ * @param geo - The buffer geometry to look up.
58
+ * @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
59
+ */
46
60
  static getPrimitiveIndex(geo: BufferGeometry): number;
61
+ /**
62
+ * Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
63
+ * Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
64
+ * Results are cached on the material so subsequent calls are free.
65
+ *
66
+ * @param material - A single material or an array of materials to inspect.
67
+ * @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
68
+ * @returns An object with `min_count` / `max_count` (the range of LOD levels across all textures) and a per-level `lods` array with `min_height` / `max_height`.
69
+ */
47
70
  static getMaterialMinMaxLODsCount(material: Material | Material[], minmax?: TextureLODsMinMaxInfo): TextureLODsMinMaxInfo;
48
71
  /** Check if a LOD level is available for a mesh or a texture
49
72
  * @param obj the mesh or texture to check
@@ -76,6 +99,12 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
76
99
  static assignTextureLOD(materialOrTexture: Material, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
77
100
  static assignTextureLOD(materialOrTexture: Mesh, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
78
101
  static assignTextureLOD(materialOrTexture: Texture, level: number): Promise<Texture | null>;
102
+ /**
103
+ * Set the maximum number of concurrent loading tasks for LOD resources. This limits how many LOD resources (meshes or textures) can be loaded at the same time to prevent overloading the network or GPU. If the limit is reached, additional loading requests will be queued and processed as previous ones finish.
104
+ * @default 50 on desktop, 20 on mobile devices
105
+ */
106
+ static set maxConcurrentLoadingTasks(value: number);
107
+ static get maxConcurrentLoadingTasks(): number;
79
108
  private static assignTextureLODForSlot;
80
109
  private readonly parser;
81
110
  private readonly url;
@@ -84,17 +113,36 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
84
113
  loadMesh: (meshIndex: number) => Promise<any> | null;
85
114
  afterRoot(gltf: GLTF): null;
86
115
  /**
87
- * Register a texture with LOD information
116
+ * Register a texture with progressive LOD information. This associates the texture with its LOD extension data
117
+ * so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
118
+ * Typically called during glTF loading when the progressive extension is parsed.
119
+ *
120
+ * @param url - The source URL of the glTF file this texture was loaded from.
121
+ * @param tex - The three.js Texture instance to register.
122
+ * @param level - The LOD level this texture represents (0 = highest resolution).
123
+ * @param index - The texture index within the glTF file.
124
+ * @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
88
125
  */
89
126
  static registerTexture: (url: string, tex: Texture, level: number, index: number, ext: NEEDLE_ext_progressive_texture) => void;
90
127
  /**
91
- * Register a mesh with LOD information
128
+ * Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
129
+ * so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
130
+ * Typically called during glTF loading when the progressive extension is parsed.
131
+ * If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
132
+ *
133
+ * @param url - The source URL of the glTF file this mesh was loaded from.
134
+ * @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
135
+ * @param mesh - The three.js Mesh instance to register.
136
+ * @param level - The LOD level this mesh represents (0 = highest resolution / full density).
137
+ * @param index - The primitive index within the glTF mesh node.
138
+ * @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
92
139
  */
93
140
  static registerMesh: (url: string, key: string, mesh: Mesh, level: number, index: number, ext: NEEDLE_ext_progressive_mesh) => void;
94
141
  /**
95
142
  * Dispose cached resources to free memory.
96
143
  * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
97
144
  * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
145
+ * Also clears reference counts for disposed textures.
98
146
  * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
99
147
  */
100
148
  static dispose(guid?: string): void;
@@ -106,17 +154,29 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
106
154
  private static readonly cache;
107
155
  /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
108
156
  private static readonly lowresCache;
157
+ /** Reference counting for textures to track usage across multiple materials/objects */
158
+ private static readonly textureRefCounts;
109
159
  /**
110
160
  * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
111
161
  * when their associated three.js resources are garbage collected by the browser.
112
162
  * The held value is the cache key string used in `previouslyLoaded`.
113
163
  */
114
164
  private static readonly _resourceRegistry;
165
+ /**
166
+ * Track texture usage by incrementing reference count
167
+ */
168
+ private static trackTextureUsage;
169
+ /**
170
+ * Untrack texture usage by decrementing reference count.
171
+ * Automatically disposes the texture when reference count reaches zero.
172
+ * @returns true if the texture was disposed, false otherwise
173
+ */
174
+ private static untrackTextureUsage;
115
175
  private static readonly workers;
116
176
  private static _workersIndex;
117
177
  private static getOrLoadLOD;
118
- private static maxConcurrent;
119
- private static queue;
178
+ private static _queue;
179
+ private static get queue();
120
180
  private static assignLODInformation;
121
181
  private static getAssignedLODInformation;
122
182
  private static copySettings;
package/lib/extension.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
2
2
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { addDracoAndKTX2Loaders } from "./loaders.js";
4
- import { getParam, PromiseQueue, resolveUrl } from "./utils.internal.js";
4
+ import { determineTextureMemoryInBytes, getParam, isMobileDevice, PromiseQueue, resolveUrl } from "./utils.internal.js";
5
5
  import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
6
6
  // All of this has to be removed
7
7
  // import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
@@ -11,6 +11,7 @@ import { debug } from "./lods.debug.js";
11
11
  import { getWorker } from "./worker/loader.mainthread.js";
12
12
  const useWorker = getParam("gltf-progressive-worker");
13
13
  const reduceMipmaps = getParam("gltf-progressive-reduce-mipmaps");
14
+ const debugGC = getParam("gltf-progressive-gc");
14
15
  const $progressiveTextureExtension = Symbol("needle-progressive-texture");
15
16
  export const EXTENSION_NAME = "NEEDLE_progressive";
16
17
  // #region EXT
@@ -35,6 +36,13 @@ export class NEEDLE_progressive {
35
36
  return EXTENSION_NAME;
36
37
  }
37
38
  // #region PUBLIC API
39
+ /**
40
+ * Get the progressive mesh LOD extension data associated with a geometry.
41
+ * Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
42
+ *
43
+ * @param geo - The buffer geometry to look up.
44
+ * @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
45
+ */
38
46
  static getMeshLODExtension(geo) {
39
47
  const info = this.getAssignedLODInformation(geo);
40
48
  if (info?.key) {
@@ -42,12 +50,28 @@ export class NEEDLE_progressive {
42
50
  }
43
51
  return null;
44
52
  }
53
+ /**
54
+ * Get the glTF primitive index for a geometry within its parent mesh.
55
+ * A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
56
+ *
57
+ * @param geo - The buffer geometry to look up.
58
+ * @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
59
+ */
45
60
  static getPrimitiveIndex(geo) {
46
61
  const index = this.getAssignedLODInformation(geo)?.index;
47
62
  if (index === undefined || index === null)
48
63
  return -1;
49
64
  return index;
50
65
  }
66
+ /**
67
+ * Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
68
+ * Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
69
+ * Results are cached on the material so subsequent calls are free.
70
+ *
71
+ * @param material - A single material or an array of materials to inspect.
72
+ * @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
73
+ * @returns An object with `min_count` / `max_count` (the range of LOD levels across all textures) and a per-level `lods` array with `min_height` / `max_height`.
74
+ */
51
75
  static getMaterialMinMaxLODsCount(material, minmax) {
52
76
  const self = this;
53
77
  // we can cache this material min max data because it wont change at runtime
@@ -309,6 +333,16 @@ export class NEEDLE_progressive {
309
333
  }
310
334
  return Promise.resolve(null);
311
335
  }
336
+ /**
337
+ * Set the maximum number of concurrent loading tasks for LOD resources. This limits how many LOD resources (meshes or textures) can be loaded at the same time to prevent overloading the network or GPU. If the limit is reached, additional loading requests will be queued and processed as previous ones finish.
338
+ * @default 50 on desktop, 20 on mobile devices
339
+ */
340
+ static set maxConcurrentLoadingTasks(value) {
341
+ NEEDLE_progressive.queue.maxConcurrent = value;
342
+ }
343
+ static get maxConcurrentLoadingTasks() {
344
+ return NEEDLE_progressive.queue.maxConcurrent;
345
+ }
312
346
  // #region INTERNAL
313
347
  static assignTextureLODForSlot(current, level, material, slot) {
314
348
  if (current?.isTexture !== true) {
@@ -334,29 +368,37 @@ export class NEEDLE_progressive {
334
368
  if (assignedLOD && assignedLOD?.level < level) {
335
369
  if (debug === "verbose")
336
370
  console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
371
+ // Dispose the newly loaded texture since we're not using it
372
+ // (the assigned texture is higher quality, so we reject the new one)
373
+ // Note: We dispose directly here (not via untrackTextureUsage) because this texture
374
+ // was never tracked/used - it was rejected immediately upon loading
375
+ if (tex && tex !== assigned) {
376
+ if (debug || debugGC) {
377
+ console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
378
+ }
379
+ tex.dispose();
380
+ }
337
381
  return null;
338
382
  }
339
383
  // assigned.dispose();
340
384
  }
341
- // Since we're switching LOD level for the texture based on distance we can avoid uploading all the mipmaps
342
- if (reduceMipmaps && tex.mipmaps) {
343
- const prevCount = tex.mipmaps.length;
344
- tex.mipmaps.length = Math.min(tex.mipmaps.length, 3);
345
- if (prevCount !== tex.mipmaps.length) {
346
- if (debug)
347
- console.debug(`Reduced mipmap count from ${prevCount} to ${tex.mipmaps.length} for ${tex.uuid}: ${tex.image?.width}x${tex.image?.height}.`);
385
+ // Track reference count for new texture
386
+ this.trackTextureUsage(tex);
387
+ // Untrack the old texture (may dispose if ref count hits 0)
388
+ // This prevents accumulation of GPU VRAM while waiting for garbage collection
389
+ if (assigned && assigned !== tex) {
390
+ const wasDisposed = this.untrackTextureUsage(assigned);
391
+ if (wasDisposed && (debug || debugGC)) {
392
+ const assignedLOD = this.getAssignedLODInformation(assigned);
393
+ console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
348
394
  }
349
395
  }
350
396
  material[slot] = tex;
351
397
  }
352
- // check if the old texture is still used by other objects
353
- // if not we dispose it...
354
- // this could also be handled elsewhere and not be done immediately
355
- // const users = getResourceUserCount(current);
356
- // if (!users) {
357
- // if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
358
- // current?.dispose();
359
- // }
398
+ // Note: We use reference counting above to track texture usage across multiple materials.
399
+ // When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
400
+ // not waiting for JavaScript garbage collection which may take seconds/minutes.
401
+ // This handles cases where the same texture is shared across multiple materials/objects.
360
402
  }
361
403
  // this.onProgressiveLoadEnd(info);
362
404
  return tex;
@@ -473,7 +515,15 @@ export class NEEDLE_progressive {
473
515
  return null;
474
516
  }
475
517
  /**
476
- * Register a texture with LOD information
518
+ * Register a texture with progressive LOD information. This associates the texture with its LOD extension data
519
+ * so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
520
+ * Typically called during glTF loading when the progressive extension is parsed.
521
+ *
522
+ * @param url - The source URL of the glTF file this texture was loaded from.
523
+ * @param tex - The three.js Texture instance to register.
524
+ * @param level - The LOD level this texture represents (0 = highest resolution).
525
+ * @param index - The texture index within the glTF file.
526
+ * @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
477
527
  */
478
528
  static registerTexture = (url, tex, level, index, ext) => {
479
529
  if (!tex) {
@@ -496,7 +546,17 @@ export class NEEDLE_progressive {
496
546
  NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
497
547
  };
498
548
  /**
499
- * Register a mesh with LOD information
549
+ * Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
550
+ * so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
551
+ * Typically called during glTF loading when the progressive extension is parsed.
552
+ * If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
553
+ *
554
+ * @param url - The source URL of the glTF file this mesh was loaded from.
555
+ * @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
556
+ * @param mesh - The three.js Mesh instance to register.
557
+ * @param level - The LOD level this mesh represents (0 = highest resolution / full density).
558
+ * @param index - The primitive index within the glTF mesh node.
559
+ * @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
500
560
  */
501
561
  static registerMesh = (url, key, mesh, level, index, ext) => {
502
562
  const geometry = mesh.geometry;
@@ -531,6 +591,7 @@ export class NEEDLE_progressive {
531
591
  * Dispose cached resources to free memory.
532
592
  * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
533
593
  * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
594
+ * Also clears reference counts for disposed textures.
534
595
  * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
535
596
  */
536
597
  static dispose(guid) {
@@ -542,7 +603,9 @@ export class NEEDLE_progressive {
542
603
  const lowres = lowresRef.deref();
543
604
  if (lowres) {
544
605
  if (lowres.isTexture) {
545
- lowres.dispose();
606
+ const tex = lowres;
607
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
608
+ tex.dispose();
546
609
  }
547
610
  else if (Array.isArray(lowres)) {
548
611
  for (const geo of lowres)
@@ -565,7 +628,9 @@ export class NEEDLE_progressive {
565
628
  const entry = entryRef.deref();
566
629
  if (entry) {
567
630
  if (entry.isTexture) {
568
- entry.dispose();
631
+ const tex = entry;
632
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
633
+ tex.dispose();
569
634
  }
570
635
  else if (Array.isArray(entry)) {
571
636
  for (const geo of entry)
@@ -578,6 +643,8 @@ export class NEEDLE_progressive {
578
643
  this._disposeCacheEntry(entry);
579
644
  }
580
645
  this.cache.clear();
646
+ // Clear all texture reference counts when disposing everything
647
+ this.textureRefCounts.clear();
581
648
  }
582
649
  }
583
650
  /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
@@ -585,7 +652,13 @@ export class NEEDLE_progressive {
585
652
  if (entry instanceof WeakRef) {
586
653
  // Single resource — deref and dispose if still alive
587
654
  const resource = entry.deref();
588
- resource?.dispose();
655
+ if (resource) {
656
+ // Clear ref count for textures
657
+ if (resource.isTexture) {
658
+ this.textureRefCounts.delete(resource.uuid);
659
+ }
660
+ resource.dispose();
661
+ }
589
662
  }
590
663
  else {
591
664
  // Promise — may be in-flight or already resolved.
@@ -597,6 +670,10 @@ export class NEEDLE_progressive {
597
670
  geo.dispose();
598
671
  }
599
672
  else {
673
+ // Clear ref count for textures
674
+ if (resource.isTexture) {
675
+ this.textureRefCounts.delete(resource.uuid);
676
+ }
600
677
  resource.dispose();
601
678
  }
602
679
  }
@@ -609,6 +686,8 @@ export class NEEDLE_progressive {
609
686
  static cache = new Map();
610
687
  /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
611
688
  static lowresCache = new Map();
689
+ /** Reference counting for textures to track usage across multiple materials/objects */
690
+ static textureRefCounts = new Map();
612
691
  /**
613
692
  * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
614
693
  * when their associated three.js resources are garbage collected by the browser.
@@ -616,18 +695,72 @@ export class NEEDLE_progressive {
616
695
  */
617
696
  static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
618
697
  const entry = NEEDLE_progressive.cache.get(cacheKey);
619
- if (debug)
620
- console.debug(`[gltf-progressive] Resource GC'd\n${cacheKey}`);
698
+ if (debug || debugGC)
699
+ console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
621
700
  // Only delete if the entry is still a WeakRef and the resource is gone
622
701
  if (entry instanceof WeakRef) {
623
702
  const derefed = entry.deref();
624
703
  if (!derefed) {
625
704
  NEEDLE_progressive.cache.delete(cacheKey);
626
- if (debug)
627
- console.log(`[gltf-progressive] Cache entry auto-cleaned (GC'd): ${cacheKey}`);
705
+ if (debug || debugGC)
706
+ console.log(`[gltf-progressive] Cache entry deleted (GC)`);
628
707
  }
629
708
  }
630
709
  });
710
+ /**
711
+ * Track texture usage by incrementing reference count
712
+ */
713
+ static trackTextureUsage(texture) {
714
+ const uuid = texture.uuid;
715
+ const count = this.textureRefCounts.get(uuid) || 0;
716
+ this.textureRefCounts.set(uuid, count + 1);
717
+ if (debug === "verbose") {
718
+ console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
719
+ }
720
+ }
721
+ /**
722
+ * Untrack texture usage by decrementing reference count.
723
+ * Automatically disposes the texture when reference count reaches zero.
724
+ * @returns true if the texture was disposed, false otherwise
725
+ */
726
+ static untrackTextureUsage(texture) {
727
+ const uuid = texture.uuid;
728
+ const count = this.textureRefCounts.get(uuid);
729
+ if (!count) {
730
+ // Texture wasn't tracked, dispose immediately (safe fallback)
731
+ if (debug === "verbose" || debugGC) {
732
+ logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
733
+ }
734
+ texture.dispose();
735
+ return true;
736
+ }
737
+ const newCount = count - 1;
738
+ if (newCount <= 0) {
739
+ this.textureRefCounts.delete(uuid);
740
+ if (debug || debugGC) {
741
+ logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
742
+ }
743
+ texture.dispose();
744
+ return true;
745
+ }
746
+ else {
747
+ this.textureRefCounts.set(uuid, newCount);
748
+ if (debug === "verbose") {
749
+ logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
750
+ }
751
+ return false;
752
+ }
753
+ function logDebugInfo(prefix, newCount) {
754
+ let width = texture.image?.width || texture.source?.data?.width || 0;
755
+ let height = texture.image?.height || texture.source?.data?.height || 0;
756
+ const textureSize = width && height ? `${width}x${height}` : "N/A";
757
+ let memorySize = "N/A";
758
+ if (width && height) {
759
+ memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
760
+ }
761
+ console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
762
+ }
763
+ }
631
764
  static workers = [];
632
765
  static _workersIndex = 0;
633
766
  static async getOrLoadLOD(current, level) {
@@ -964,8 +1097,8 @@ export class NEEDLE_progressive {
964
1097
  }
965
1098
  return null;
966
1099
  }
967
- static maxConcurrent = 50;
968
- static queue = new PromiseQueue(NEEDLE_progressive.maxConcurrent, { debug: debug != false });
1100
+ static _queue;
1101
+ static get queue() { return this._queue ??= new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false }); }
969
1102
  static assignLODInformation(url, res, key, level, index) {
970
1103
  if (!res)
971
1104
  return;
@@ -98,7 +98,31 @@ export declare class LODsManager {
98
98
  private readonly _newPromiseGroups;
99
99
  private _promiseGroupIds;
100
100
  /**
101
- * Call to await LODs loading during the next render cycle.
101
+ * Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
102
+ * This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
103
+ *
104
+ * By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
105
+ * Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
106
+ *
107
+ * @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
108
+ * @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Wait for initial LODs to finish loading, then remove a blur overlay
113
+ * const result = await lodsManager.awaitLoading({
114
+ * frames: 5,
115
+ * signal: AbortSignal.timeout(10_000),
116
+ * });
117
+ * console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
118
+ * document.querySelector('.blur-overlay')?.remove();
119
+ * ```
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * // Wait until at least one LOD starts loading before resolving
124
+ * await lodsManager.awaitLoading({ waitForFirstCapture: true });
125
+ * ```
102
126
  */
103
127
  awaitLoading(opts?: PromiseGroupOptions): Promise<{
104
128
  cancelled: boolean;
@@ -107,8 +131,29 @@ export declare class LODsManager {
107
131
  }>;
108
132
  private _postprocessPromiseGroups;
109
133
  private readonly _lodchangedlisteners;
110
- addEventListener(evt: "changed", listener: LODChangedEventListener): void;
111
- removeEventListener(evt: "changed", listener: LODChangedEventListener): void;
134
+ /**
135
+ * Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
136
+ * The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
137
+ *
138
+ * @param evt - The event type. Currently only `"changed"` is supported.
139
+ * @param listener - Callback invoked after a LOD swap completes.
140
+ * @return A function to unregister the listener.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * lodsManager.addEventListener("changed", ({ type, level, object }) => {
145
+ * console.log(`${type} LOD changed to level ${level}`, object);
146
+ * });
147
+ * ```
148
+ */
149
+ addEventListener(evt: "changed", listener: LODChangedEventListener): () => void;
150
+ /**
151
+ * Remove a previously registered `"changed"` event listener.
152
+ * @param evt - The event type (`"changed"`).
153
+ * @param listener - The listener to remove.
154
+ * @return `true` if the listener was found and removed, `false` otherwise.
155
+ */
156
+ removeEventListener(evt: "changed", listener: LODChangedEventListener): boolean;
112
157
  private constructor();
113
158
  private _fpsBuffer;
114
159
  /**
@@ -116,6 +161,21 @@ export declare class LODsManager {
116
161
  */
117
162
  enable(): void;
118
163
  disable(): void;
164
+ /**
165
+ * Manually trigger a LOD update for a scene and camera.
166
+ * Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
167
+ *
168
+ * @param scene - The scene containing objects with progressive LODs.
169
+ * @param camera - The camera used to determine screen coverage and LOD levels.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const lodsManager = LODsManager.get(renderer);
174
+ * lodsManager.manual = true;
175
+ * // ... later, trigger an update at a specific point:
176
+ * lodsManager.update(scene, camera);
177
+ * ```
178
+ */
119
179
  update(scene: Scene, camera: Camera): void;
120
180
  private onAfterRender;
121
181
  /**
@@ -116,7 +116,31 @@ export class LODsManager {
116
116
  _newPromiseGroups = [];
117
117
  _promiseGroupIds = 0;
118
118
  /**
119
- * Call to await LODs loading during the next render cycle.
119
+ * Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
120
+ * This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
121
+ *
122
+ * By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
123
+ * Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
124
+ *
125
+ * @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
126
+ * @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // Wait for initial LODs to finish loading, then remove a blur overlay
131
+ * const result = await lodsManager.awaitLoading({
132
+ * frames: 5,
133
+ * signal: AbortSignal.timeout(10_000),
134
+ * });
135
+ * console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
136
+ * document.querySelector('.blur-overlay')?.remove();
137
+ * ```
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * // Wait until at least one LOD starts loading before resolving
142
+ * await lodsManager.awaitLoading({ waitForFirstCapture: true });
143
+ * ```
120
144
  */
121
145
  awaitLoading(opts) {
122
146
  const id = this._promiseGroupIds++;
@@ -145,17 +169,46 @@ export class LODsManager {
145
169
  }
146
170
  }
147
171
  _lodchangedlisteners = [];
172
+ /**
173
+ * Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
174
+ * The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
175
+ *
176
+ * @param evt - The event type. Currently only `"changed"` is supported.
177
+ * @param listener - Callback invoked after a LOD swap completes.
178
+ * @return A function to unregister the listener.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * lodsManager.addEventListener("changed", ({ type, level, object }) => {
183
+ * console.log(`${type} LOD changed to level ${level}`, object);
184
+ * });
185
+ * ```
186
+ */
148
187
  addEventListener(evt, listener) {
149
188
  if (evt === "changed") {
150
189
  this._lodchangedlisteners.push(listener);
190
+ return () => {
191
+ this.removeEventListener(evt, listener);
192
+ };
151
193
  }
194
+ return () => { };
152
195
  }
196
+ /**
197
+ * Remove a previously registered `"changed"` event listener.
198
+ * @param evt - The event type (`"changed"`).
199
+ * @param listener - The listener to remove.
200
+ * @return `true` if the listener was found and removed, `false` otherwise.
201
+ */
153
202
  removeEventListener(evt, listener) {
203
+ let removed = false;
154
204
  if (evt === "changed") {
155
205
  const index = this._lodchangedlisteners.indexOf(listener);
156
- if (index >= 0)
206
+ if (index >= 0) {
157
207
  this._lodchangedlisteners.splice(index, 1);
208
+ removed = true;
209
+ }
158
210
  }
211
+ return removed;
159
212
  }
160
213
  // readonly plugins: NEEDLE_progressive_plugin[] = [];
161
214
  constructor(renderer, context) {
@@ -217,6 +270,21 @@ export class LODsManager {
217
270
  this.renderer.render = this.#originalRender;
218
271
  this.#originalRender = undefined;
219
272
  }
273
+ /**
274
+ * Manually trigger a LOD update for a scene and camera.
275
+ * Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
276
+ *
277
+ * @param scene - The scene containing objects with progressive LODs.
278
+ * @param camera - The camera used to determine screen coverage and LOD levels.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * const lodsManager = LODsManager.get(renderer);
283
+ * lodsManager.manual = true;
284
+ * // ... later, trigger an update at a specific point:
285
+ * lodsManager.update(scene, camera);
286
+ * ```
287
+ */
220
288
  update(scene, camera) {
221
289
  this.internalUpdate(scene, camera);
222
290
  }
@@ -1,3 +1,4 @@
1
+ import { Texture } from "three";
1
2
  export declare function isDebugMode(): string | boolean;
2
3
  export declare function getParam(name: string): boolean | string;
3
4
  export declare function resolveUrl(source: string | undefined, uri: string): string;
@@ -18,11 +19,11 @@ export type SlotReturnValue<T = any> = {
18
19
  * Use the `slot` method to request a slot for a promise with a specific key. The returned promise resolves to an object with a `use` method that can be called to add the promise to the queue.
19
20
  */
20
21
  export declare class PromiseQueue<T = any> {
21
- readonly maxConcurrent: number;
22
+ maxConcurrent: number;
22
23
  private readonly _running;
23
24
  private readonly _queue;
24
25
  debug: boolean;
25
- constructor(maxConcurrent?: number, opts?: {
26
+ constructor(maxConcurrent: number, opts?: {
26
27
  debug?: boolean;
27
28
  });
28
29
  private tick;
@@ -33,3 +34,8 @@ export declare class PromiseQueue<T = any> {
33
34
  private add;
34
35
  private internalUpdate;
35
36
  }
37
+ export declare function determineTextureMemoryInBytes(texture: Texture): number;
38
+ /**
39
+ * Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
40
+ */
41
+ export declare function detectGPUMemory(): number | undefined;