@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/NEEDLE_progressive/README.md +45 -47
- package/gltf-progressive.js +596 -520
- package/gltf-progressive.min.js +7 -7
- package/gltf-progressive.umd.cjs +7 -7
- package/lib/extension.d.ts +17 -2
- package/lib/extension.js +196 -36
- package/lib/version.js +1 -1
- package/package.json +2 -2
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
|
-
|
|
515
|
-
|
|
514
|
+
const existingRef = NEEDLE_progressive.lowresCache.get(key);
|
|
515
|
+
let existing = existingRef?.deref();
|
|
516
|
+
if (existing) {
|
|
516
517
|
existing.push(mesh.geometry);
|
|
517
|
-
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
518
520
|
existing = [mesh.geometry];
|
|
519
|
-
|
|
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
|
|
571
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-progressive",
|
|
3
|
-
"version": "3.
|
|
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
|
+
}
|