@needle-tools/gltf-progressive 3.3.5 → 3.4.0-beta

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/lib/extension.js CHANGED
@@ -493,7 +493,7 @@ export class NEEDLE_progressive {
493
493
  const key = ext.guid;
494
494
  NEEDLE_progressive.assignLODInformation(url, tex, key, level, index);
495
495
  NEEDLE_progressive.lodInfos.set(key, ext);
496
- NEEDLE_progressive.lowresCache.set(key, tex);
496
+ NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
497
497
  };
498
498
  /**
499
499
  * Register a mesh with LOD information
@@ -511,12 +511,15 @@ export class NEEDLE_progressive {
511
511
  console.log("> Progressive: register mesh " + mesh.name, { index, uuid: mesh.uuid }, ext, mesh);
512
512
  NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index);
513
513
  NEEDLE_progressive.lodInfos.set(key, ext);
514
- let existing = NEEDLE_progressive.lowresCache.get(key);
515
- if (existing)
514
+ const existingRef = NEEDLE_progressive.lowresCache.get(key);
515
+ let existing = existingRef?.deref();
516
+ if (existing) {
516
517
  existing.push(mesh.geometry);
517
- else
518
+ }
519
+ else {
518
520
  existing = [mesh.geometry];
519
- NEEDLE_progressive.lowresCache.set(key, existing);
521
+ }
522
+ NEEDLE_progressive.lowresCache.set(key, new WeakRef(existing));
520
523
  if (level > 0 && !getRaycastMesh(mesh)) {
521
524
  registerRaycastMesh(mesh, geometry);
522
525
  }
@@ -524,12 +527,107 @@ export class NEEDLE_progressive {
524
527
  plugin.onRegisteredNewMesh?.(mesh, ext);
525
528
  }
526
529
  };
530
+ /**
531
+ * Dispose cached resources to free memory.
532
+ * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
533
+ * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
534
+ * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
535
+ */
536
+ static dispose(guid) {
537
+ if (guid) {
538
+ this.lodInfos.delete(guid);
539
+ // Dispose lowres cache entries (original proxy resources)
540
+ const lowresRef = this.lowresCache.get(guid);
541
+ if (lowresRef) {
542
+ const lowres = lowresRef.deref();
543
+ if (lowres) {
544
+ if (lowres.isTexture) {
545
+ lowres.dispose();
546
+ }
547
+ else if (Array.isArray(lowres)) {
548
+ for (const geo of lowres)
549
+ geo.dispose();
550
+ }
551
+ }
552
+ this.lowresCache.delete(guid);
553
+ }
554
+ // Dispose previously loaded LOD entries
555
+ for (const [key, entry] of this.cache) {
556
+ if (key.includes(guid)) {
557
+ this._disposeCacheEntry(entry);
558
+ this.cache.delete(key);
559
+ }
560
+ }
561
+ }
562
+ else {
563
+ this.lodInfos.clear();
564
+ for (const [, entryRef] of this.lowresCache) {
565
+ const entry = entryRef.deref();
566
+ if (entry) {
567
+ if (entry.isTexture) {
568
+ entry.dispose();
569
+ }
570
+ else if (Array.isArray(entry)) {
571
+ for (const geo of entry)
572
+ geo.dispose();
573
+ }
574
+ }
575
+ }
576
+ this.lowresCache.clear();
577
+ for (const [, entry] of this.cache) {
578
+ this._disposeCacheEntry(entry);
579
+ }
580
+ this.cache.clear();
581
+ }
582
+ }
583
+ /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
584
+ static _disposeCacheEntry(entry) {
585
+ if (entry instanceof WeakRef) {
586
+ // Single resource — deref and dispose if still alive
587
+ const resource = entry.deref();
588
+ resource?.dispose();
589
+ }
590
+ else {
591
+ // Promise — may be in-flight or already resolved.
592
+ // Attach disposal to run after resolution.
593
+ entry.then(resource => {
594
+ if (resource) {
595
+ if (Array.isArray(resource)) {
596
+ for (const geo of resource)
597
+ geo.dispose();
598
+ }
599
+ else {
600
+ resource.dispose();
601
+ }
602
+ }
603
+ }).catch(() => { });
604
+ }
605
+ }
527
606
  /** A map of key = asset uuid and value = LOD information */
528
607
  static lodInfos = new Map();
529
- /** cache of already loaded mesh lods */
530
- static previouslyLoaded = new Map();
531
- /** this contains the geometry/textures that were originally loaded */
608
+ /** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
609
+ static cache = new Map();
610
+ /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
532
611
  static lowresCache = new Map();
612
+ /**
613
+ * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
614
+ * when their associated three.js resources are garbage collected by the browser.
615
+ * The held value is the cache key string used in `previouslyLoaded`.
616
+ */
617
+ static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
618
+ const entry = NEEDLE_progressive.cache.get(cacheKey);
619
+ if (debug)
620
+ console.debug(`[gltf-progressive] Resource GC'd\n${cacheKey}`);
621
+ // Only delete if the entry is still a WeakRef and the resource is gone
622
+ if (entry instanceof WeakRef) {
623
+ const derefed = entry.deref();
624
+ if (!derefed) {
625
+ NEEDLE_progressive.cache.delete(cacheKey);
626
+ if (debug)
627
+ console.log(`[gltf-progressive] Cache entry auto-cleaned (GC'd): ${cacheKey}`);
628
+ }
629
+ }
630
+ });
533
631
  static workers = [];
534
632
  static _workersIndex = 0;
