@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/NEEDLE_progressive/README.md +45 -47
- package/gltf-progressive.js +535 -458
- package/gltf-progressive.min.js +8 -7
- package/gltf-progressive.umd.cjs +8 -7
- package/lib/extension.d.ts +18 -3
- package/lib/extension.js +203 -40
- package/lib/version.js +1 -1
- package/package.json +1 -1
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,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
|
|
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
|
|
571
|
-
|
|
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.
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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.
|
|
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
package/package.json
CHANGED