@needle-tools/gltf-progressive 3.5.0-rc → 3.6.0-alpha.1

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.
@@ -24,6 +24,10 @@ type TextureLODsMinMaxInfo = {
24
24
  max_height: number;
25
25
  }>;
26
26
  };
27
+ export type AssignTextureLODOptions = {
28
+ /** Force the exact requested level. Intended for explicit/debug LOD overrides. */
29
+ force?: boolean;
30
+ };
27
31
  /**
28
32
  * The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
29
33
  * This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
@@ -42,8 +46,31 @@ type TextureLODsMinMaxInfo = {
42
46
  export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
43
47
  /** The name of the extension */
44
48
  get name(): string;
49
+ /**
50
+ * Get the progressive mesh LOD extension data associated with a geometry.
51
+ * Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
52
+ *
53
+ * @param geo - The buffer geometry to look up.
54
+ * @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
55
+ */
45
56
  static getMeshLODExtension(geo: BufferGeometry): NEEDLE_ext_progressive_mesh | null;
57
+ /**
58
+ * Get the glTF primitive index for a geometry within its parent mesh.
59
+ * A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
60
+ *
61
+ * @param geo - The buffer geometry to look up.
62
+ * @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
63
+ */
46
64
  static getPrimitiveIndex(geo: BufferGeometry): number;
65
+ /**
66
+ * Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
67
+ * Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
68
+ * Results are cached on the material so subsequent calls are free.
69
+ *
70
+ * @param material - A single material or an array of materials to inspect.
71
+ * @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
72
+ * @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`.
73
+ */
47
74
  static getMaterialMinMaxLODsCount(material: Material | Material[], minmax?: TextureLODsMinMaxInfo): TextureLODsMinMaxInfo;
48
75
  /** Check if a LOD level is available for a mesh or a texture
49
76
  * @param obj the mesh or texture to check
@@ -73,9 +100,9 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
73
100
  * @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
74
101
  * @returns a promise that resolves to the material or texture with the requested LOD level
75
102
  */