535
633
  static async getOrLoadLOD(current, level) {
@@ -567,8 +665,18 @@ export class NEEDLE_progressive {
567
665
  useLowRes = true;
568
666
  }
569
667
  if (useLowRes) {
570
- const lowres = this.lowresCache.get(LODKEY);
571
- return lowres;
668
+ const lowresRef = this.lowresCache.get(LODKEY);
669
+ if (lowresRef) {
670
+ const lowres = lowresRef.deref();
671
+ if (lowres)
672
+ return lowres;
673
+ // Resource was GC'd, remove stale entry
674
+ this.lowresCache.delete(LODKEY);
675
+ if (debug)
676
+ console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${LODKEY}`);
677
+ }
678
+ // Fallback to current if lowres was GC'd
679
+ return null;
572
680
  }
573
681
  }
574
682
  /** the unresolved LOD url */
@@ -592,42 +700,73 @@ export class NEEDLE_progressive {
592
700
  // check if the requested file has already been loaded
593
701
  const KEY = lod_url + "_" + lodInfo.guid;
594
702
  const slot = await this.queue.slot(lod_url);
595
- // check if the requested file is currently being loaded
596
- const existing = this.previouslyLoaded.get(KEY);
703
+ // check if the requested file is currently being loaded or was previously loaded
704
+ const existing = this.cache.get(KEY);
597
705
  if (existing !== undefined) {
598
706
  if (debugverbose)
599
707
  console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
600
- let res = await existing.catch(err => {
601
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
602
- return null;
603
- });
604
- let resouceIsDisposed = false;
605
- if (res == null) {
606
- // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
607
- // in which case we don't attempt to load it again
608
- }
609
- else if (res instanceof Texture && current instanceof Texture) {
610
- // check if the texture has been disposed or not
611
- if (res.image?.data || res.source?.data) {
612
- res = this.copySettings(current, res);
613
- }
614
- // if it has been disposed we need to load it again
615
- else {
616
- resouceIsDisposed = true;
617
- this.previouslyLoaded.delete(KEY);
708
+ if (existing instanceof WeakRef) {
709
+ // Previously resolved resource check if still alive in memory
710
+ const derefed = existing.deref();
711
+ if (derefed) {
712
+ let res = derefed;
713
+ let resourceIsDisposed = false;
714
+ if (res instanceof Texture && current instanceof Texture) {
715
+ if (res.image?.data || res.source?.data) {
716
+ res = this.copySettings(current, res);
717
+ }
718
+ else {
719
+ resourceIsDisposed = true;
720
+ }
721
+ }
722
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
723
+ if (!res.attributes.position?.array) {
724
+ resourceIsDisposed = true;
725
+ }
726
+ }
727
+ if (!resourceIsDisposed) {
728
+ return res;
729
+ }
618
730
  }
731
+ // Resource was garbage collected or disposed — remove stale entry and re-load
732
+ this.cache.delete(KEY);
733
+ if (debug)
734
+ console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
619
735
  }
620
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
621
- if (res.attributes.position?.array) {
622
- // the geometry is OK
736
+ else {
737
+ // Promise — loading in progress or previously completed
738
+ let res = await existing.catch(err => {
739
+ console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
740
+ return null;
741
+ });
742
+ let resouceIsDisposed = false;
743
+ if (res == null) {
744
+ // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
745
+ // in which case we don't attempt to load it again
623
746
  }
624
- else {
625
- resouceIsDisposed = true;
626
- this.previouslyLoaded.delete(KEY);
747
+ else if (res instanceof Texture && current instanceof Texture) {
748
+ // check if the texture has been disposed or not
749
+ if (res.image?.data || res.source?.data) {
750
+ res = this.copySettings(current, res);
751
+ }
752
+ // if it has been disposed we need to load it again
753
+ else {
754
+ resouceIsDisposed = true;
755
+ this.cache.delete(KEY);
756
+ }
757
+ }
758
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
759
+ if (res.attributes.position?.array) {
760
+ // the geometry is OK
761
+ }
762
+ else {
763
+ resouceIsDisposed = true;
764
+ this.cache.delete(KEY);
765
+ }
766
+ }
767
+ if (!resouceIsDisposed) {
768
+ return res;
627
769
  }
628
- }
629
- if (!resouceIsDisposed) {
630
- return res;
631
770
  }
632
771
  }
633
772
  // #region loading
@@ -774,9 +913,33 @@ export class NEEDLE_progressive {
774
913
  // we could not find a texture or mesh with the given guid
775
914
  return resolve(null);
776
915
  });
777
- this.previouslyLoaded.set(KEY, request);
916
+ this.cache.set(KEY, request);
778
917
  slot.use(request);
779
918
  const res = await request;
919
+ // Optimize cache entry: replace loading promise with lightweight reference.
920
+ // This releases closure variables captured during the loading function.
921
+ if (res != null) {
922
+ if (res instanceof Texture) {
923
+ // For Texture resources, use WeakRef to allow garbage collection.
924
+ // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
925
+ this.cache.set(KEY, new WeakRef(res));
926
+ NEEDLE_progressive._resourceRegistry.register(res, KEY);
927
+ }
928
+ else if (Array.isArray(res)) {
929
+ // For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
930
+ // This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
931
+ this.cache.set(KEY, Promise.resolve(res));
932
+ }
933
+ else {
934
+ // For single BufferGeometry, keep in memory (don't use WeakRef)
935
+ this.cache.set(KEY, Promise.resolve(res));
936
+ }
937
+ }
938
+ else {
939
+ // Failed load — replace with clean resolved promise to release loading closure.
940
+ // Keeping the entry prevents retrying (existing behavior).
941
+ this.cache.set(KEY, Promise.resolve(null));
942
+ }
780
943
  return res;
781
944
  }
782
945
  else {
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.3.5";
2
+ export const version = "3.4.0-beta";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.3.5",
3
+ "version": "3.4.0-beta",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {