@needle-tools/gltf-progressive 3.0.0 → 3.1.0-next.f550970

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
@@ -1,7 +1,7 @@
1
1
  import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
2
2
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { addDracoAndKTX2Loaders } from "./loaders.js";
4
- import { resolveUrl } from "./utils.internal.js";
4
+ import { PromiseQueue, resolveUrl } from "./utils.internal.js";
5
5
  import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
6
6
  // All of this has to be removed
7
7
  // import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
@@ -309,8 +309,10 @@ export class NEEDLE_progressive {
309
309
  }
310
310
  return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
311
311
  // this can currently not happen
312
- if (Array.isArray(tex))
312
+ if (Array.isArray(tex)) {
313
+ console.warn("Progressive: Got an array of textures for a texture slot, this should not happen", tex);
313
314
  return null;
315
+ }
314
316
  if (tex?.isTexture === true) {
315
317
  if (tex != current) {
316
318
  if (material && slot) {
@@ -375,6 +377,20 @@ export class NEEDLE_progressive {
375
377
  return mesh;
376
378
  });
377
379
  };
380
+ // private _isLoadingTexture;
381
+ // loadTexture = (textureIndex: number) => {
382
+ // if (this._isLoadingTexture) return null;
383
+ // const ext = this.parser.json.textures[textureIndex]?.extensions?.[EXTENSION_NAME] as NEEDLE_ext_progressive_texture;
384
+ // if (!ext) return null;
385
+ // this._isLoadingTexture = true;
386
+ // return this.parser.getDependency("texture", textureIndex).then(tex => {
387
+ // this._isLoadingTexture = false;
388
+ // if (tex) {
389
+ // NEEDLE_progressive.registerTexture(this.url, tex as Texture, ext.lods?.length, textureIndex, ext);
390
+ // }
391
+ // return tex;
392
+ // });
393
+ // }
378
394
  afterRoot(gltf) {
379
395
  if (debug)
380
396
  console.log("AFTER", this.url, gltf);
@@ -440,13 +456,16 @@ export class NEEDLE_progressive {
440
456
  * Register a texture with LOD information
441
457
  */
442
458
  static registerTexture = (url, tex, level, index, ext) => {
443
- if (debug)
444
- console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
445
459
  if (!tex) {
446
460
  if (debug)
447
- console.error("gltf-progressive: Register texture without texture");
461
+ console.error("gltf-progressive: Called register texture without texture");
448
462
  return;
449
463
  }
464
+ if (debug) {
465
+ const width = tex.image?.width || tex.source?.data?.width || 0;
466
+ const height = tex.image?.height || tex.source?.data?.height || 0;
467
+ console.log(`> Progressive: register texture[${index}] "${tex.name || tex.uuid}", Current: ${width}x${height}, Max: ${ext.lods[0]?.width}x${ext.lods[0]?.height}, uuid: ${tex.uuid}`, ext, tex);
468
+ }
450
469
  // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
451
470
  // see https://github.com/needle-tools/needle-engine-support/issues/133
452
471
  if (tex.source)
@@ -494,25 +513,27 @@ export class NEEDLE_progressive {
494
513
  static async getOrLoadLOD(current, level) {
495
514
  const debugverbose = debug == "verbose";
496
515
  /** this key is used to lookup the LOD information */
497
- const LOD = current.userData.LODS;
516
+ const LOD = this.getAssignedLODInformation(current);
498
517
  if (!LOD) {
518
+ if (debug)
519
+ console.warn(`[gltf-progressive] No LOD information found: ${current.name}, uuid: ${current.uuid}, type: ${current.type}`, current);
499
520
  return null;
500
521
  }
501
522
  const LODKEY = LOD?.key;
502
- let progressiveInfo;
523
+ let lodInfo;
503
524
  // See https://github.com/needle-tools/needle-engine-support/issues/133
504
525
  if (current.isTexture === true) {
505
526
  const tex = current;
506
527
  if (tex.source && tex.source[$progressiveTextureExtension])
507
- progressiveInfo = tex.source[$progressiveTextureExtension];
528
+ lodInfo = tex.source[$progressiveTextureExtension];
508
529
  }
509
- if (!progressiveInfo)
510
- progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
511
- if (progressiveInfo) {
530
+ if (!lodInfo)
531
+ lodInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
532
+ if (lodInfo) {
512
533
  if (level > 0) {
513
534
  let useLowRes = false;
514
- const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
515
- if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
535
+ const hasMultipleLevels = Array.isArray(lodInfo.lods);
536
+ if (hasMultipleLevels && level >= lodInfo.lods.length) {
516
537
  useLowRes = true;
517
538
  }
518
539
  else if (!hasMultipleLevels) {
@@ -524,12 +545,12 @@ export class NEEDLE_progressive {
524
545
  }
525
546
  }
526
547
  /** the unresolved LOD url */
527
- const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level]?.path : progressiveInfo.lods;
548
+ const unresolved_lod_url = Array.isArray(lodInfo.lods) ? lodInfo.lods[level]?.path : lodInfo.lods;
528
549
  // check if we have a uri
529
550
  if (!unresolved_lod_url) {
530
- if (debug && !progressiveInfo["missing:uri"]) {
531
- progressiveInfo["missing:uri"] = true;
532
- console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
551
+ if (debug && !lodInfo["missing:uri"]) {
552
+ lodInfo["missing:uri"] = true;
553
+ console.warn("Missing uri for progressive asset for LOD " + level, lodInfo);
533
554
  }
534
555
  return null;
535
556
  }
@@ -537,12 +558,12 @@ export class NEEDLE_progressive {
537
558
  const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
538
559
  // check if the requested file needs to be loaded via a GLTFLoader
539
560
  if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
540
- if (!progressiveInfo.guid) {
541
- console.warn("missing pointer for glb/gltf texture", progressiveInfo);
561
+ if (!lodInfo.guid) {
562
+ console.warn("missing pointer for glb/gltf texture", lodInfo);
542
563
  return null;
543
564
  }
544
565
  // check if the requested file has already been loaded
545
- const KEY = lod_url + "_" + progressiveInfo.guid;
566
+ const KEY = lod_url + "_" + lodInfo.guid;
546
567
  // check if the requested file is currently being loaded
547
568
  const existing = this.previouslyLoaded.get(KEY);
548
569
  if (existing !== undefined) {
@@ -581,7 +602,13 @@ export class NEEDLE_progressive {
581
602
  return res;
582
603
  }
583
604
  }
584
- const ext = progressiveInfo;
605
+ const slot = await this.queue.slot(lod_url);
606
+ if (!slot.use) {
607
+ if (debug)
608
+ console.log(`LOD ${level} was aborted: ${lod_url}`);
609
+ return null; // the request was aborted, we don't load it again
610
+ }
611
+ const ext = lodInfo;
585
612
  const request = new Promise(async (resolve, _) => {
586
613
  const loader = new GLTFLoader();
587
614
  addDracoAndKTX2Loaders(loader);
@@ -599,10 +626,11 @@ export class NEEDLE_progressive {
599
626
  }
600
627
  const gltf = await loader.loadAsync(url).catch(err => {
601
628
  console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
602
- return null;
629
+ return resolve(null);
603
630
  });
604
- if (!gltf)
605
- return null;
631
+ if (!gltf) {
632
+ return resolve(null);
633
+ }
606
634
  const parser = gltf.parser;
607
635
  if (debugverbose)
608
636
  console.log("Loading finished " + lod_url, ext.guid);
@@ -687,6 +715,7 @@ export class NEEDLE_progressive {
687
715
  return resolve(null);
688
716
  });
689
717
  this.previouslyLoaded.set(KEY, request);
718
+ slot.use(request);
690
719
  const res = await request;
691
720
  return res;
692
721
  }
@@ -697,12 +726,12 @@ export class NEEDLE_progressive {
697
726
  const loader = new TextureLoader();
698
727
  const tex = await loader.loadAsync(lod_url);
699
728
  if (tex) {
700
- tex.guid = progressiveInfo.guid;
729
+ tex.guid = lodInfo.guid;
701
730
  tex.flipY = false;
702
731
  tex.needsUpdate = true;
703
732
  tex.colorSpace = current.colorSpace;
704
733
  if (debugverbose)
705
- console.log(progressiveInfo, tex);
734
+ console.log(lodInfo, tex);
706
735
  }
707
736
  else if (debug)
708
737
  console.warn("failed loading", lod_url);
@@ -716,6 +745,7 @@ export class NEEDLE_progressive {
716
745
  }
717
746
  return null;
718
747
  }
748
+ static queue = new PromiseQueue(100, { debug: debug != false });
719
749
  static assignLODInformation(url, res, key, level, index) {
720
750
  if (!res)
721
751
  return;
@@ -723,9 +753,17 @@ export class NEEDLE_progressive {
723
753
  res.userData = {};
724
754
  const info = new LODInformation(url, key, level, index);
725
755
  res.userData.LODS = info;
756
+ if ("source" in res && typeof res.source === "object")
757
+ res.source.LODS = info; // for tiled textures
726
758
  }
727
759
  static getAssignedLODInformation(res) {
728
- return res?.userData?.LODS || null;
760
+ if (!res)
761
+ return null;
762
+ if (res.userData?.LODS)
763
+ return res.userData.LODS;
764
+ if ("source" in res && res.source?.LODS)
765
+ return res.source.LODS;
766
+ return null;
729
767
  }
730
768
  // private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
731
769
  static copySettings(source, target) {
package/lib/index.d.ts CHANGED
@@ -1,13 +1,13 @@
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, 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";
8
8
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
9
9
  import { SmartLoadingHints } from "./loaders.js";
10
- import { LODsManager } from "./lods_manager.js";
10
+ import { LODsManager } from "./lods.manager.js";
11
11
  declare type UseNeedleGLTFProgressiveOptions = {
12
12
  /**
13
13
  * When set to true the LODs manager will automatically be enabled
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
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 } 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";
8
8
  import { NEEDLE_progressive } from "./extension.js";
9
- import { LODsManager } from "./lods_manager.js";
9
+ import { LODsManager } from "./lods.manager.js";
10
10
  /** Use this function to enable progressive loading of gltf models.
11
11
  * @param url The url of the gltf model.
12
12
  * @param renderer The renderer of the scene.
@@ -1,5 +1,6 @@
1
- import { Camera, Material, Matrix4, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
1
+ import { Camera, Material, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
2
2
  import { NEEDLE_progressive_plugin } from "./plugins/plugin.js";
3
+ import { PromiseGroupOptions } from "./lods.promise.js";
3
4
  export type LODManagerContext = {
4
5
  engine: "three" | "needle-engine" | "model-viewer" | "react-three-fiber" | "unknown";
5
6
  };
@@ -61,9 +62,9 @@ export declare class LODsManager {
61
62
  * @returns The LODsManager instance.
62
63
  */
63
64
  static get(renderer: WebGLRenderer, context?: LODManagerContext): LODsManager;
64
- private readonly context;
65
65
  readonly renderer: WebGLRenderer;
66
- readonly projectionScreenMatrix: Matrix4;
66
+ private readonly context;
67
+ private readonly projectionScreenMatrix;
67
68
  /** @deprecated use static `LODsManager.addPlugin()` method. This getter will be removed in later versions */
68
69
  get plugins(): NEEDLE_progressive_plugin[];
69
70
  /**
@@ -93,6 +94,17 @@ export declare class LODsManager {
93
94
  * @default false
94
95
  */
95
96
  manual: boolean;
97
+ private readonly _newPromiseGroups;
98
+ private _promiseGroupIds;
99
+ /**
100
+ * Call to await LODs loading during the next render cycle.
101
+ */
102
+ awaitLoading(opts?: PromiseGroupOptions): Promise<{
103
+ cancelled: boolean;
104
+ awaited_count: number;
105
+ resolved_count: number;
106
+ }>;
107
+ private _postprocessPromiseGroups;
96
108
  private readonly _lodchangedlisteners;
97
109
  addEventListener(evt: "changed", listener: LODChangedEventListener): void;
98
110
  removeEventListener(evt: "changed", listener: LODChangedEventListener): void;
@@ -137,6 +149,8 @@ export declare class LODsManager {
137
149
  private static corner3;
138
150
  private static readonly _tempPtInside;
139
151
  private static isInside;
152
+ private static skinnedMeshBoundsFrameOffsetCounter;
153
+ private static $skinnedMeshBoundsOffset;
140
154
  private calculateLodLevel;
141
155
  }
142
156
  declare class LOD_state {
@@ -1,10 +1,11 @@
1
1
  import { Box3, Clock, Matrix4, Mesh, MeshStandardMaterial, Sphere, Vector3 } from "three";
2
2
  import { NEEDLE_progressive } from "./extension.js";
3
3
  import { createLoaders } from "./loaders.js";
4
- import { getParam, isMobileDevice } from "./utils.internal.js";
4
+ import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
5
5
  import { plugins } from "./plugins/plugin.js";
6
6
  import { getRaycastMesh } from "./utils.js";
7
7
  import { applyDebugSettings, debug, debug_OverrideLodLevel } from "./lods.debug.js";
8
+ import { PromiseGroup } from "./lods.promise.js";
8
9
  const debugProgressiveLoading = getParam("debugprogressive");
9
10
  const suppressProgressiveLoading = getParam("noprogressive");
10
11
  const $lodsManager = Symbol("Needle:LODSManager");
@@ -78,8 +79,8 @@ export class LODsManager {
78
79
  renderer[$lodsManager] = lodsManager;
79
80
  return lodsManager;
80
81
  }
81
- context;
82
82
  renderer;
83
+ context;
83
84
  projectionScreenMatrix = new Matrix4();
84
85
  /** @deprecated use static `LODsManager.addPlugin()` method. This getter will be removed in later versions */
85
86
  get plugins() { return plugins; }
@@ -111,6 +112,37 @@ export class LODsManager {
111
112
  * @default false
112
113
  */
113
114
  manual = false;
115
+ _newPromiseGroups = [];
116
+ _promiseGroupIds = 0;
117
+ /**
118
+ * Call to await LODs loading during the next render cycle.
119
+ */
120
+ awaitLoading(opts) {
121
+ const id = this._promiseGroupIds++;
122
+ const newGroup = new PromiseGroup(this.#frame, { ...opts, });
123
+ this._newPromiseGroups.push(newGroup);
124
+ const start = performance.now();
125
+ newGroup.ready.finally(() => {
126
+ const index = this._newPromiseGroups.indexOf(newGroup);
127
+ if (index >= 0) {
128
+ this._newPromiseGroups.splice(index, 1);
129
+ if (isDevelopmentServer())
130
+ performance.measure("LODsManager:awaitLoading", {
131
+ start,
132
+ detail: { id, name: opts?.name, awaited: newGroup.awaitedCount, resolved: newGroup.resolvedCount }
133
+ });
134
+ }
135
+ });
136
+ return newGroup.ready;
137
+ }
138
+ _postprocessPromiseGroups() {
139
+ if (this._newPromiseGroups.length === 0)
140
+ return;
141
+ for (let i = this._newPromiseGroups.length - 1; i >= 0; i--) {
142
+ const group = this._newPromiseGroups[i];
143
+ group.update(this.#frame);
144
+ }
145
+ }
114
146
  _lodchangedlisteners = [];
115
147
  addEventListener(evt, listener) {
116
148
  if (evt === "changed") {
@@ -232,6 +264,7 @@ export class LODsManager {
232
264
  return;
233
265
  }
234
266
  this.internalUpdate(scene, camera);
267
+ this._postprocessPromiseGroups();
235
268
  }
236
269
  }
237
270
  /**
@@ -368,9 +401,10 @@ export class LODsManager {
368
401
  }
369
402
  if (update) {
370
403
  material[$currentLOD] = level;
371
- NEEDLE_progressive.assignTextureLOD(material, level).then(_ => {
404
+ const promise = NEEDLE_progressive.assignTextureLOD(material, level).then(_ => {
372
405
  this._lodchangedlisteners.forEach(l => l({ type: "texture", level, object: material }));
373
406
  });
407
+ PromiseGroup.addPromise("texture", material, promise, this._newPromiseGroups);
374
408
  }
375
409
  }
376
410
  /** Load progressive meshes for the given mesh
@@ -391,19 +425,14 @@ export class LODsManager {
391
425
  if (update) {
392
426
  mesh[$currentLOD] = level;
393
427
  const originalGeometry = mesh.geometry;
394
- return NEEDLE_progressive.assignMeshLOD(mesh, level).then(res => {
428
+ const promise = NEEDLE_progressive.assignMeshLOD(mesh, level).then(res => {
395
429
  if (res && mesh[$currentLOD] == level && originalGeometry != mesh.geometry) {
396
430
  this._lodchangedlisteners.forEach(l => l({ type: "mesh", level, object: mesh }));
397
- // if (this.handles) {
398
- // for (const inst of this.handles) {
399
- // // if (inst["LOD"] < level) continue;
400
- // // inst["LOD"] = level;
401
- // inst.setGeometry(mesh.geometry);
402
- // }
403
- // }
404
431
  }
405
432
  return res;
406
433
  });
434
+ PromiseGroup.addPromise("mesh", mesh, promise, this._newPromiseGroups);
435
+ return promise;
407
436
  }
408
437
  return Promise.resolve(null);
409
438
  }
@@ -428,6 +457,8 @@ export class LODsManager {
428
457
  const pt1 = this._tempPtInside.set(centerx, centery, min.z).applyMatrix4(matrix);
429
458
  return pt1.z < 0;
430
459
  }
460
+ static skinnedMeshBoundsFrameOffsetCounter = 0;
461
+ static $skinnedMeshBoundsOffset = Symbol("gltf-progressive-skinnedMeshBoundsOffset");
431
462
  calculateLodLevel(camera, mesh, state, desiredDensity, result) {
432
463
  if (!mesh) {
433
464
  result.mesh_lod = -1;
@@ -473,16 +504,25 @@ export class LODsManager {
473
504
  skinnedMesh.computeBoundingBox();
474
505
  }
475
506
  // Fix: https://linear.app/needle/issue/NE-5264
476
- else if (this.skinnedMeshAutoUpdateBoundsInterval > 0
477
- && state.frames % this.skinnedMeshAutoUpdateBoundsInterval === 0) {
478
- // use lowres geometry for bounding box calculation
479
- const raycastmesh = getRaycastMesh(skinnedMesh);
480
- const originalGeometry = skinnedMesh.geometry;
481
- if (raycastmesh) {
482
- skinnedMesh.geometry = raycastmesh;
507
+ else if (this.skinnedMeshAutoUpdateBoundsInterval > 0) {
508
+ // Save a frame offset per object to stagger updates of skinned meshes across multiple frames
509
+ // This isn't a perfect solution to improve perf impact of skinned mesh updates (e.g. large skinned meshes would still be costly)
510
+ // But for many smaller meshes it helps to avoid spikes in performance
511
+ if (!skinnedMesh[LODsManager.$skinnedMeshBoundsOffset]) {
512
+ const offset = LODsManager.skinnedMeshBoundsFrameOffsetCounter++;
513
+ skinnedMesh[LODsManager.$skinnedMeshBoundsOffset] = offset;
514
+ }
515
+ const frameOffset = skinnedMesh[LODsManager.$skinnedMeshBoundsOffset];
516
+ if ((state.frames + frameOffset) % this.skinnedMeshAutoUpdateBoundsInterval === 0) {
517
+ // use lowres geometry for bounding box calculation
518
+ const raycastmesh = getRaycastMesh(skinnedMesh);
519
+ const originalGeometry = skinnedMesh.geometry;
520
+ if (raycastmesh) {
521
+ skinnedMesh.geometry = raycastmesh;
522
+ }
523
+ skinnedMesh.computeBoundingBox();
524
+ skinnedMesh.geometry = originalGeometry;
483
525
  }
484
- skinnedMesh.computeBoundingBox();
485
- skinnedMesh.geometry = originalGeometry;
486
526
  }
487
527
  boundingBox = skinnedMesh.boundingBox;
488
528
  }
@@ -608,6 +648,10 @@ export class LODsManager {
608
648
  const lod = mesh_lods[l];
609
649
  const densityForThisLevel = lod.densities?.[primitive_index] || lod.density || .00001;
610
650
  const resultingDensity = densityForThisLevel / state.lastScreenCoverage;
651
+ if (primitive_index > 0 && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
652
+ window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
653
+ console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
654
+ }
611
655
  if (resultingDensity < desiredDensity) {
612
656
  expectedLevel = l;
613
657
  break;
@@ -0,0 +1,61 @@
1
+ type PromiseType = "texture" | "mesh";
2
+ export type PromiseGroupOptions = {
3
+ /** Name for debugging purposes */
4
+ name?: string;
5
+ /** Define many frames new LOD promises will be captured and awaited. The group will resolve after all promises captured during this time have resolved (or when the abort signal is triggered).
6
+ * @default 2 frames, which means the group will capture promises for 2 frames before resolving.
7
+ */
8
+ frames?: number;
9
+ /** An optional signal to abort the promise */
10
+ signal?: AbortSignal;
11
+ /**
12
+ * If set to true, the group will only await one promise per object.
13
+ * @default 1
14
+ */
15
+ maxPromisesPerObject?: number;
16
+ };
17
+ type PromiseGroupResolveResult = {
18
+ /**
19
+ * `true` if the group was cancelled, `false` if it was resolved normally.
20
+ */
21
+ cancelled: boolean;
22
+ /**
23
+ * The number of promises that started to being awaited
24
+ */
25
+ awaited_count: number;
26
+ /**
27
+ * The number of promises that were resolved
28
+ */
29
+ resolved_count: number;
30
+ };
31
+ /**
32
+ * A group of promises that can be awaited together.
33
+ * This is used for awaiting LOD
34
+ */
35
+ export declare class PromiseGroup {
36
+ static readonly addPromise: (type: PromiseType, object: object, promise: Promise<any>, groups: PromiseGroup[]) => void;
37
+ readonly frame_start: number;
38
+ readonly frame_capture_end: number;
39
+ readonly ready: Promise<PromiseGroupResolveResult>;
40
+ private _resolve;
41
+ private readonly _signal?;
42
+ /**
43
+ * The number of promises that have been added to this group so far.
44
+ */
45
+ get awaitedCount(): number;
46
+ get resolvedCount(): number;
47
+ get currentlyAwaiting(): number;
48
+ private _resolved;
49
+ private _addedCount;
50
+ private _resolvedCount;
51
+ /** These promises are currently being awaited */
52
+ private readonly _awaiting;
53
+ private _maxPromisesPerObject;
54
+ constructor(frame: number, options: PromiseGroupOptions);
55
+ private _currentFrame;
56
+ update(frame: number): void;
57
+ private readonly _seen;
58
+ private add;
59
+ private resolveNow;
60
+ }
61
+ export {};
@@ -0,0 +1,101 @@
1
+ import { debug } from "./lods.debug";
2
+ /**
3
+ * A group of promises that can be awaited together.
4
+ * This is used for awaiting LOD
5
+ */
6
+ export class PromiseGroup {
7
+ static addPromise = (type, object, promise, groups) => {
8
+ groups.forEach(group => {
9
+ group.add(type, object, promise);
10
+ });
11
+ };
12
+ frame_start;
13
+ frame_capture_end;
14
+ ready;
15
+ _resolve;
16
+ _signal;
17
+ /**
18
+ * The number of promises that have been added to this group so far.
19
+ */
20
+ get awaitedCount() {
21
+ return this._addedCount;
22
+ }
23
+ get resolvedCount() {
24
+ return this._resolvedCount;
25
+ }
26
+ get currentlyAwaiting() {
27
+ return this._awaiting.length;
28
+ }
29
+ _resolved = false;
30
+ _addedCount = 0;
31
+ _resolvedCount = 0;
32
+ /** These promises are currently being awaited */
33
+ _awaiting = [];
34
+ _maxPromisesPerObject = 1;
35
+ constructor(frame, options) {
36
+ const minFrames = 2; // wait at least 2 frames to capture promises
37
+ const framesToCapture = Math.max(options.frames ?? minFrames, minFrames); // default to 2 frames and make sure it's at least 2 frames
38
+ this.frame_start = frame;
39
+ this.frame_capture_end = frame + framesToCapture;
40
+ this.ready = new Promise((resolve) => {
41
+ this._resolve = resolve;
42
+ });
43
+ this.ready.finally(() => {
44
+ this._resolved = true;
45
+ this._awaiting.length = 0;
46
+ });
47
+ this._signal = options.signal;
48
+ this._signal?.addEventListener("abort", () => {
49
+ this.resolveNow();
50
+ });
51
+ this._maxPromisesPerObject = Math.max(1, options.maxPromisesPerObject ?? 1);
52
+ }
53
+ _currentFrame = 0;
54
+ update(frame) {
55
+ this._currentFrame = frame;
56
+ // If we've passes the frame capture end frame and didn't add any promises, we resolve immediately
57
+ if (this._signal?.aborted || (this._currentFrame > this.frame_capture_end && this._awaiting.length === 0)) {
58
+ this.resolveNow();
59
+ }
60
+ }
61
+ _seen = new WeakMap();
62
+ add(_type, object, promise) {
63
+ if (this._resolved) {
64
+ if (debug)
65
+ console.warn("PromiseGroup: Trying to add a promise to a resolved group, ignoring.");
66
+ return;
67
+ }
68
+ if (this._currentFrame > this.frame_capture_end) {
69
+ return; // we are not capturing any more promises
70
+ }
71
+ if (this._maxPromisesPerObject >= 1) {
72
+ if (this._seen.has(object)) {
73
+ let count = this._seen.get(object);
74
+ if (count >= this._maxPromisesPerObject) {
75
+ if (debug)
76
+ console.warn(`PromiseGroup: Already awaiting object ignoring new promise for it.`);
77
+ return;
78
+ }
79
+ this._seen.set(object, count + 1);
80
+ }
81
+ else {
82
+ this._seen.set(object, 1);
83
+ }
84
+ }
85
+ this._awaiting.push(promise);
86
+ this._addedCount++;
87
+ promise.finally(() => {
88
+ this._resolvedCount++;
89
+ this._awaiting.splice(this._awaiting.indexOf(promise), 1);
90
+ });
91
+ }
92
+ resolveNow() {
93
+ if (this._resolved)
94
+ return;
95
+ this._resolve?.({
96
+ awaited_count: this._addedCount,
97
+ resolved_count: this._resolvedCount,
98
+ cancelled: this._signal?.aborted ?? false,
99
+ });
100
+ }
101
+ }
@@ -1,4 +1,4 @@
1
- import { LODsManager } from "../lods_manager.js";
1
+ import { LODsManager } from "../lods.manager.js";
2
2
  import { EXTENSION_NAME, NEEDLE_progressive } from "../extension.js";
3
3
  const $meshLODSymbol = Symbol("NEEDLE_mesh_lod");
4
4
  const $textureLODSymbol = Symbol("NEEDLE_texture_lod");
@@ -3,3 +3,24 @@ export declare function getParam(name: string): boolean | string;
3
3
  export declare function resolveUrl(source: string | undefined, uri: string): string;
4
4
  /** @returns `true` if it's a phone or tablet */
5
5
  export declare function isMobileDevice(): boolean;
6
+ export declare function isDevelopmentServer(): boolean;
7
+ type SlotReturnValue = {
8
+ use?: ((promise: Promise<any>) => void);
9
+ };
10
+ export declare class PromiseQueue {
11
+ readonly maxConcurrent: number;
12
+ private readonly _running;
13
+ private readonly _queue;
14
+ debug: boolean;
15
+ constructor(maxConcurrent?: number, opts?: {
16
+ debug?: boolean;
17
+ });
18
+ private tick;
19
+ /**
20
+ * Request a slot for a promise with a specific key. This function returns a promise with a `use` method that can be called to add the promise to the queue.
21
+ */
22
+ slot(key: string): Promise<SlotReturnValue>;
23
+ private add;
24
+ private internalUpdate;
25
+ }
26
+ export {};