76
- static assignTextureLOD(materialOrTexture: Material, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
77
- static assignTextureLOD(materialOrTexture: Mesh, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
78
- static assignTextureLOD(materialOrTexture: Texture, level: number): Promise<Texture | null>;
103
+ static assignTextureLOD(materialOrTexture: Material, level: number, options?: AssignTextureLODOptions): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
104
+ static assignTextureLOD(materialOrTexture: Mesh, level: number, options?: AssignTextureLODOptions): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
105
+ static assignTextureLOD(materialOrTexture: Texture, level: number, options?: AssignTextureLODOptions): Promise<Texture | null>;
79
106
  /**
80
107
  * 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.
81
108
  * @default 50 on desktop, 20 on mobile devices
@@ -83,6 +110,16 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
83
110
  static set maxConcurrentLoadingTasks(value: number);
84
111
  static get maxConcurrentLoadingTasks(): number;
85
112
  private static assignTextureLODForSlot;
113
+ private static trackedTextureSlots;
114
+ private static pendingTextureSlotRequests;
115
+ private static trackCurrentMaterialTextureSlots;
116
+ private static getPendingTextureSlotRequest;
117
+ private static setPendingTextureSlotRequest;
118
+ private static getMaterialTextureSlot;
119
+ private static setMaterialTextureSlot;
120
+ private static assignTrackedTextureSlot;
121
+ private static ensureTrackedTextureSlot;
122
+ private static releaseTrackedTextureSlot;
86
123
  private readonly parser;
87
124
  private readonly url;
88
125
  constructor(parser: GLTFParser);
@@ -90,11 +127,29 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
90
127
  loadMesh: (meshIndex: number) => Promise<any> | null;
91
128
  afterRoot(gltf: GLTF): null;
92
129
  /**
93
- * Register a texture with LOD information
130
+ * Register a texture with progressive LOD information. This associates the texture with its LOD extension data
131
+ * so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
132
+ * Typically called during glTF loading when the progressive extension is parsed.
133
+ *
134
+ * @param url - The source URL of the glTF file this texture was loaded from.
135
+ * @param tex - The three.js Texture instance to register.
136
+ * @param level - The LOD level this texture represents (0 = highest resolution).
137
+ * @param index - The texture index within the glTF file.
138
+ * @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
94
139
  */
95
140
  static registerTexture: (url: string, tex: Texture, level: number, index: number, ext: NEEDLE_ext_progressive_texture) => void;
96
141
  /**
97
- * Register a mesh with LOD information
142
+ * Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
143
+ * so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
144
+ * Typically called during glTF loading when the progressive extension is parsed.
145
+ * If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
146
+ *
147
+ * @param url - The source URL of the glTF file this mesh was loaded from.
148
+ * @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
149
+ * @param mesh - The three.js Mesh instance to register.
150
+ * @param level - The LOD level this mesh represents (0 = highest resolution / full density).
151
+ * @param index - The primitive index within the glTF mesh node.
152
+ * @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
98
153
  */
99
154
  static registerMesh: (url: string, key: string, mesh: Mesh, level: number, index: number, ext: NEEDLE_ext_progressive_mesh) => void;
100
155
  /**
@@ -134,6 +189,7 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
134
189
  private static readonly workers;
135
190
  private static _workersIndex;
136
191
  private static getOrLoadLOD;
192
+ private static tryResolveLODCacheEntry;
137
193
  private static _queue;
138
194
  private static get queue();
139
195
  private static assignLODInformation;
package/lib/extension.js CHANGED
@@ -36,6 +36,13 @@ export class NEEDLE_progressive {
36
36
  return EXTENSION_NAME;
37
37
  }
38
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
+ */
39
46
  static getMeshLODExtension(geo) {
40
47
  const info = this.getAssignedLODInformation(geo);
41
48
  if (info?.key) {
@@ -43,12 +50,28 @@ export class NEEDLE_progressive {
43
50
  }
44
51
  return null;
45
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
+ */
46
60
  static getPrimitiveIndex(geo) {
47
61
  const index = this.getAssignedLODInformation(geo)?.index;
48
62
  if (index === undefined || index === null)
49
63
  return -1;
50
64
  return index;
51
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
+ */
52
75
  static getMaterialMinMaxLODsCount(material, minmax) {
53
76
  const self = this;
54
77
  // we can cache this material min max data because it wont change at runtime
@@ -231,15 +254,16 @@ export class NEEDLE_progressive {
231
254
  }
232
255
  return Promise.resolve(null);
233
256
  }
234
- static assignTextureLOD(materialOrTexture, level = 0) {
257
+ static assignTextureLOD(materialOrTexture, level = 0, options) {
235
258
  if (!materialOrTexture)
236
259
  return Promise.resolve(null);
260
+ const force = options?.force === true;
237
261
  if (materialOrTexture.isMesh === true) {
238
262
  const mesh = materialOrTexture;
239
263
  if (Array.isArray(mesh.material)) {
240
264
  const arr = new Array();
241
265
  for (const mat of mesh.material) {
242
- const promise = this.assignTextureLOD(mat, level);
266
+ const promise = this.assignTextureLOD(mat, level, options);
243
267
  arr.push(promise);
244
268
  }
245
269
  return Promise.all(arr).then(res => {
@@ -253,13 +277,14 @@ export class NEEDLE_progressive {
253
277
  });
254
278
  }
255
279
  else {
256
- return this.assignTextureLOD(mesh.material, level);
280
+ return this.assignTextureLOD(mesh.material, level, options);
257
281
  }
258
282
  }
259
283
  if (materialOrTexture.isMaterial === true) {
260
284
  const material = materialOrTexture;
261
285
  const promises = [];
262
286
  const slots = new Array();
287
+ this.trackCurrentMaterialTextureSlots(material);
263
288
  // Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
264
289
  if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
265
290
  // iterate uniforms of custom shaders
@@ -267,7 +292,7 @@ export class NEEDLE_progressive {
267
292
  for (const slot of Object.keys(shaderMaterial.uniforms)) {
268
293
  const val = shaderMaterial.uniforms[slot].value;
269
294
  if (val?.isTexture === true) {
270
- const task = this.assignTextureLODForSlot(val, level, material, slot).then(res => {
295
+ const task = this.assignTextureLODForSlot(val, level, material, slot, force).then(res => {
271
296
  if (res && shaderMaterial.uniforms[slot].value != res) {
272
297
  shaderMaterial.uniforms[slot].value = res;
273
298
  shaderMaterial.uniformsNeedUpdate = true;
@@ -283,7 +308,7 @@ export class NEEDLE_progressive {
283
308
  for (const slot of Object.keys(material)) {
284
309
  const val = material[slot];
285
310
  if (val?.isTexture === true) {
286
- const task = this.assignTextureLODForSlot(val, level, material, slot);
311
+ const task = this.assignTextureLODForSlot(val, level, material, slot, force);
287
312
  promises.push(task);
288
313
  slots.push(slot);
289
314
  }
@@ -306,7 +331,7 @@ export class NEEDLE_progressive {
306
331
  }
307
332
  if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
308
333
  const texture = materialOrTexture;
309
- return this.assignTextureLODForSlot(texture, level, null, null);
334
+ return this.assignTextureLODForSlot(texture, level, null, null, force);
310
335
  }
311
336
  return Promise.resolve(null);
312
337
  }
@@ -321,14 +346,29 @@ export class NEEDLE_progressive {
321
346
  return NEEDLE_progressive.queue.maxConcurrent;
322
347
  }
323
348
  // #region INTERNAL
324
- static assignTextureLODForSlot(current, level, material, slot) {
349
+ static assignTextureLODForSlot(current, level, material, slot, force) {
325
350
  if (current?.isTexture !== true) {
326
351
  return Promise.resolve(null);
327
352
  }
328
353
  if (slot === "glyphMap") {
329
354
  return Promise.resolve(current);
330
355
  }
331
- return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
356
+ const currentLOD = this.getAssignedLODInformation(current);
357
+ if (currentLOD) {
358
+ if (currentLOD.level === level) {
359
+ return Promise.resolve(current);
360
+ }
361
+ if (!force && currentLOD.level < level) {
362
+ return Promise.resolve(current);
363
+ }
364
+ }
365
+ if (material && slot) {
366
+ const pending = this.getPendingTextureSlotRequest(material, slot);
367
+ if (pending && pending.level === level && pending.force === force) {
368
+ return pending.promise;
369
+ }
370
+ }
371
+ const promise = NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
332
372
  // this can currently not happen
333
373
  if (Array.isArray(tex)) {
334
374
  console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
@@ -337,40 +377,19 @@ export class NEEDLE_progressive {
337
377
  if (tex?.isTexture === true) {
338
378
  if (tex != current) {
339
379
  if (material && slot) {
340
- const assigned = material[slot];
380
+ const assigned = this.getMaterialTextureSlot(material, slot) ?? current;
341
381
  // Check if the assigned texture LOD is higher quality than the current LOD
342
382
  // This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
343
- if (assigned && !debug) {
383
+ if (assigned && !force) {
344
384
  const assignedLOD = this.getAssignedLODInformation(assigned);
345
385
  if (assignedLOD && assignedLOD?.level < level) {
346
386
  if (debug === "verbose")
347
387
  console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
348
- // Dispose the newly loaded texture since we're not using it
349
- // (the assigned texture is higher quality, so we reject the new one)
350
- // Note: We dispose directly here (not via untrackTextureUsage) because this texture
351
- // was never tracked/used - it was rejected immediately upon loading
352
- if (tex && tex !== assigned) {
353
- if (debug || debugGC) {
354
- console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
355
- }
356
- tex.dispose();
357
- }
358
388
  return null;
359
389
  }
360
390
  // assigned.dispose();
361
391
  }
362
- // Track reference count for new texture
363
- this.trackTextureUsage(tex);
364
- // Untrack the old texture (may dispose if ref count hits 0)
365
- // This prevents accumulation of GPU VRAM while waiting for garbage collection
366
- if (assigned && assigned !== tex) {
367
- const wasDisposed = this.untrackTextureUsage(assigned);
368
- if (wasDisposed && (debug || debugGC)) {
369
- const assignedLOD = this.getAssignedLODInformation(assigned);
370
- console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
371
- }
372
- }
373
- material[slot] = tex;
392
+ this.assignTrackedTextureSlot(material, slot, tex);
374
393
  }
375
394
  // Note: We use reference counting above to track texture usage across multiple materials.
376
395
  // When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
@@ -390,6 +409,129 @@ export class NEEDLE_progressive {
390
409
  console.error("Error loading LOD", current, err);
391
410
  return null;
392
411
  });
412
+ if (material && slot) {
413
+ this.setPendingTextureSlotRequest(material, slot, level, force, promise);
414
+ }
415
+ return promise;
416
+ }
417
+ // Track material slots, not just texture objects. A shared fallback texture can be
418
+ // referenced by many slots and should only be disposed after every slot moved away.
419
+ static trackedTextureSlots = new WeakMap();
420
+ static pendingTextureSlotRequests = new WeakMap();
421
+ static trackCurrentMaterialTextureSlots(material) {
422
+ if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
423
+ const shaderMaterial = material;
424
+ for (const slot of Object.keys(shaderMaterial.uniforms)) {
425
+ const value = shaderMaterial.uniforms[slot].value;
426
+ if (value?.isTexture === true) {
427
+ this.ensureTrackedTextureSlot(material, slot, value);
428
+ }
429
+ }
430
+ return;
431
+ }
432
+ for (const slot of Object.keys(material)) {
433
+ const value = material[slot];
434
+ if (value?.isTexture === true) {
435
+ this.ensureTrackedTextureSlot(material, slot, value);
436
+ }
437
+ }
438
+ }
439
+ static getPendingTextureSlotRequest(material, slot) {
440
+ return this.pendingTextureSlotRequests.get(material)?.get(slot);
441
+ }
442
+ static setPendingTextureSlotRequest(material, slot, level, force, promise) {
443
+ let slots = this.pendingTextureSlotRequests.get(material);
444
+ if (!slots) {
445
+ slots = new Map();
446
+ this.pendingTextureSlotRequests.set(material, slots);
447
+ }
448
+ const request = { level, force, promise };
449
+ slots.set(slot, request);
450
+ promise.finally(() => {
451
+ const current = slots.get(slot);
452
+ if (current === request) {
453
+ slots.delete(slot);
454
+ }
455
+ });
456
+ }
457
+ static getMaterialTextureSlot(material, slot) {
458
+ const uniforms = material.uniforms;
459
+ const uniform = uniforms?.[slot];
460
+ if (uniform?.value?.isTexture === true) {
461
+ return uniform.value;
462
+ }
463
+ const value = material[slot];
464
+ if (value?.isTexture === true) {
465
+ return value;
466
+ }
467
+ return null;
468
+ }
469
+ static setMaterialTextureSlot(material, slot, texture) {
470
+ const uniforms = material.uniforms;
471
+ const uniform = uniforms?.[slot];
472
+ if (uniform?.value?.isTexture === true) {
473
+ uniform.value = texture;
474
+ material.uniformsNeedUpdate = true;
475
+ return;
476
+ }
477
+ material[slot] = texture;
478
+ }
479
+ static assignTrackedTextureSlot(material, slot, texture) {
480
+ let slots = this.trackedTextureSlots.get(material);
481
+ if (!slots) {
482
+ slots = new Map();
483
+ this.trackedTextureSlots.set(material, slots);
484
+ }
485
+ const assigned = this.getMaterialTextureSlot(material, slot);
486
+ let previousTracked = slots.get(slot);
487
+ if (!previousTracked && assigned) {
488
+ previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
489
+ }
490
+ else if (previousTracked && assigned && previousTracked !== assigned) {
491
+ this.releaseTrackedTextureSlot(material, slot, previousTracked);
492
+ previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
493
+ }
494
+ if (previousTracked === texture && assigned === texture) {
495
+ return;
496
+ }
497
+ if (previousTracked && previousTracked !== texture) {
498
+ this.releaseTrackedTextureSlot(material, slot, previousTracked);
499
+ }
500
+ if (previousTracked !== texture) {
501
+ this.trackTextureUsage(texture);
502
+ slots.set(slot, texture);
503
+ }
504
+ if (assigned !== texture) {
505
+ this.setMaterialTextureSlot(material, slot, texture);
506
+ }
507
+ }
508
+ static ensureTrackedTextureSlot(material, slot, texture) {
509
+ let slots = this.trackedTextureSlots.get(material);
510
+ if (!slots) {
511
+ slots = new Map();
512
+ this.trackedTextureSlots.set(material, slots);
513
+ }
514
+ const previous = slots.get(slot);
515
+ if (previous === texture) {
516
+ return previous;
517
+ }
518
+ if (previous) {
519
+ this.releaseTrackedTextureSlot(material, slot, previous);
520
+ }
521
+ this.trackTextureUsage(texture);
522
+ slots.set(slot, texture);
523
+ return texture;
524
+ }
525
+ static releaseTrackedTextureSlot(material, slot, texture) {
526
+ const slots = this.trackedTextureSlots.get(material);
527
+ if (slots?.get(slot) === texture) {
528
+ slots.delete(slot);
529
+ }
530
+ const wasDisposed = this.untrackTextureUsage(texture);
531
+ if (wasDisposed && (debug || debugGC)) {
532
+ const assignedLOD = this.getAssignedLODInformation(texture);
533
+ console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} for ${material.name || material.type}.${slot}`, texture.uuid);
534
+ }
393
535
  }
394
536
  parser;
395
537
  url;
@@ -492,7 +634,15 @@ export class NEEDLE_progressive {
492
634
  return null;
493
635
  }
494
636
  /**
495
- * Register a texture with LOD information
637
+ * Register a texture with progressive LOD information. This associates the texture with its LOD extension data
638
+ * so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
639
+ * Typically called during glTF loading when the progressive extension is parsed.
640
+ *
641
+ * @param url - The source URL of the glTF file this texture was loaded from.
642
+ * @param tex - The three.js Texture instance to register.
643
+ * @param level - The LOD level this texture represents (0 = highest resolution).
644
+ * @param index - The texture index within the glTF file.
645
+ * @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
496
646
  */
497
647
  static registerTexture = (url, tex, level, index, ext) => {
498
648
  if (!tex) {
@@ -515,7 +665,17 @@ export class NEEDLE_progressive {
515
665
  NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
516
666
  };
517
667
  /**
518
- * Register a mesh with LOD information
668
+ * Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
669
+ * so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
670
+ * Typically called during glTF loading when the progressive extension is parsed.
671
+ * If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
672
+ *
673
+ * @param url - The source URL of the glTF file this mesh was loaded from.
674
+ * @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
675
+ * @param mesh - The three.js Mesh instance to register.
676
+ * @param level - The LOD level this mesh represents (0 = highest resolution / full density).
677
+ * @param index - The primitive index within the glTF mesh node.
678
+ * @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
519
679
  */
520
680
  static registerMesh = (url, key, mesh, level, index, ext) => {
521
681
  const geometry = mesh.geometry;
@@ -604,6 +764,8 @@ export class NEEDLE_progressive {
604
764
  this.cache.clear();
605
765
  // Clear all texture reference counts when disposing everything
606
766
  this.textureRefCounts.clear();
767
+ this.trackedTextureSlots = new WeakMap();
768
+ this.pendingTextureSlotRequests = new WeakMap();
607
769
  }
608
770
  }
609
771
  /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
@@ -710,8 +872,8 @@ export class NEEDLE_progressive {
710
872
  return false;
711
873
  }
712
874
  function logDebugInfo(prefix, newCount) {
713
- let width = texture.image?.width || texture.source?.data?.width || 0;
714
- let height = texture.image?.height || texture.source?.data?.height || 0;
875
+ const width = texture.image?.width || texture.source?.data?.width || 0;
876
+ const height = texture.image?.height || texture.source?.data?.height || 0;
715
877
  const textureSize = width && height ? `${width}x${height}` : "N/A";
716
878
  let memorySize = "N/A";
717
879
  if (width && height) {
@@ -791,76 +953,16 @@ export class NEEDLE_progressive {
791
953
  }
792
954
  // check if the requested file has already been loaded
793
955
  const KEY = lod_url + "_" + lodInfo.guid;
794
- const slot = await this.queue.slot(lod_url);
795
956
  // check if the requested file is currently being loaded or was previously loaded
796
- const existing = this.cache.get(KEY);
797
- if (existing !== undefined) {
798
- if (debugverbose)
799
- console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
800
- if (existing instanceof WeakRef) {
801
- // Previously resolved resource check if still alive in memory
802
- const derefed = existing.deref();
803
- if (derefed) {
804
- let res = derefed;
805
- let resourceIsDisposed = false;
806
- if (res instanceof Texture && current instanceof Texture) {
807
- if (res.image?.data || res.source?.data) {
808
- res = this.copySettings(current, res);
809
- }
810
- else {
811
- resourceIsDisposed = true;
812
- }
813
- }
814
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
815
- if (!res.attributes.position?.array) {
816
- resourceIsDisposed = true;
817
- }
818
- }
819
- if (!resourceIsDisposed) {
820
- return res;
821
- }
822
- }
823
- // Resource was garbage collected or disposed — remove stale entry and re-load
824
- this.cache.delete(KEY);
825
- if (debug)
826
- console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
827
- }
828
- else {
829
- // Promise — loading in progress or previously completed
830
- let res = await existing.catch(err => {
831
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
832
- return null;
833
- });
834
- let resouceIsDisposed = false;
835
- if (res == null) {
836
- // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
837
- // in which case we don't attempt to load it again
838
- }
839
- else if (res instanceof Texture && current instanceof Texture) {
840
- // check if the texture has been disposed or not
841
- if (res.image?.data || res.source?.data) {
842
- res = this.copySettings(current, res);
843
- }
844
- // if it has been disposed we need to load it again
845
- else {
846
- resouceIsDisposed = true;
847
- this.cache.delete(KEY);
848
- }
849
- }
850
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
851
- if (res.attributes.position?.array) {
852
- // the geometry is OK
853
- }
854
- else {
855
- resouceIsDisposed = true;
856
- this.cache.delete(KEY);
857
- }
858
- }
859
- if (!resouceIsDisposed) {
860
- return res;
861
- }
862
- }
863
- }
957
+ const cached = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
958
+ if (cached.found)
959
+ return cached.value;
960
+ const slot = await this.queue.slot(lod_url);
961
+ // Another request can fill the cache while this one waits for a queue slot.
962
+ // Re-checking here avoids duplicate loads for heavily instanced assets.
963
+ const cachedAfterQueue = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
964
+ if (cachedAfterQueue.found)
965
+ return cachedAfterQueue.value;
864
966
  // #region loading
865
967
  if (!slot.use) {
866
968
  if (debug)
@@ -1056,6 +1158,67 @@ export class NEEDLE_progressive {
1056
1158
  }
1057
1159
  return null;
1058
1160
  }
1161
+ static async tryResolveLODCacheEntry(existing, key, lodUrl, current, level, debugverbose) {
1162
+ if (existing === undefined) {
1163
+ return { found: false };
1164
+ }
1165
+ if (debugverbose)
1166
+ console.log(`LOD ${level} was already loading/loaded: ${key}`);
1167
+ if (existing instanceof WeakRef) {
1168
+ const derefed = existing.deref();
1169
+ if (derefed) {
1170
+ let res = derefed;
1171
+ let resourceIsDisposed = false;
1172
+ if (res instanceof Texture && current instanceof Texture) {
1173
+ if (res.image?.data || res.source?.data) {
1174
+ res = this.copySettings(current, res);
1175
+ }
1176
+ else {
1177
+ resourceIsDisposed = true;
1178
+ }
1179
+ }
1180
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
1181
+ if (!res.attributes.position?.array) {
1182
+ resourceIsDisposed = true;
1183
+ }
1184
+ }
1185
+ if (!resourceIsDisposed) {
1186
+ return { found: true, value: res };
1187
+ }
1188
+ }
1189
+ this.cache.delete(key);
1190
+ if (debug)
1191
+ console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${key}`);
1192
+ return { found: false };
1193
+ }
1194
+ let res = await existing.catch(err => {
1195
+ console.error(`Error loading LOD ${level} from ${lodUrl}\n`, err);
1196
+ return null;
1197
+ });
1198
+ let resourceIsDisposed = false;
1199
+ if (res == null) {
1200
+ // Failed loads stay cached as null so we don't retry the same missing resource forever.
1201
+ }
1202
+ else if (res instanceof Texture && current instanceof Texture) {
1203
+ if (res.image?.data || res.source?.data) {
1204
+ res = this.copySettings(current, res);
1205
+ }
1206
+ else {
1207
+ resourceIsDisposed = true;
1208
+ this.cache.delete(key);
1209
+ }
1210
+ }
1211
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
1212
+ if (!res.attributes.position?.array) {
1213
+ resourceIsDisposed = true;
1214
+ this.cache.delete(key);
1215
+ }
1216
+ }
1217
+ if (resourceIsDisposed) {
1218
+ return { found: false };
1219
+ }
1220
+ return { found: true, value: res };
1221
+ }
1059
1222
  static _queue;
1060
1223
  static get queue() { return this._queue ??= new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false }); }
1061
1224
  static assignLODInformation(url, res, key, level, index) {
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { version as VERSION } from "./version.js";
2
2
  export * from "./extension.js";
3
3
  export * from "./plugins/index.js";
4
- export { LODsManager, type LOD_Results } from "./lods.manager.js";
4
+ export { LODsManager, getLODColor, lodDebugColors, type LOD_Results } from "./lods.manager.js";
5
5
  export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
6
6
  export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
7
7
  import { WebGLRenderer } from "three";
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { version as VERSION } from "./version.js";
2
2
  export * from "./extension.js";
3
3
  export * from "./plugins/index.js";
4
- export { LODsManager } from "./lods.manager.js";
4
+ export { LODsManager, getLODColor, lodDebugColors } from "./lods.manager.js";
5
5
  export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
6
6
  export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
7
7
  import { addDracoAndKTX2Loaders, configureLoader, createLoaders } from "./loaders.js";
package/lib/lods.debug.js CHANGED
@@ -3,7 +3,7 @@ export const debug = getParam("debugprogressive");
3
3
  let debug_RenderWireframe = undefined;
4
4
  export let debug_OverrideLodLevel = -1; // -1 is automatic
5
5
  if (debug) {
6
- let maxLevel = 6;
6
+ const maxLevel = 6;
7
7
  function debugToggleProgressive() {
8
8
  debug_OverrideLodLevel += 1;
9
9
  if (debug_OverrideLodLevel >= maxLevel) {