@needle-tools/gltf-progressive 3.3.5 → 3.4.0-next.a30adb3

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,106 @@ 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.previouslyLoaded) {
556
+ if (key.includes(guid)) {
557
+ this._disposeCacheEntry(entry);
558
+ this.previouslyLoaded.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.previouslyLoaded) {
578
+ this._disposeCacheEntry(entry);
579
+ }
580
+ this.previouslyLoaded.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 */
608
+ /** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
530
609
  static previouslyLoaded = new Map();
531
- /** this contains the geometry/textures that were originally loaded */
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.previouslyLoaded.get(cacheKey);
619
+ console.debug(`[gltf-progressive] FinalizationRegistry cleanup: Resource GC'd for ${cacheKey}.`);
620
+ // Only delete if the entry is still a WeakRef and the resource is gone
621
+ if (entry instanceof WeakRef) {
622
+ const derefed = entry.deref();
623
+ if (!derefed) {
624
+ NEEDLE_progressive.previouslyLoaded.delete(cacheKey);
625
+ if (debug)
626
+ console.log(`[gltf-progressive] Cache entry auto-cleaned (GC'd): ${cacheKey}`);
627
+ }
628
+ }
629
+ });
533
630
  static workers = [];
534
631
  static _workersIndex = 0;
535
632
  static async getOrLoadLOD(current, level) {
@@ -567,8 +664,18 @@ export class NEEDLE_progressive {
567
664
  useLowRes = true;
568
665
  }
569
666
  if (useLowRes) {
570
- const lowres = this.lowresCache.get(LODKEY);
571
- return lowres;
667
+ const lowresRef = this.lowresCache.get(LODKEY);
668
+ if (lowresRef) {
669
+ const lowres = lowresRef.deref();
670
+ if (lowres)
671
+ return lowres;
672
+ // Resource was GC'd, remove stale entry
673
+ this.lowresCache.delete(LODKEY);
674
+ if (debug)
675
+ console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${LODKEY}`);
676
+ }
677
+ // Fallback to current if lowres was GC'd
678
+ return null;
572
679
  }
573
680
  }
574
681
  /** the unresolved LOD url */
@@ -592,43 +699,74 @@ export class NEEDLE_progressive {
592
699
  // check if the requested file has already been loaded
593
700
  const KEY = lod_url + "_" + lodInfo.guid;
594
701
  const slot = await this.queue.slot(lod_url);
595
- // check if the requested file is currently being loaded
702
+ // check if the requested file is currently being loaded or was previously loaded
596
703
  const existing = this.previouslyLoaded.get(KEY);
597
704
  if (existing !== undefined) {
598
705
  if (debugverbose)
599
706
  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
707
+ if (existing instanceof WeakRef) {
708
+ // Previously resolved resource check if still alive in memory
709
+ const derefed = existing.deref();
710
+ if (derefed) {
711
+ let res = derefed;
712
+ let resourceIsDisposed = false;
713
+ if (res instanceof Texture && current instanceof Texture) {
714
+ if (res.image?.data || res.source?.data) {
715
+ res = this.copySettings(current, res);
716
+ }
717
+ else {
718
+ resourceIsDisposed = true;
719
+ }
720
+ }
721
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
722
+ if (!res.attributes.position?.array) {
723
+ resourceIsDisposed = true;
724
+ }
725
+ }
726
+ if (!resourceIsDisposed) {
727
+ return res;
728
+ }
729
+ }
730
+ // Resource was garbage collected or disposed — remove stale entry and re-load
731
+ this.previouslyLoaded.delete(KEY);
732
+ if (debug)
733
+ console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
608
734
  }
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);
735
+ else {
736
+ // Promise loading in progress or previously completed
737
+ let res = await existing.catch(err => {
738
+ console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
739
+ return null;
740
+ });
741
+ let resouceIsDisposed = false;
742
+ if (res == null) {
743
+ // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
744
+ // in which case we don't attempt to load it again
613
745
  }
614
- // if it has been disposed we need to load it again
615
- else {
616
- resouceIsDisposed = true;
617
- this.previouslyLoaded.delete(KEY);
746
+ else if (res instanceof Texture && current instanceof Texture) {
747
+ // check if the texture has been disposed or not
748
+ if (res.image?.data || res.source?.data) {
749
+ res = this.copySettings(current, res);
750
+ }
751
+ // if it has been disposed we need to load it again
752
+ else {
753
+ resouceIsDisposed = true;
754
+ this.previouslyLoaded.delete(KEY);
755
+ }
618
756
  }
619
- }
620
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
621
- if (res.attributes.position?.array) {
622
- // the geometry is OK
757
+ else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
758
+ if (res.attributes.position?.array) {
759
+ // the geometry is OK
760
+ }
761
+ else {
762
+ resouceIsDisposed = true;
763
+ this.previouslyLoaded.delete(KEY);
764
+ }
623
765
  }
624
- else {
625
- resouceIsDisposed = true;
626
- this.previouslyLoaded.delete(KEY);
766
+ if (!resouceIsDisposed) {
767
+ return res;
627
768
  }
628
769
  }
629
- if (!resouceIsDisposed) {
630
- return res;
631
- }
632
770
  }
633
771
  // #region loading
634
772
  if (!slot.use) {
@@ -777,6 +915,28 @@ export class NEEDLE_progressive {
777
915
  this.previouslyLoaded.set(KEY, request);
778
916
  slot.use(request);
779
917
  const res = await request;
918
+ // Optimize cache entry: replace loading promise with lightweight reference.
919
+ // This releases closure variables captured during the loading function.
920
+ if (res != null) {
921
+ if (Array.isArray(res)) {
922
+ // For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
923
+ // WeakRef can't be used here because callers only extract individual elements
924
+ // from the array, so the array object itself would be GC'd immediately.
925
+ this.previouslyLoaded.set(KEY, Promise.resolve(res));
926
+ }
927
+ else {
928
+ // For single resources (Texture or BufferGeometry), use WeakRef to allow
929
+ // garbage collection when the resource is no longer referenced by the scene.
930
+ // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
931
+ this.previouslyLoaded.set(KEY, new WeakRef(res));
932
+ NEEDLE_progressive._resourceRegistry.register(res, KEY);
933
+ }
934
+ }
935
+ else {
936
+ // Failed load — replace with clean resolved promise to release loading closure.
937
+ // Keeping the entry prevents retrying (existing behavior).
938
+ this.previouslyLoaded.set(KEY, Promise.resolve(null));
939
+ }
780
940
  return res;
781
941
  }
782
942
  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-next.a30adb3",
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": {
@@ -73,4 +73,4 @@
73
73
  "vite": "7"
74
74
  },
75
75
  "types": "./lib/index.d.ts"
76
- }
76
+ }