@needle-tools/engine 4.16.10 → 4.16.12

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/SKILL.md +64 -4
  3. package/dist/gltf-progressive-DLDvZ8Xm.umd.cjs +10 -0
  4. package/dist/{gltf-progressive-CzxjNmG6.js → gltf-progressive-DMK8kKVB.js} +352 -346
  5. package/dist/gltf-progressive-RPgYR6rv.min.js +10 -0
  6. package/dist/{needle-engine.bundle-a0Pw5htf.js → needle-engine.bundle-BfsnYTeP.js} +7329 -7105
  7. package/dist/{needle-engine.bundle-D_nuVSFb.umd.cjs → needle-engine.bundle-COr8D1vc.umd.cjs} +132 -132
  8. package/dist/{needle-engine.bundle-DlyYXu9v.min.js → needle-engine.bundle-DG1sGkxn.min.js} +131 -131
  9. package/dist/needle-engine.d.ts +60 -3
  10. package/dist/needle-engine.js +3 -3
  11. package/dist/needle-engine.min.js +1 -1
  12. package/dist/needle-engine.umd.cjs +1 -1
  13. package/dist/three-examples.js +52 -52
  14. package/dist/three-examples.min.js +4 -4
  15. package/dist/three-examples.umd.cjs +2 -2
  16. package/lib/engine/api.d.ts +2 -1
  17. package/lib/engine/api.js +1 -6
  18. package/lib/engine/api.js.map +1 -1
  19. package/lib/engine/debug/debug_spector.js +1 -1
  20. package/lib/engine/debug/debug_spector.js.map +1 -1
  21. package/lib/engine/engine_addressables.d.ts +32 -1
  22. package/lib/engine/engine_addressables.js +71 -9
  23. package/lib/engine/engine_addressables.js.map +1 -1
  24. package/lib/engine/engine_application.d.ts +2 -0
  25. package/lib/engine/engine_application.js +7 -3
  26. package/lib/engine/engine_application.js.map +1 -1
  27. package/lib/engine/engine_audio.d.ts +30 -1
  28. package/lib/engine/engine_audio.js +57 -13
  29. package/lib/engine/engine_audio.js.map +1 -1
  30. package/lib/engine/engine_context.js +25 -1
  31. package/lib/engine/engine_context.js.map +1 -1
  32. package/lib/engine/engine_context_registry.js +1 -1
  33. package/lib/engine/engine_context_registry.js.map +1 -1
  34. package/lib/engine/engine_input.js +1 -1
  35. package/lib/engine/engine_input.js.map +1 -1
  36. package/lib/engine/engine_license.d.ts +1 -1
  37. package/lib/engine/engine_license.js +31 -5
  38. package/lib/engine/engine_license.js.map +1 -1
  39. package/lib/engine/engine_lifecycle_functions_internal.js +20 -2
  40. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  41. package/lib/engine/engine_networking_streams.d.ts +5 -1
  42. package/lib/engine/engine_networking_streams.js +64 -17
  43. package/lib/engine/engine_networking_streams.js.map +1 -1
  44. package/lib/engine/engine_shims.js +1 -1
  45. package/lib/engine/engine_shims.js.map +1 -1
  46. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +5 -0
  47. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  48. package/lib/engine/webcomponents/fonts.d.ts +1 -1
  49. package/lib/engine/webcomponents/fonts.js +7 -0
  50. package/lib/engine/webcomponents/fonts.js.map +1 -1
  51. package/lib/engine/webcomponents/icons.js +8 -1
  52. package/lib/engine/webcomponents/icons.js.map +1 -1
  53. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +5 -0
  54. package/lib/engine/webcomponents/needle menu/needle-menu.js +31 -13
  55. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  56. package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
  57. package/lib/engine/webcomponents/needle-engine.js +16 -4
  58. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  59. package/lib/engine/webcomponents/needle-engine.loading.d.ts +2 -0
  60. package/lib/engine/webcomponents/needle-engine.loading.js +10 -0
  61. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  62. package/lib/engine-components/AudioSource.d.ts +49 -2
  63. package/lib/engine-components/AudioSource.js +261 -66
  64. package/lib/engine-components/AudioSource.js.map +1 -1
  65. package/lib/engine-components/CameraUtils.js +1 -1
  66. package/lib/engine-components/CameraUtils.js.map +1 -1
  67. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  68. package/package.json +2 -2
  69. package/src/engine/api.ts +3 -1
  70. package/src/engine/debug/debug_spector.ts +1 -1
  71. package/src/engine/engine_addressables.ts +75 -9
  72. package/src/engine/engine_application.ts +7 -2
  73. package/src/engine/engine_audio.ts +68 -15
  74. package/src/engine/engine_context.ts +25 -1
  75. package/src/engine/engine_context_registry.ts +1 -1
  76. package/src/engine/engine_input.ts +2 -2
  77. package/src/engine/engine_license.ts +28 -5
  78. package/src/engine/engine_lifecycle_functions_internal.ts +21 -2
  79. package/src/engine/engine_networking_streams.ts +64 -19
  80. package/src/engine/engine_shims.ts +1 -1
  81. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +8 -1
  82. package/src/engine/webcomponents/fonts.ts +6 -0
  83. package/src/engine/webcomponents/icons.ts +9 -2
  84. package/src/engine/webcomponents/needle menu/needle-menu.ts +30 -12
  85. package/src/engine/webcomponents/needle-engine.loading.ts +13 -1
  86. package/src/engine/webcomponents/needle-engine.ts +15 -5
  87. package/src/engine-components/AudioSource.ts +244 -63
  88. package/src/engine-components/CameraUtils.ts +1 -1
  89. package/dist/gltf-progressive-BacJPTD6.umd.cjs +0 -10
  90. package/dist/gltf-progressive-BmXoz_HR.min.js +0 -10
@@ -1,4 +1,4 @@
1
- import { AudioLoader, PositionalAudio } from "three";
1
+ import { PositionalAudio } from "three";
2
2
  import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
3
3
 
4
4
  import { isDevEnvironment } from "../engine/debug/index.js";
@@ -137,7 +137,13 @@ export class AudioSource extends Behaviour {
137
137
  *
138
138
  * @returns True if the audio is playing, false otherwise
139
139
  */
140
- get isPlaying(): boolean { return this.sound?.isPlaying ?? false; }
140
+ get isPlaying(): boolean {
141
+ // For the media-element path (file-URL string clips) three.js does NOT maintain
142
+ // sound.isPlaying (hasPlaybackControl is false once a media source is set), so the
143
+ // <audio> element is the source of truth.
144
+ if (this._usesMediaElementSource && this._audioElement) return !this._audioElement.paused && !this._audioElement.ended;
145
+ return this.sound?.isPlaying ?? false;
146
+ }
141
147
 
142
148
  /**
143
149
  * The total duration of the current audio clip in seconds.
@@ -145,6 +151,9 @@ export class AudioSource extends Behaviour {
145
151
  * @returns Duration in seconds or undefined if no clip is loaded
146
152
  */
147
153
  get duration() {
154
+ // Media-element path is the primary source of truth for file-URL string clips.
155
+ const elDuration = this._usesMediaElementSource ? this._audioElement?.duration : undefined;
156
+ if (elDuration && isFinite(elDuration)) return elDuration;
148
157
  return this.sound?.buffer?.duration;
149
158
  }
150
159
 
@@ -154,14 +163,14 @@ export class AudioSource extends Behaviour {
154
163
  */
155
164
  get time01() {
156
165
  const duration = this.duration;
157
- if (duration && this.sound) {
158
- return this.sound?.context.currentTime / duration;
166
+ if (duration) {
167
+ return this.time / duration;
159
168
  }
160
169
  return 0;
161
170
  }
162
171
  set time01(val: number) {
163
172
  const duration = this.duration;
164
- if (duration && this.sound) {
173
+ if (duration) {
165
174
  this.time = val * duration;
166
175
  }
167
176
  }
@@ -170,8 +179,17 @@ export class AudioSource extends Behaviour {
170
179
  * The current playback position in seconds.
171
180
  * Can be set to seek to a specific time in the audio.
172
181
  */
173
- get time(): number { return this.sound?.source ? (this.sound.source?.context.currentTime - this._lastContextTime + this.sound.offset) : 0; }
182
+ get time(): number {
183
+ // Media-element path: the element's currentTime is authoritative.
184
+ if (this._usesMediaElementSource && this._audioElement) return this._audioElement.currentTime;
185
+ return this.sound?.source ? (this.sound.source?.context.currentTime - this._lastContextTime + this.sound.offset) : 0;
186
+ }
174
187
  set time(val: number) {
188
+ // Media-element path: seek directly on the element (sound.offset is not used for media nodes).
189
+ if (this._usesMediaElementSource && this._audioElement) {
190
+ this._audioElement.currentTime = val;
191
+ return;
192
+ }
175
193
  if (this.sound) {
176
194
  if (val === this.sound.offset) return;
177
195
  const wasPlaying = this.isPlaying;
@@ -189,12 +207,18 @@ export class AudioSource extends Behaviour {
189
207
  */
190
208
  @serializable()
191
209
  get loop(): boolean {
210
+ // Media-element path: the element holds the authoritative loop flag.
211
+ if (this._usesMediaElementSource && this._audioElement) {
212
+ this._loop = this._audioElement.loop;
213
+ return this._loop;
214
+ }
192
215
  if (this.sound) this._loop = this.sound.getLoop();
193
216
  return this._loop;
194
217
  }
195
218
  set loop(val: boolean) {
196
219
  this._loop = val;
197
- if (this.sound) this.sound.setLoop(val);
220
+ if (this._usesMediaElementSource && this._audioElement) this._audioElement.loop = val;
221
+ else if (this.sound) this.sound.setLoop(val);
198
222
  }
199
223
 
200
224
  /**
@@ -268,9 +292,13 @@ export class AudioSource extends Behaviour {
268
292
  */
269
293
  @serializable()
270
294
  set pitch(val: number) {
271
- if (this.sound) this.sound.setPlaybackRate(val);
295
+ // Media-element path: playbackRate on the element (sound.setPlaybackRate is a no-op once
296
+ // hasPlaybackControl is false).
297
+ if (this._usesMediaElementSource && this._audioElement) this._audioElement.playbackRate = val;
298
+ else if (this.sound) this.sound.setPlaybackRate(val);
272
299
  }
273
300
  get pitch(): number {
301
+ if (this._usesMediaElementSource && this._audioElement) return this._audioElement.playbackRate;
274
302
  return this.sound ? this.sound.getPlaybackRate() : 1;
275
303
  }
276
304
 
@@ -287,12 +315,21 @@ export class AudioSource extends Behaviour {
287
315
  private sound: PositionalAudio | null = null;
288
316
  private helper: PositionalAudioHelper | null = null;
289
317
  private wasPlaying = false;
290
- private audioLoader: AudioLoader | null = null;
291
318
  private shouldPlay: boolean = false;
292
319
  // set this from audio context time, used to set clip offset when setting "time" property
293
320
  // there is maybe a better way to set a audio clip current time?!
294
- private _lastClipStartedLoading: string | MediaStream | null = null;
321
+ private _loadedClip: string | MediaStream | null = null;
295
322
  private _audioElement: HTMLAudioElement | null = null;
323
+ /**
324
+ * True when {@link _audioElement} is a file-URL element routed through
325
+ * `sound.setMediaElementSource()` — the iOS-survivable string-clip path. For this path ALL
326
+ * playback control (play/pause/stop/loop/pitch/time/isPlaying) goes through the element,
327
+ * because three.js sets `hasPlaybackControl = false` once a media source is attached.
328
+ * It is `false` for the distinct MediaStream path.
329
+ */
330
+ private _usesMediaElementSource: boolean = false;
331
+ /** Dedicated gain between the media source and the panner, used for click-free pause/resume fades. */
332
+ private _fadeNode: GainNode | null = null;
296
333
 
297
334
  /**
298
335
  * Returns the underlying {@link PositionalAudio} object, creating it if necessary.
@@ -368,16 +405,42 @@ export class AudioSource extends Behaviour {
368
405
  return this.sound?.context;
369
406
  }
370
407
 
408
+ /**
409
+ * Resumes the shared AudioContext if it is currently suspended/interrupted and the page is
410
+ * visible. No-op when already running or when resuming is not allowed (resuming a hidden
411
+ * context throws iOS `InvalidStateError: Failed to start the audio device`).
412
+ */
413
+ private ensureContextResumed() {
414
+ const ctx = this.audioContext;
415
+ if (!ctx) return;
416
+ const visibility = typeof document !== "undefined" ? document.visibilityState : "visible";
417
+ if (visibility !== "visible") return;
418
+ // Resume even when ctx.state reports "running": on iOS an auto-resume that happened outside a
419
+ // user gesture can leave the context "running" while the audio hardware stays off. play() is
420
+ // user-initiated, so this resume() runs inside the gesture and re-enables real output.
421
+ ctx.resume().catch(e => { if (debug) console.warn("[AudioSource] Failed to resume AudioContext:", e); });
422
+ }
423
+
371
424
  /** @internal */
372
425
  awake() {
373
426
  if (debug) console.log("[AudioSource]", this);
374
427
 
375
- this.audioLoader = new AudioLoader();
376
428
  if (this.playOnAwake) this.shouldPlay = true;
377
429
 
378
430
  if (this.preload) {
379
- if (typeof this.clip === "string") {
380
- this.audioLoader.load(this.clip, this.createAudio, () => { }, console.error);
431
+ if (typeof this.clip === "string" && this.clip.length > 0) {
432
+ // Preload = create the <audio> element and wire it into the graph, but do NOT play.
433
+ // Wiring needs the PositionalAudio (Sound), which needs an AudioListener that only
434
+ // exists after a user gesture — so defer when not ready.
435
+ if (this.Sound) {
436
+ this.createAudioFromElement();
437
+ }
438
+ else {
439
+ AudioSource.registerWaitForAllowAudio(() => {
440
+ if (this.destroyed || this.enabled === false) return;
441
+ if (typeof this.clip === "string" && this.clip.length > 0) this.createAudioFromElement();
442
+ });
443
+ }
381
444
  }
382
445
  }
383
446
  }
@@ -411,15 +474,22 @@ export class AudioSource extends Behaviour {
411
474
  switch (document.visibilityState) {
412
475
  case "hidden":
413
476
  if (this.playInBackground === false || DeviceUtilities.isMobileDevice()) {
414
- this.wasPlaying = this.isPlaying;
415
- if (this.isPlaying) {
477
+ const wasPlaying = this.isPlaying;
478
+ if (wasPlaying) {
416
479
  this.pause();
417
480
  }
481
+ // pause() clears wasPlaying so a *user-initiated* pause is not auto-resumed on
482
+ // the next foreground. This pause is automatic (page hidden / device locked), so
483
+ // re-assert the resume intent AFTER pause() — otherwise we never resume on unlock.
484
+ this.wasPlaying = wasPlaying;
418
485
  }
419
486
  break;
420
487
  case "visible":
421
488
  if (debug) console.log("visible", this.enabled, this.playOnAwake, !this.isPlaying, AudioSource.userInteractionRegistered, this.wasPlaying);
422
- if (this.enabled && this.playOnAwake && !this.isPlaying && AudioSource.userInteractionRegistered && this.wasPlaying) {
489
+ // Resume anything that was actively playing when the page was hidden (not just
490
+ // playOnAwake sources). wasPlaying is only set when this source was playing at hide
491
+ // time, so script-started clips resume too.
492
+ if (this.enabled && !this.isPlaying && AudioSource.userInteractionRegistered && this.wasPlaying) {
423
493
  this.play();
424
494
  }
425
495
  break;
@@ -433,44 +503,117 @@ export class AudioSource extends Behaviour {
433
503
  this.sound?.setVolume(this.volume);
434
504
  }
435
505
 
436
- private createAudio = (buffer?: AudioBuffer) => {
437
- if(this.destroyed) {
438
- if(debug) console.warn("AudioSource destroyed, not creating audio", this.name);
506
+ /**
507
+ * Sets up playback of a file-URL string clip through an `HTMLAudioElement` routed into the Web
508
+ * Audio graph via `sound.setMediaElementSource()`.
509
+ *
510
+ * Why a media element instead of a decoded `AudioBuffer`: on iOS Safari an
511
+ * `AudioBufferSourceNode`'s output is torn down after a device lock / audio-session interruption
512
+ * and cannot be reliably revived. A `MediaElementAudioSourceNode` survives because the playing
513
+ * `<audio>` element keeps iOS's media session alive, so the shared AudioContext resumes cleanly
514
+ * on unlock. This mirrors three.js's `webaudio_orientation` example.
515
+ *
516
+ * Does NOT start playback — it only creates/wires the element so a later `.play()` is cheap.
517
+ */
518
+ private createAudioFromElement() {
519
+ if (this.destroyed) {
520
+ if (debug) console.warn("AudioSource destroyed, not creating audio", this.name);
439
521
  return;
440
522
  }
441
-
442
- if (debug) console.log("AudioBuffer finished loading", buffer);
443
-
444
523
  const sound = this.Sound;
445
524
  if (!sound) {
446
525
  if (debug) console.warn("Failed getting sound?", this.name);
447
526
  return;
448
527
  }
449
528
 
450
- if (sound.isPlaying)
451
- sound.stop();
529
+ // Already wired for the current clip — re-running setMediaElementSource on the same element
530
+ // throws InvalidStateError, so just keep volume/mute in sync and bail.
531
+ if (this._audioElement && this._usesMediaElementSource && this._loadedClip === this.clip) {
532
+ if (this.context.application.muted) sound.setVolume(0);
533
+ else sound.setVolume(this.volume);
534
+ return;
535
+ }
536
+
537
+ // (re)create the element when the clip changed since the element was wired.
538
+ if (this._audioElement && this._loadedClip !== this.clip) {
539
+ this._audioElement.pause();
540
+ this._audioElement = null;
541
+ this._usesMediaElementSource = false;
542
+ }
543
+ if (!this._audioElement) {
544
+ const el = new Audio();
545
+ el.crossOrigin = "anonymous";
546
+ el.preload = "auto";
547
+ el.src = this.clip as string;
548
+ this._audioElement = el;
549
+ }
550
+ this._audioElement!.loop = this._loop;
551
+
552
+ // IMPORTANT: build the fade node FIRST so getOutput() returns it, THEN route the media
553
+ // element source in. setMediaElementSource() immediately calls sound.connect()
554
+ // (source → getOutput()), so the fade node and the getOutput override must already exist.
555
+ this.setupFadeNode();
556
+ // Only connect once per element — createMediaElementSource() throws if the same element is
557
+ // passed twice.
558
+ if (!this._usesMediaElementSource) {
559
+ sound.setMediaElementSource(this._audioElement!);
560
+ this._usesMediaElementSource = true;
561
+ }
562
+ this._loadedClip = this.clip;
452
563
 
453
- if (buffer) sound.setBuffer(buffer);
454
- sound.loop = this._loop;
455
564
  if (this.context.application.muted) sound.setVolume(0);
456
565
  else sound.setVolume(this.volume);
457
- sound.autoplay = this.shouldPlay && AudioSource.userInteractionRegistered;
458
566
  this.applySpatialDistanceSettings();
567
+ }
459
568
 
460
- if (sound.isPlaying)
461
- sound.stop();
569
+ /**
570
+ * Inserts a dedicated gain node between the media source and the panner so pause/resume can fade
571
+ * click-free without touching `sound.gain` (which carries volume/mute). three.js connects the
572
+ * source to `getOutput()` inside `setMediaElementSource()`, so we override it to feed our fade
573
+ * node, then route fade → panner (→ gain → listener, both already wired by PositionalAudio).
574
+ */
575
+ private setupFadeNode() {
576
+ const sound = this.sound;
577
+ if (!sound || this._fadeNode) return;
578
+ this._fadeNode = sound.context.createGain();
579
+ this._fadeNode.connect(sound.panner);
580
+ sound.getOutput = () => this._fadeNode! as unknown as PannerNode;
581
+ }
462
582
 
463
- // const panner = sound.panner;
464
- // panner.coneOuterGain = 1;
465
- // sound.setDirectionalCone(360, 360, 1);
466
- // const src = sound.context.createBufferSource();
467
- // src.buffer = sound.buffer;
468
- // src.connect(sound.panner);
469
- // src.start(this.audioContext?.currentTime);
470
- // const gain = sound.context.createGain();
471
- // gain.gain.value = 1 - this.spatialBlend;
472
- // src.connect(gain);
583
+ /**
584
+ * Smoothly ramp the fade gain to `target` (1 = audible, 0 = silent) over a few ms. A short fade —
585
+ * not a hard 0/1 gain change and not a connect/disconnect — avoids the click on pause/resume. On
586
+ * iOS the MediaElementAudioSourceNode keeps feeding the graph after element.pause() following an
587
+ * interruption, so fading this gain to 0 is what actually silences a "paused" media clip.
588
+ */
589
+ private fadeGain(target: number, seconds = 0.02) {
590
+ if (!this._fadeNode || !this.sound) return;
591
+ const ctx = this.sound.context;
592
+ const g = this._fadeNode.gain;
593
+ g.cancelScheduledValues(ctx.currentTime);
594
+ g.setValueAtTime(g.value, ctx.currentTime);
595
+ g.linearRampToValueAtTime(target, ctx.currentTime + seconds);
596
+ }
473
597
 
598
+ /**
599
+ * Sets up the {@link PositionalAudio} graph for a MediaStream clip (file-URL string clips go
600
+ * through {@link createAudioFromElement}). The stream node itself is attached in {@link play}
601
+ * via `setMediaStreamSource`.
602
+ */
603
+ private createAudio = () => {
604
+ if (this.destroyed) {
605
+ if (debug) console.warn("AudioSource destroyed, not creating audio", this.name);
606
+ return;
607
+ }
608
+ const sound = this.Sound;
609
+ if (!sound) {
610
+ if (debug) console.warn("Failed getting sound?", this.name);
611
+ return;
612
+ }
613
+ this._loadedClip = this.clip;
614
+ if (this.context.application.muted) sound.setVolume(0);
615
+ else sound.setVolume(this.volume);
616
+ this.applySpatialDistanceSettings();
474
617
  // make sure we only play the sound if the user has interacted with the page
475
618
  AudioSource.registerWaitForAllowAudio(this.__onAllowAudioCallback);
476
619
  }
@@ -520,20 +663,20 @@ export class AudioSource extends Behaviour {
520
663
  if (debug)
521
664
  console.log(clip);
522
665
  if (clip.endsWith(".mp3") || clip.endsWith(".wav")) {
523
- if (!this.audioLoader)
524
- this.audioLoader = new AudioLoader();
525
666
  this.shouldPlay = true;
526
- if (this._lastClipStartedLoading === clip) {
527
- if (debug) console.log("Is currently loading:", this._lastClipStartedLoading, this)
528
- return;
667
+ // Route the file URL through an <audio> element (survives iOS lock — see
668
+ // createAudioFromElement). This creates/wires the element without playing.
669
+ this.createAudioFromElement();
670
+ // Start playback through the element (NOT this.sound — hasPlaybackControl is false
671
+ // for a media-element source). Covers the already-interacted case directly.
672
+ if (this.shouldPlay && AudioSource.userInteractionRegistered) {
673
+ this.ensureContextResumed();
674
+ this._hasEnded = false;
675
+ this._audioElement?.play().catch(e => {
676
+ if (debug) console.warn(`[AudioSource] Failed to play audio clip "${clip}":`, e);
677
+ });
678
+ this.fadeGain(1);
529
679
  }
530
- this._lastClipStartedLoading = clip;
531
- if (debug)
532
- console.log("load audio", clip);
533
- const buffer = await this.audioLoader.loadAsync(clip).catch(console.error);
534
- if(this.destroyed) return;
535
- if(this._lastClipStartedLoading === clip) this._lastClipStartedLoading = null;
536
- if (buffer) this.createAudio(buffer);
537
680
  }
538
681
  else console.warn("Unsupported audio clip type", clip)
539
682
  }
@@ -563,9 +706,11 @@ export class AudioSource extends Behaviour {
563
706
  clip = this.clip;
564
707
  }
565
708
 
566
- // Check if we need to call load first
567
- let needsLoading = !this.sound || (clip && clip !== this.clip);
568
- if (typeof clip === "string" && !this.audioLoader) needsLoading = true;
709
+ // Load if the sound hasn't been created yet or if the clip changed since last load.
710
+ let needsLoading = !this.sound || (clip && clip !== this._loadedClip);
711
+ // For string clips the <audio> element must additionally be wired (createAudioFromElement);
712
+ // if it isn't yet (e.g. preload was off, or the element was torn down), force a (re)load.
713
+ if (typeof clip === "string" && (!this._audioElement || !this._usesMediaElementSource)) needsLoading = true;
569
714
  if (clip instanceof MediaStream || typeof clip === "string")
570
715
  this.clip = clip;
571
716
  if (needsLoading) {
@@ -578,15 +723,21 @@ export class AudioSource extends Behaviour {
578
723
  this._hasEnded = false;
579
724
  if (debug)
580
725
  console.log("play", this.sound?.getVolume(), this.sound);
581
- if (this.sound && !this.sound.isPlaying) {
726
+ // Use the element-aware isPlaying getter so we don't re-trigger an already-playing media clip.
727
+ if (this.sound && !this.isPlaying) {
728
+ // On iOS the shared context can be suspended/interrupted (lock, backgrounding, a call) —
729
+ // resume it here (we are inside a user gesture) so playback doesn't silently depend on
730
+ // the global resume handler running first. See engine_audio.ts.
731
+ this.ensureContextResumed();
582
732
  const muted = this.context.application.muted;
583
733
  if (muted) this.sound.setVolume(0);
584
734
  this.gameObject?.add(this.sound);
585
735
 
586
736
  if (this.clip instanceof MediaStream) {
587
737
 
588
- // We have to set the audio element source to the mediastream as well
589
- // otherwise it will not play for some reason...
738
+ // Distinct from the file-URL media-element path: a MediaStream uses
739
+ // setMediaStreamSource + an element holding srcObject.
740
+ this._usesMediaElementSource = false;
590
741
  this.sound.setMediaStreamSource(this.clip);
591
742
 
592
743
  if (!this._audioElement) {
@@ -600,8 +751,13 @@ export class AudioSource extends Behaviour {
600
751
 
601
752
  }
602
753
  else {
603
- if (this._audioElement) this._audioElement.remove();
604
- this.sound.play(muted ? .1 : 0);
754
+ // File-URL media-element path: playback control is on the element, not this.sound.
755
+ // Resume from the (frozen) position and fade the gain back in so the resume is
756
+ // click-free.
757
+ this._audioElement?.play().catch(e => {
758
+ if (debug) console.warn("[AudioSource] Failed to play audio element:", e);
759
+ });
760
+ this.fadeGain(1);
605
761
  }
606
762
  }
607
763
  }
@@ -611,13 +767,26 @@ export class AudioSource extends Behaviour {
611
767
  * Use play() to resume from the paused position.
612
768
  */
613
769
  pause() {
614
- if (debug) console.log("Pause", this);
770
+ if (debug) console.log("[AudioSource] pause", this);
615
771
  this._hasEnded = true;
616
772
  this.shouldPlay = false;
773
+ // A user-initiated pause must not be revived by the visibility handler on the next
774
+ // foreground (it replays sources whose `wasPlaying` is set).
775
+ this.wasPlaying = false;
776
+ if (this._usesMediaElementSource) {
777
+ // File-URL media-element path. On iOS the MediaElementAudioSourceNode keeps emitting into
778
+ // the graph after element.pause() once the audio session has been interrupted — pausing
779
+ // the element does NOT silence it. So fade the gain to 0 (click-free silence), then
780
+ // freeze the element's position. play() fades it back in.
781
+ this.fadeGain(0);
782
+ this._audioElement?.pause();
783
+ return;
784
+ }
617
785
  if (this.sound && this.sound.isPlaying && this.sound.source) {
618
786
  this._lastContextTime = this.sound?.context.currentTime;
619
787
  this.sound.pause();
620
788
  }
789
+ // MediaStream / legacy path: drop the stream element as before.
621
790
  this._audioElement?.remove();
622
791
  }
623
792
 
@@ -626,15 +795,25 @@ export class AudioSource extends Behaviour {
626
795
  * Unlike pause(), calling play() after stop() will start from the beginning.
627
796
  */
628
797
  stop() {
629
- if (debug) console.log("Pause", this);
798
+ if (debug) console.log("[AudioSource] stop", this);
630
799
  this._hasEnded = true;
631
800
  this.shouldPlay = false;
801
+ this.wasPlaying = false;
802
+ if (this._usesMediaElementSource) {
803
+ // True stop: fade to silence (element.pause() alone doesn't silence it on iOS after an
804
+ // interruption), pause, and rewind to the start.
805
+ this.fadeGain(0);
806
+ this._audioElement?.pause();
807
+ if (this._audioElement) this._audioElement.currentTime = 0;
808
+ return;
809
+ }
632
810
  if (this.sound && this.sound.source) {
633
811
  this._lastContextTime = this.sound?.context.currentTime;
634
812
  if (debug)
635
- console.log(this._lastContextTime)
813
+ console.log("[AudioSource] lastContextTime", this._lastContextTime);
636
814
  this.sound.stop();
637
815
  }
816
+ // MediaStream / legacy path: drop the stream element as before.
638
817
  this._audioElement?.remove();
639
818
  }
640
819
 
@@ -655,7 +834,9 @@ export class AudioSource extends Behaviour {
655
834
  this.applySpatialDistanceSettings();
656
835
  }
657
836
 
658
- if (this.sound && !this.sound.isPlaying && this.shouldPlay && !this._hasEnded) {
837
+ // Use the element-aware isPlaying getter, NOT sound.isPlaying: for a media-element source
838
+ // three.js leaves sound.isPlaying permanently false, which would fire "ended" every frame.
839
+ if (this.sound && !this.isPlaying && this.shouldPlay && !this._hasEnded) {
659
840
  this._hasEnded = true;
660
841
  if (debug)
661
842
  console.log("Audio clip ended", this.clip);
@@ -46,7 +46,7 @@ ContextRegistry.registerCallback(ContextEvent.MissingCamera, (evt) => {
46
46
  // Don't set the background color if the user set a background color in the <needle-engine> element
47
47
  if (!evt.context.domElement.getAttribute("background-color")) {
48
48
  let backgroundColor = "#efefef";
49
- if (typeof window !== undefined && (window.matchMedia('(prefers-color-scheme: dark)').matches)) {
49
+ if (typeof window !== "undefined" && (window.matchMedia('(prefers-color-scheme: dark)').matches)) {
50
50
  backgroundColor = "#1f1f1f";
51
51
  }
52
52
  scene.background = new Color(backgroundColor); // dont set it on the camera because this might be controlled from "background-color" attribute which is set on the scene directly. If the camera has a background color, it will override the scene's background color
@@ -1,10 +0,0 @@
1
- "use strict";const c=require("./three.umd.cjs"),X=require("./three-examples.umd.cjs");var le=typeof document<"u"?document.currentScript:null;const $e="";globalThis.GLTF_PROGRESSIVE_VERSION=$e;console.debug("[gltf-progressive] version -");let I="https://www.gstatic.com/draco/versioned/decoders/1.5.7/",N="https://cdn.needle.tools/static/three/0.179.1/basis2/";const Be=I,Ge=N,Oe=new URL(I+"draco_decoder.js");Oe.searchParams.append("range","true");fetch(Oe,{method:"GET",headers:{Range:"bytes=0-1"}}).catch(i=>{console.debug(`Failed to fetch remote Draco decoder from ${I} (offline: ${typeof navigator<"u"?navigator.onLine:"unknown"})`),I===Be&&Te("./include/draco/"),N===Ge&&Se("./include/ktx2/")}).finally(()=>{Pe()});const Fe=()=>({dracoDecoderPath:I,ktx2TranscoderPath:N});function Te(i){I=i,A&&A[pe]!=I?(console.debug("Updating Draco decoder path to "+i),A[pe]=I,A.setDecoderPath(I),A.preload()):console.debug("Setting Draco decoder path to "+i)}function Se(i){N=i,$&&$.transcoderPath!=N?(console.debug("Updating KTX2 transcoder path to "+i),$.setTranscoderPath(N),$.init()):console.debug("Setting KTX2 transcoder path to "+i)}function ne(i){return Pe(),i?$.detectSupport(i):i!==null&&console.warn("No renderer provided to detect ktx2 support - loading KTX2 textures might fail"),{dracoLoader:A,ktx2Loader:$,meshoptDecoder:oe}}function xe(i){i.dracoLoader||i.setDRACOLoader(A),i.ktx2Loader||i.setKTX2Loader($),i.meshoptDecoder||i.setMeshoptDecoder(oe)}const pe=Symbol("dracoDecoderPath");let A,oe,$;function Pe(){A||(A=new X.DRACOLoader,A[pe]=I,A.setDecoderPath(I),A.setDecoderConfig({type:"js"}),A.preload()),$||($=new X.KTX2Loader,$.setTranscoderPath(N),$.init()),oe||(oe=X.MeshoptDecoder)}const me=new WeakMap;function we(i,t){let e=me.get(i);e?e=Object.assign(e,t):e=t,me.set(i,e)}const Ue=X.GLTFLoader.prototype.load;function We(...i){const t=me.get(this);let e=i[0];const s=new URL(e,window.location.href);if(s.hostname.endsWith("needle.tools")){const o=t?.progressive!==void 0?t.progressive:!0,n=t?.usecase?t.usecase:"default";o?this.requestHeader.Accept=`*/*;progressive=allowed;usecase=${n}`:this.requestHeader.Accept=`*/*;usecase=${n}`,e=s.toString()}return i[0]=e,Ue?.call(this,...i)}X.GLTFLoader.prototype.load=We;z("debugprogressive");function z(i){if(typeof window>"u")return!1;const e=new URL(window.location.href).searchParams.get(i);return e==null||e==="0"||e==="false"?!1:e===""?!0:e}function ze(i,t){if(t===void 0||i===void 0||t.startsWith("./")||t.startsWith("http")||t.startsWith("data:")||t.startsWith("blob:"))return t;const e=i.lastIndexOf("/");if(e>=0){const s=i.substring(0,e+1);for(;s.endsWith("/")&&t.startsWith("/");)t=t.substring(1);return s+t}return t}function Le(){return Y!==void 0||(Y=/iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent),z("debugprogressive")&&console.log("[glTF Progressive]: isMobileDevice",Y)),Y}let Y;function Me(){if(typeof window>"u")return!1;const i=new URL(window.location.href),t=i.hostname==="localhost"||/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(i.hostname);return i.hostname==="127.0.0.1"||t}class Ve{constructor(t,e={}){this.maxConcurrent=t,this.debug=e.debug??!1,window.requestAnimationFrame(this.tick)}_running=new Map;_queue=[];debug=!1;tick=()=>{this.internalUpdate(),setTimeout(this.tick,10)};slot(t){return this.debug&&console.debug(`[PromiseQueue]: Requesting slot for key ${t}, running: ${this._running.size}, waiting: ${this._queue.length}`),new Promise(e=>{this._queue.push({key:t,resolve:e})})}add(t,e){this._running.has(t)||(this._running.set(t,e),e.finally(()=>{this._running.delete(t),this.debug&&console.debug(`[PromiseQueue]: Promise finished now running: ${this._running.size}, waiting: ${this._queue.length}. (finished ${t})`)}),this.debug&&console.debug(`[PromiseQueue]: Added new promise, now running: ${this._running.size}, waiting: ${this._queue.length}. (added ${t})`))}internalUpdate(){const t=this.maxConcurrent-this._running.size;for(let e=0;e<t&&this._queue.length>0;e++){this.debug&&console.debug(`[PromiseQueue]: Running ${this._running.size} promises, waiting for ${this._queue.length} more.`);const{key:s,resolve:r}=this._queue.shift();r({use:o=>this.add(s,o)})}}}function qe(i){const t=i.image?.width??0,e=i.image?.height??0,s=i.image?.depth??1,r=Math.floor(Math.log2(Math.max(t,e,s)))+1,o=Ne(i);return t*e*s*o*(1-Math.pow(.25,r))/(1-.25)}function Ne(i){let t=4;const e=i.format;e===1024||e===1025?t=1:e===1026||e===1027?t=2:e===1022||e===1029?t=3:(e===1023||e===1033)&&(t=4);let s=1;const r=i.type;return r===1009||r===1010?s=1:r===1011||r===1012?s=2:r===1013||r===1014||r===1015?s=4:r===1016&&(s=2),t*s}const Ee=typeof window>"u"&&typeof document>"u",ye=Symbol("needle:raycast-mesh");function j(i){return i?.[ye]instanceof c.BufferGeometry?i[ye]:null}function Xe(i,t){if((i.type==="Mesh"||i.type==="SkinnedMesh")&&!j(i)){const s=Ke(t);s.userData={isRaycastMesh:!0},i[ye]=s}}function je(i=!0){if(i){if(H)return;const t=H=c.Mesh.prototype.raycast;c.Mesh.prototype.raycast=function(e,s){const r=this,o=j(r);let n;o&&r.isMesh&&(n=r.geometry,r.geometry=o),t.call(this,e,s),n&&(r.geometry=n)}}else{if(!H)return;c.Mesh.prototype.raycast=H,H=null}}let H=null;function Ke(i){const t=new c.BufferGeometry;for(const e in i.attributes)t.setAttribute(e,i.getAttribute(e));return t.setIndex(i.getIndex()),t}const W=new Array,h=z("debugprogressive");let J,q=-1;if(h){let i=function(){q+=1,q>=t&&(q=-1),console.log(`Toggle LOD level [${q}]`)},t=6;window.addEventListener("keyup",e=>{e.key==="p"&&i(),e.key==="w"&&(J=!J,console.log(`Toggle wireframe [${J}]`));const s=parseInt(e.key);!isNaN(s)&&s>=0&&(q=s,console.log(`Set LOD level to [${q}]`))})}function Ce(i){if(h&&J!==void 0)if(Array.isArray(i))for(const t of i)Ce(t);else i&&"wireframe"in i&&(i.wireframe=J===!0)}const Q=new Array;let Ye=0;const He=Le()?2:10;function Qe(i){if(Q.length<He){const s=Q.length;h&&console.warn(`[Worker] Creating new worker #${s}`);const r=_e.createWorker(i||{});return Q.push(r),r}const t=Ye++%Q.length;return Q[t]}class _e{constructor(t,e){this.worker=t,this._debug=e.debug??!1,t.onmessage=s=>{const r=s.data;switch(this._debug&&console.log("[Worker] EVENT",r),r.type){case"loaded-gltf":for(const o of this._running)if(o.url===r.result.url){Je(r.result),o.resolve(r.result);const n=o.url;n.startsWith("blob:")&&URL.revokeObjectURL(n)}}},t.onerror=s=>{console.error("[Worker] Error in gltf-progressive worker:",s)},t.postMessage({type:"init"})}static async createWorker(t){const e=new Worker(new URL("/loader.worker-BqODMeeW.js",typeof document>"u"?require("url").pathToFileURL(__filename).href:le&&le.tagName.toUpperCase()==="SCRIPT"&&le.src||new URL("gltf-progressive-BacJPTD6.umd.cjs",document.baseURI).href),{type:"module"});return new _e(e,t)}_running=[];_webglRenderer=null;async load(t,e){const s=Fe();let r=e?.renderer;r||(this._webglRenderer??=(async()=>{const{WebGLRenderer:u}=await Promise.resolve().then(()=>require("./three.umd.cjs")).then(d=>d.THREE);return new u})(),r=await this._webglRenderer);const l=ne(r).ktx2Loader.workerConfig;t instanceof URL?t=t.toString():t.startsWith("file:")?t=URL.createObjectURL(new Blob([t])):!t.startsWith("blob:")&&!t.startsWith("http:")&&!t.startsWith("https:")&&(t=new URL(t,window.location.href).toString());const a={type:"load",url:t,dracoDecoderPath:s.dracoDecoderPath,ktx2TranscoderPath:s.ktx2TranscoderPath,ktx2LoaderConfig:l};return this._debug&&console.debug("[Worker] Sending load request",a),this.worker.postMessage(a),new Promise(u=>{this._running.push({url:t.toString(),resolve:u})})}_debug=!1}function Je(i){for(const t of i.geometries){const e=t.geometry,s=new c.BufferGeometry;if(s.name=e.name||"",e.index){const r=e.index;s.setIndex(ue(r))}for(const r in e.attributes){const o=e.attributes[r],n=ue(o);s.setAttribute(r,n)}if(e.morphAttributes)for(const r in e.morphAttributes){const n=e.morphAttributes[r].map(l=>ue(l));s.morphAttributes[r]=n}if(s.morphTargetsRelative=e.morphTargetsRelative??!1,s.boundingBox=new c.Box3,s.boundingBox.min=new c.Vector3(e.boundingBox?.min.x,e.boundingBox?.min.y,e.boundingBox?.min.z),s.boundingBox.max=new c.Vector3(e.boundingBox?.max.x,e.boundingBox?.max.y,e.boundingBox?.max.z),s.boundingSphere=new c.Sphere(new c.Vector3(e.boundingSphere?.center.x,e.boundingSphere?.center.y,e.boundingSphere?.center.z),e.boundingSphere?.radius),e.groups)for(const r of e.groups)s.addGroup(r.start,r.count,r.materialIndex);e.userData&&(s.userData=e.userData),t.geometry=s}for(const t of i.textures){const e=t.texture;let s=null;if(e.isCompressedTexture){const r=e.mipmaps,o=e.image?.width||e.source?.data?.width||-1,n=e.image?.height||e.source?.data?.height||-1;s=new c.CompressedTexture(r,o,n,e.format,e.type,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.anisotropy,e.colorSpace)}else s=new c.Texture(e.image,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),s.mipmaps=e.mipmaps,s.channel=e.channel,s.source.data=e.source.data,s.flipY=e.flipY,s.premultiplyAlpha=e.premultiplyAlpha,s.unpackAlignment=e.unpackAlignment,s.matrix=new c.Matrix3(...e.matrix.elements);if(!s){console.error("[Worker] Failed to create new texture from received data. Texture is not a CompressedTexture or Texture.");continue}t.texture=s}return i}function ue(i){let t=i;if("isInterleavedBufferAttribute"in i&&i.isInterleavedBufferAttribute){const e=i.data,s=e.array,r=new c.InterleavedBuffer(s,e.stride);t=new c.InterleavedBufferAttribute(r,i.itemSize,s.byteOffset,i.normalized),t.offset=i.offset}else"isBufferAttribute"in i&&i.isBufferAttribute&&(t=new c.BufferAttribute(i.array,i.itemSize,i.normalized),t.usage=i.usage,t.gpuType=i.gpuType,t.updateRanges=i.updateRanges);return t}const Ze=z("gltf-progressive-worker");z("gltf-progressive-reduce-mipmaps");const E=z("gltf-progressive-gc"),ce=Symbol("needle-progressive-texture"),F="NEEDLE_progressive";class m{get name(){return F}static getMeshLODExtension(t){const e=this.getAssignedLODInformation(t);return e?.key?this.lodInfos.get(e.key):null}static getPrimitiveIndex(t){const e=this.getAssignedLODInformation(t)?.index;return e??-1}static getMaterialMinMaxLODsCount(t,e){const s=this,r="LODS:minmax",o=t[r];if(o!=null)return o;if(e||(e={min_count:1/0,max_count:0,lods:[]}),Array.isArray(t)){for(const l of t)this.getMaterialMinMaxLODsCount(l,e);return t[r]=e,e}if(h==="verbose"&&console.log("getMaterialMinMaxLODsCount",t),t.type==="ShaderMaterial"||t.type==="RawShaderMaterial"){const l=t;for(const a of Object.keys(l.uniforms)){const u=l.uniforms[a].value;u?.isTexture===!0&&n(u,e)}}else if(t.isMaterial)for(const l of Object.keys(t)){const a=t[l];a?.isTexture===!0&&n(a,e)}else h&&console.warn(`[getMaterialMinMaxLODsCount] Unsupported material type: ${t.type}`);return t[r]=e,e;function n(l,a){const u=s.getAssignedLODInformation(l);if(u){const d=s.lodInfos.get(u.key);if(d&&d.lods){a.min_count=Math.min(a.min_count,d.lods.length),a.max_count=Math.max(a.max_count,d.lods.length);for(let x=0;x<d.lods.length;x++){const L=d.lods[x];L.width&&(a.lods[x]=a.lods[x]||{min_height:1/0,max_height:0},a.lods[x].min_height=Math.min(a.lods[x].min_height,L.height),a.lods[x].max_height=Math.max(a.lods[x].max_height,L.height))}}}}}static hasLODLevelAvailable(t,e){if(Array.isArray(t)){for(const o of t)if(this.hasLODLevelAvailable(o,e))return!0;return!1}if(t.isMaterial===!0){for(const o of Object.keys(t)){const n=t[o];if(n&&n.isTexture&&this.hasLODLevelAvailable(n,e))return!0}return!1}else if(t.isGroup===!0){for(const o of t.children)if(o.isMesh===!0&&this.hasLODLevelAvailable(o,e))return!0}let s,r;if(t.isMesh?s=t.geometry:(t.isBufferGeometry||t.isTexture)&&(s=t),s&&s?.userData?.LODS){const o=s.userData.LODS;if(r=this.lodInfos.get(o.key),e===void 0)return r!=null;if(r)return Array.isArray(r.lods)?e<r.lods.length:e===0}return!1}static assignMeshLOD(t,e){if(!t)return Promise.resolve(null);if(t instanceof c.Mesh||t.isMesh===!0){const s=t.geometry,r=this.getAssignedLODInformation(s);if(!r)return Promise.resolve(null);for(const o of W)o.onBeforeGetLODMesh?.(t,e);return t["LOD:requested level"]=e,m.getOrLoadLOD(s,e).then(o=>{if(Array.isArray(o)){const n=r.index||0;o=o[n]}return t["LOD:requested level"]===e&&(delete t["LOD:requested level"],o&&s!=o&&(o?.isBufferGeometry?t.geometry=o:h&&console.error("Invalid LOD geometry",o))),o}).catch(o=>(console.error("Error loading mesh LOD",t,o),null))}else h&&console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh",t);return Promise.resolve(null)}static assignTextureLOD(t,e=0){if(!t)return Promise.resolve(null);if(t.isMesh===!0){const s=t;if(Array.isArray(s.material)){const r=new Array;for(const o of s.material){const n=this.assignTextureLOD(o,e);r.push(n)}return Promise.all(r).then(o=>{const n=new Array;for(const l of o)Array.isArray(l)&&n.push(...l);return n})}else return this.assignTextureLOD(s.material,e)}if(t.isMaterial===!0){const s=t,r=[],o=new Array;if(s.uniforms&&(s.isRawShaderMaterial||s.isShaderMaterial===!0)){const n=s;for(const l of Object.keys(n.uniforms)){const a=n.uniforms[l].value;if(a?.isTexture===!0){const u=this.assignTextureLODForSlot(a,e,s,l).then(d=>(d&&n.uniforms[l].value!=d&&(n.uniforms[l].value=d,n.uniformsNeedUpdate=!0),d));r.push(u),o.push(l)}}}else for(const n of Object.keys(s)){const l=s[n];if(l?.isTexture===!0){const a=this.assignTextureLODForSlot(l,e,s,n);r.push(a),o.push(n)}}return Promise.all(r).then(n=>{const l=new Array;for(let a=0;a<n.length;a++){const u=n[a],d=o[a];u&&u.isTexture===!0?l.push({material:s,slot:d,texture:u,level:e}):l.push({material:s,slot:d,texture:null,level:e})}return l})}if(t instanceof c.Texture||t.isTexture===!0){const s=t;return this.assignTextureLODForSlot(s,e,null,null)}return Promise.resolve(null)}static set maxConcurrentLoadingTasks(t){m.queue.maxConcurrent=t}static get maxConcurrentLoadingTasks(){return m.queue.maxConcurrent}static assignTextureLODForSlot(t,e,s,r){return t?.isTexture!==!0?Promise.resolve(null):r==="glyphMap"?Promise.resolve(t):m.getOrLoadLOD(t,e).then(o=>{if(Array.isArray(o))return console.warn("Progressive: Got an array of textures for a texture slot, this should not happen..."),null;if(o?.isTexture===!0){if(o!=t&&s&&r){const n=s[r];if(n&&!h){const l=this.getAssignedLODInformation(n);if(l&&l?.level<e)return h==="verbose"&&console.warn("Assigned texture level is already higher: ",l.level,e,s,n,o),o&&o!==n&&((h||E)&&console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${e} (assigned is ${l.level})`,o.uuid),o.dispose()),null}if(this.trackTextureUsage(o),n&&n!==o&&this.untrackTextureUsage(n)&&(h||E)){const a=this.getAssignedLODInformation(n);console.log(`[gltf-progressive] Disposed old texture LOD ${a?.level??"?"} → ${e} for ${s.name||s.type}.${r}`,n.uuid)}s[r]=o}return o}else h=="verbose"&&console.warn("No LOD found for",t,e);return null}).catch(o=>(console.error("Error loading LOD",t,o),null))}parser;url;constructor(t){const e=t.options.path;h&&console.log("Progressive extension registered for",e),this.parser=t,this.url=e}_isLoadingMesh;loadMesh=t=>{if(this._isLoadingMesh)return null;const e=this.parser.json.meshes[t]?.extensions?.[F];return e?(this._isLoadingMesh=!0,this.parser.getDependency("mesh",t).then(s=>(this._isLoadingMesh=!1,s&&m.registerMesh(this.url,e.guid,s,e.lods?.length,0,e),s))):null};afterRoot(t){return h&&console.log("AFTER",this.url,t),this.parser.json.textures?.forEach((e,s)=>{if(e?.extensions){const r=e?.extensions[F];if(r){if(!r.lods){h&&console.warn("Texture has no LODs",r);return}let o=!1;for(const n of this.parser.associations.keys())n.isTexture===!0&&this.parser.associations.get(n)?.textures===s&&(o=!0,m.registerTexture(this.url,n,r.lods?.length,s,r));o||this.parser.getDependency("texture",s).then(n=>{n&&m.registerTexture(this.url,n,r.lods?.length,s,r)})}}}),this.parser.json.meshes?.forEach((e,s)=>{if(e?.extensions){const r=e?.extensions[F];if(r&&r.lods){for(const o of this.parser.associations.keys())if(o.isMesh){const n=this.parser.associations.get(o);n?.meshes===s&&m.registerMesh(this.url,r.guid,o,r.lods.length,n.primitives,r)}}}}),null}static registerTexture=(t,e,s,r,o)=>{if(!e){h&&console.error("!! gltf-progressive: Called register texture without texture");return}if(h){const l=e.image?.width||e.source?.data?.width||0,a=e.image?.height||e.source?.data?.height||0;console.log(`> gltf-progressive: register texture[${r}] "${e.name||e.uuid}", Current: ${l}x${a}, Max: ${o.lods[0]?.width}x${o.lods[0]?.height}, uuid: ${e.uuid}`,o,e)}e.source&&(e.source[ce]=o);const n=o.guid;m.assignLODInformation(t,e,n,s,r),m.lodInfos.set(n,o),m.lowresCache.set(n,new WeakRef(e))};static registerMesh=(t,e,s,r,o,n)=>{const l=s.geometry;if(!l){h&&console.warn("gltf-progressive: Register mesh without geometry");return}l.userData||(l.userData={}),h&&console.log("> Progressive: register mesh "+s.name,{index:o,uuid:s.uuid},n,s),m.assignLODInformation(t,l,e,r,o),m.lodInfos.set(e,n);let u=m.lowresCache.get(e)?.deref();u?u.push(s.geometry):u=[s.geometry],m.lowresCache.set(e,new WeakRef(u)),r>0&&!j(s)&&Xe(s,l);for(const d of W)d.onRegisteredNewMesh?.(s,n)};static dispose(t){if(t){this.lodInfos.delete(t);const e=this.lowresCache.get(t);if(e){const s=e.deref();if(s){if(s.isTexture){const r=s;this.textureRefCounts.delete(r.uuid),r.dispose()}else if(Array.isArray(s))for(const r of s)r.dispose()}this.lowresCache.delete(t)}for(const[s,r]of this.cache)s.includes(t)&&(this._disposeCacheEntry(r),this.cache.delete(s))}else{this.lodInfos.clear();for(const[,e]of this.lowresCache){const s=e.deref();if(s){if(s.isTexture){const r=s;this.textureRefCounts.delete(r.uuid),r.dispose()}else if(Array.isArray(s))for(const r of s)r.dispose()}}this.lowresCache.clear();for(const[,e]of this.cache)this._disposeCacheEntry(e);this.cache.clear(),this.textureRefCounts.clear()}}static _disposeCacheEntry(t){if(t instanceof WeakRef){const e=t.deref();e&&(e.isTexture&&this.textureRefCounts.delete(e.uuid),e.dispose())}else t.then(e=>{if(e)if(Array.isArray(e))for(const s of e)s.dispose();else e.isTexture&&this.textureRefCounts.delete(e.uuid),e.dispose()}).catch(()=>{})}static lodInfos=new Map;static cache=new Map;static lowresCache=new Map;static textureRefCounts=new Map;static _resourceRegistry=new FinalizationRegistry(t=>{const e=m.cache.get(t);(h||E)&&console.debug(`[gltf-progressive] Memory: Resource GC'd
2
- ${t}`),e instanceof WeakRef&&(e.deref()||(m.cache.delete(t),(h||E)&&console.log("[gltf-progressive] ↪ Cache entry deleted (GC)")))});static trackTextureUsage(t){const e=t.uuid,s=this.textureRefCounts.get(e)||0;this.textureRefCounts.set(e,s+1),h==="verbose"&&console.log(`[gltf-progressive] Track texture ${e}, refCount: ${s} → ${s+1}`)}static untrackTextureUsage(t){const e=t.uuid,s=this.textureRefCounts.get(e);if(!s)return(h==="verbose"||E)&&o("[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)",0),t.dispose(),!0;const r=s-1;if(r<=0)return this.textureRefCounts.delete(e),(h||E)&&o("[gltf-progressive] Memory: Dispose texture",r),t.dispose(),!0;return this.textureRefCounts.set(e,r),h==="verbose"&&o("[gltf-progressive] Memory: Untrack texture",r),!1;function o(n,l){let a=t.image?.width||t.source?.data?.width||0,u=t.image?.height||t.source?.data?.height||0;const d=a&&u?`${a}x${u}`:"N/A";let x="N/A";a&&u&&(x=`~${(qe(t)/(1024*1024)).toFixed(2)} MB`),console.log(`${n} — ${t.name} ${d} (${x}), refCount: ${s} → ${l}
3
- ${e}`)}}static workers=[];static _workersIndex=0;static async getOrLoadLOD(t,e){const s=h=="verbose",r=this.getAssignedLODInformation(t);if(!r)return h&&console.warn(`[gltf-progressive] No LOD information found: ${t.name}, uuid: ${t.uuid}, type: ${t.type}`,t),null;const o=r?.key;let n;if(t.isTexture===!0){const a=t;a.source&&a.source[ce]&&(n=a.source[ce])}if(n||(n=m.lodInfos.get(o)),!n)h&&console.warn(`Can not load LOD ${e}: no LOD info found for "${o}" ${t.name}`,t.type,m.lodInfos);else{if(e>0){let d=!1;const x=Array.isArray(n.lods);if(x&&e>=n.lods.length?d=!0:x||(d=!0),d){const L=this.lowresCache.get(o);if(L){const _=L.deref();if(_)return _;this.lowresCache.delete(o),h&&console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${o}`)}return null}}const a=Array.isArray(n.lods)?n.lods[e]?.path:n.lods;if(!a)return h&&!n["missing:uri"]&&(n["missing:uri"]=!0,console.warn("Missing uri for progressive asset for LOD "+e,n)),null;const u=ze(r.url,a);if(u.endsWith(".glb")||u.endsWith(".gltf")){if(!n.guid)return console.warn("missing pointer for glb/gltf texture",n),null;const d=u+"_"+n.guid,x=await this.queue.slot(u),L=this.cache.get(d);if(L!==void 0)if(s&&console.log(`LOD ${e} was already loading/loaded: ${d}`),L instanceof WeakRef){const f=L.deref();if(f){let p=f,v=!1;if(p instanceof c.Texture&&t instanceof c.Texture?p.image?.data||p.source?.data?p=this.copySettings(t,p):v=!0:p instanceof c.BufferGeometry&&t instanceof c.BufferGeometry&&(p.attributes.position?.array||(v=!0)),!v)return p}this.cache.delete(d),h&&console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${d}`)}else{let f=await L.catch(v=>(console.error(`Error loading LOD ${e} from ${u}
4
- `,v),null)),p=!1;if(f==null||(f instanceof c.Texture&&t instanceof c.Texture?f.image?.data||f.source?.data?f=this.copySettings(t,f):(p=!0,this.cache.delete(d)):f instanceof c.BufferGeometry&&t instanceof c.BufferGeometry&&(f.attributes.position?.array||(p=!0,this.cache.delete(d)))),!p)return f}if(!x.use)return h&&console.log(`LOD ${e} was aborted: ${u}`),null;const _=n,k=new Promise(async(f,p)=>{if(Ze){const w=await(await Qe({})).load(u);if(w.textures.length>0)for(const g of w.textures){let y=g.texture;return m.assignLODInformation(r.url,y,o,e,void 0),t instanceof c.Texture&&(y=this.copySettings(t,y)),y&&(y.guid=_.guid),f(y)}if(w.geometries.length>0){const g=new Array;for(const y of w.geometries){const O=y.geometry;m.assignLODInformation(r.url,O,o,e,y.primitiveIndex),g.push(O)}return f(g)}return f(null)}const v=new X.GLTFLoader;xe(v),h&&(await new Promise(M=>setTimeout(M,1e3)),s&&console.warn("Start loading (delayed) "+u,_.guid));let B=u;if(_&&Array.isArray(_.lods)){const M=_.lods[e];M.hash&&(B+="?v="+M.hash)}const b=await v.loadAsync(B).catch(M=>(console.error(`Error loading LOD ${e} from ${u}
5
- `,M),f(null)));if(!b)return f(null);const V=b.parser;s&&console.log("Loading finished "+u,_.guid);let P=0;if(b.parser.json.textures){let M=!1;for(const w of b.parser.json.textures){if(w?.extensions){const g=w?.extensions[F];if(g?.guid&&g.guid===_.guid){M=!0;break}}P++}if(M){let w=await V.getDependency("texture",P);return w&&m.assignLODInformation(r.url,w,o,e,void 0),s&&console.log('change "'+t.name+'" → "'+w.name+'"',u,P,w,d),t instanceof c.Texture&&(w=this.copySettings(t,w)),w&&(w.guid=_.guid),f(w)}else h&&console.warn("Could not find texture with guid",_.guid,b.parser.json)}if(P=0,b.parser.json.meshes){let M=!1;for(const w of b.parser.json.meshes){if(w?.extensions){const g=w?.extensions[F];if(g?.guid&&g.guid===_.guid){M=!0;break}}P++}if(M){const w=await V.getDependency("mesh",P);if(s&&console.log(`Loaded Mesh "${w.name}"`,u,P,w,d),w.isMesh===!0){const g=w.geometry;return m.assignLODInformation(r.url,g,o,e,0),f(g)}else{const g=new Array;for(let y=0;y<w.children.length;y++){const O=w.children[y];if(O.isMesh===!0){const S=O.geometry;m.assignLODInformation(r.url,S,o,e,y),g.push(S)}}return f(g)}}else h&&console.warn("Could not find mesh with guid",_.guid,b.parser.json)}return f(null)});this.cache.set(d,k),x.use(k);const D=await k;return D!=null?D instanceof c.Texture?(this.cache.set(d,new WeakRef(D)),m._resourceRegistry.register(D,d)):Array.isArray(D)?this.cache.set(d,Promise.resolve(D)):this.cache.set(d,Promise.resolve(D)):this.cache.set(d,Promise.resolve(null)),D}else if(t instanceof c.Texture){s&&console.log("Load texture from uri: "+u);const x=await new c.TextureLoader().loadAsync(u);return x?(x.guid=n.guid,x.flipY=!1,x.needsUpdate=!0,x.colorSpace=t.colorSpace,s&&console.log(n,x)):h&&console.warn("failed loading",u),x}}return null}static queue=new Ve(Le()?20:50,{debug:h!=!1});static assignLODInformation(t,e,s,r,o){if(!e)return;e.userData||(e.userData={});const n=new et(t,s,r,o);e.userData.LODS=n,"source"in e&&typeof e.source=="object"&&(e.source.LODS=n)}static getAssignedLODInformation(t){return t?t.userData?.LODS?t.userData.LODS:"source"in t&&t.source?.LODS?t.source.LODS:null:null}static copySettings(t,e){return e?(h==="verbose"&&console.debug(`Copy texture settings
6
- `,t.uuid,`
7
- `,e.uuid),e=e.clone(),e.offset=t.offset,e.repeat=t.repeat,e.colorSpace=t.colorSpace,e.magFilter=t.magFilter,e.minFilter=t.minFilter,e.wrapS=t.wrapS,e.wrapT=t.wrapT,e.flipY=t.flipY,e.anisotropy=t.anisotropy,e.mipmaps||(e.generateMipmaps=t.generateMipmaps),e):t}}class et{url;key;level;index;constructor(t,e,s,r){this.url=t,this.key=e,this.level=s,r!=null&&(this.index=r)}}class de{static addPromise=(t,e,s,r)=>{r.forEach(o=>{o.add(t,e,s)})};ready;get awaitedCount(){return this._addedCount}get resolvedCount(){return this._resolvedCount}get currentlyAwaiting(){return this._awaiting.length}_resolve;_signal;_frame_start;_frames_to_capture;_resolved=!1;_addedCount=0;_resolvedCount=0;_awaiting=[];_maxPromisesPerObject=1;constructor(t,e){const r=Math.max(e.frames??2,2);this._frame_start=e.waitForFirstCapture?void 0:t,this._frames_to_capture=r,this.ready=new Promise(o=>{this._resolve=o}),this.ready.finally(()=>{this._resolved=!0,this._awaiting.length=0}),this._signal=e.signal,this._signal?.addEventListener("abort",()=>{this.resolveNow()}),this._maxPromisesPerObject=Math.max(1,e.maxPromisesPerObject??1)}_currentFrame=0;update(t){this._currentFrame=t,this._frame_start===void 0&&this._addedCount>0&&(this._frame_start=t),(this._signal?.aborted||this._awaiting.length===0&&this._frame_start!==void 0&&t>this._frame_start+this._frames_to_capture)&&this.resolveNow()}_seen=new WeakMap;add(t,e,s){if(this._resolved){h&&console.warn("PromiseGroup: Trying to add a promise to a resolved group, ignoring.");return}if(!(this._frame_start!==void 0&&this._currentFrame>this._frame_start+this._frames_to_capture)){if(this._maxPromisesPerObject>=1)if(this._seen.has(e)){let r=this._seen.get(e);if(r>=this._maxPromisesPerObject){h&&console.warn("PromiseGroup: Already awaiting object ignoring new promise for it.");return}this._seen.set(e,r+1)}else this._seen.set(e,1);this._awaiting.push(s),this._addedCount++,s.finally(()=>{this._resolvedCount++,this._awaiting.splice(this._awaiting.indexOf(s),1)})}}resolveNow(){this._resolved||this._resolve?.({awaited_count:this._addedCount,resolved_count:this._resolvedCount,cancelled:this._signal?.aborted??!1})}}const R=z("debugprogressive"),tt=z("noprogressive"),fe=Symbol("Needle:LODSManager"),he=Symbol("Needle:LODState"),U=Symbol("Needle:CurrentLOD"),C={mesh_lod:-1,texture_lod:-1};let Z=class T{static debugDrawLine;static getObjectLODState(t){return t[he]}static addPlugin(t){W.push(t)}static removePlugin(t){const e=W.indexOf(t);e>=0&&W.splice(e,1)}static get(t,e){if(t[fe])return console.debug("[gltf-progressive] LODsManager already exists for this renderer"),t[fe];const s=new T(t,{engine:"unknown",...e});return t[fe]=s,s}renderer;context;projectionScreenMatrix=new c.Matrix4;get plugins(){return W}overrideLodLevel=void 0;targetTriangleDensity=2e5;skinnedMeshAutoUpdateBoundsInterval=30;updateInterval="auto";#e=1;pause=!1;manual=!1;_newPromiseGroups=[];_promiseGroupIds=0;awaitLoading(t){const e=this._promiseGroupIds++,s=new de(this.#r,{...t});this._newPromiseGroups.push(s);const r=performance.now();return s.ready.finally(()=>{const o=this._newPromiseGroups.indexOf(s);o>=0&&(this._newPromiseGroups.splice(o,1),Me()&&performance.measure("LODsManager:awaitLoading",{start:r,detail:{id:e,name:t?.name,awaited:s.awaitedCount,resolved:s.resolvedCount}}))}),s.ready}_postprocessPromiseGroups(){if(this._newPromiseGroups.length!==0)for(let t=this._newPromiseGroups.length-1;t>=0;t--)this._newPromiseGroups[t].update(this.#r)}_lodchangedlisteners=[];addEventListener(t,e){t==="changed"&&this._lodchangedlisteners.push(e)}removeEventListener(t,e){if(t==="changed"){const s=this._lodchangedlisteners.indexOf(e);s>=0&&this._lodchangedlisteners.splice(s,1)}}constructor(t,e){this.renderer=t,this.context={...e}}#t;#n=new c.Clock;#r=0;#o=0;#i=0;#s=0;_fpsBuffer=[60,60,60,60,60];enable(){if(this.#t)return;console.debug("[gltf-progressive] Enabling LODsManager for renderer");let t=0;this.#t=this.renderer.render;const e=this;ne(this.renderer),this.renderer.render=function(s,r){const o=e.renderer.getRenderTarget();(o==null||"isXRRenderTarget"in o&&o.isXRRenderTarget)&&(t=0,e.#r+=1,e.#o=e.#n.getDelta(),e.#i+=e.#o,e._fpsBuffer.shift(),e._fpsBuffer.push(1/e.#o),e.#s=e._fpsBuffer.reduce((l,a)=>l+a)/e._fpsBuffer.length,R&&e.#r%200===0&&console.log("FPS",Math.round(e.#s),"Interval:",e.#e));const n=t++;e.#t.call(this,s,r),e.onAfterRender(s,r,n)}}disable(){this.#t&&(console.debug("[gltf-progressive] Disabling LODsManager for renderer"),this.renderer.render=this.#t,this.#t=void 0)}update(t,e){this.internalUpdate(t,e)}onAfterRender(t,e,s){if(this.pause)return;const o=this.renderer.renderLists.get(t,0).opaque;let n=!0;if(o.length===1){const l=o[0].material;(l.name==="EffectMaterial"||l.name==="CopyShader")&&(n=!1)}if((e.parent&&e.parent.type==="CubeCamera"||s>=1&&e.type==="OrthographicCamera")&&(n=!1),n){if(tt||(this.updateInterval==="auto"?this.#s<40&&this.#e<10?(this.#e+=1,R&&console.warn("↓ Reducing LOD updates",this.#e,this.#s.toFixed(0))):this.#s>=60&&this.#e>1&&(this.#e-=1,R&&console.warn("↑ Increasing LOD updates",this.#e,this.#s.toFixed(0))):this.#e=this.updateInterval,this.#e>0&&this.#r%this.#e!=0))return;this.internalUpdate(t,e),this._postprocessPromiseGroups()}}internalUpdate(t,e){const s=this.renderer.renderLists.get(t,0),r=s.opaque;this.projectionScreenMatrix.multiplyMatrices(e.projectionMatrix,e.matrixWorldInverse);const o=this.targetTriangleDensity;for(const a of r){if(a.material&&(a.geometry?.type==="BoxGeometry"||a.geometry?.type==="BufferGeometry")&&(a.material.name==="SphericalGaussianBlur"||a.material.name=="BackgroundCubeMaterial"||a.material.name==="CubemapFromEquirect"||a.material.name==="EquirectangularToCubeUV")){R&&(a.material["NEEDLE_PROGRESSIVE:IGNORE-WARNING"]||(a.material["NEEDLE_PROGRESSIVE:IGNORE-WARNING"]=!0,console.warn("Ignoring skybox or BLIT object",a,a.material.name,a.material.type)));continue}switch(a.material.type){case"LineBasicMaterial":case"LineDashedMaterial":case"PointsMaterial":case"ShadowMaterial":case"MeshDistanceMaterial":case"MeshDepthMaterial":continue}if(R==="color"&&a.material&&!a.object.progressive_debug_color){a.object.progressive_debug_color=!0;const d=Math.random()*16777215,x=new c.MeshStandardMaterial({color:d});a.object.material=x}const u=a.object;(u instanceof c.Mesh||u.isMesh)&&this.updateLODs(t,e,u,o)}const n=s.transparent;for(const a of n){const u=a.object;(u instanceof c.Mesh||u.isMesh)&&this.updateLODs(t,e,u,o)}const l=s.transmissive;for(const a of l){const u=a.object;(u instanceof c.Mesh||u.isMesh)&&this.updateLODs(t,e,u,o)}}updateLODs(t,e,s,r){s.userData||(s.userData={});let o=s[he];if(o||(o=new st,s[he]=o),o.frames++<2)return;for(const l of W)l.onBeforeUpdateLOD?.(this.renderer,t,e,s);const n=this.overrideLodLevel!==void 0?this.overrideLodLevel:q;n>=0?(C.mesh_lod=n,C.texture_lod=n):(this.calculateLodLevel(e,s,o,r,C),C.mesh_lod=Math.round(C.mesh_lod),C.texture_lod=Math.round(C.texture_lod)),C.mesh_lod>=0&&this.loadProgressiveMeshes(s,C.mesh_lod),s.material&&C.texture_lod>=0&&this.loadProgressiveTextures(s.material,C.texture_lod,n),h&&s.material&&!s.isGizmo&&Ce(s.material);for(const l of W)l.onAfterUpdatedLOD?.(this.renderer,t,e,s,C);o.lastLodLevel_Mesh=C.mesh_lod,o.lastLodLevel_Texture=C.texture_lod}loadProgressiveTextures(t,e,s){if(!t)return;if(Array.isArray(t)){for(const o of t)this.loadProgressiveTextures(o,e);return}let r=!1;if((t[U]===void 0||e<t[U])&&(r=!0),s!==void 0&&s>=0&&(r=t[U]!=s,e=s),r){t[U]=e;const o=m.assignTextureLOD(t,e).then(n=>{this._lodchangedlisteners.forEach(l=>l({type:"texture",level:e,object:t}))});de.addPromise("texture",t,o,this._newPromiseGroups)}}loadProgressiveMeshes(t,e){if(!t)return Promise.resolve(null);let s=t[U]!==e;const r=t["DEBUG:LOD"];if(r!=null&&(s=t[U]!=r,e=r),s){t[U]=e;const o=t.geometry,n=m.assignMeshLOD(t,e).then(l=>(l&&t[U]==e&&o!=t.geometry&&this._lodchangedlisteners.forEach(a=>a({type:"mesh",level:e,object:t})),l));return de.addPromise("mesh",t,n,this._newPromiseGroups),n}return Promise.resolve(null)}_sphere=new c.Sphere;_tempBox=new c.Box3;_tempBox2=new c.Box3;tempMatrix=new c.Matrix4;_tempWorldPosition=new c.Vector3;_tempBoxSize=new c.Vector3;_tempBox2Size=new c.Vector3;static corner0=new c.Vector3;static corner1=new c.Vector3;static corner2=new c.Vector3;static corner3=new c.Vector3;static _tempPtInside=new c.Vector3;static isInside(t,e){const s=t.min,r=t.max,o=(s.x+r.x)*.5,n=(s.y+r.y)*.5;return this._tempPtInside.set(o,n,s.z).applyMatrix4(e).z<0}static skinnedMeshBoundsFrameOffsetCounter=0;static $skinnedMeshBoundsOffset=Symbol("gltf-progressive-skinnedMeshBoundsOffset");calculateLodLevel(t,e,s,r,o){if(!e){o.mesh_lod=-1,o.texture_lod=-1;return}if(!t){o.mesh_lod=-1,o.texture_lod=-1;return}let l=10+1,a=!1;if(R&&e["DEBUG:LOD"]!=null)return e["DEBUG:LOD"];const u=m.getMeshLODExtension(e.geometry)?.lods,d=m.getPrimitiveIndex(e.geometry),x=u&&u.length>0,L=m.getMaterialMinMaxLODsCount(e.material),_=L.min_count!==1/0&&L.min_count>=0&&L.max_count>=0;if(!x&&!_){o.mesh_lod=0,o.texture_lod=0;return}x||(a=!0,l=0);const k=this.renderer.domElement.clientHeight||this.renderer.domElement.height;let D=e.geometry.boundingBox;if(e.type==="SkinnedMesh"){const f=e;if(!f.boundingBox)f.computeBoundingBox();else if(this.skinnedMeshAutoUpdateBoundsInterval>0){if(!f[T.$skinnedMeshBoundsOffset]){const v=T.skinnedMeshBoundsFrameOffsetCounter++;f[T.$skinnedMeshBoundsOffset]=v}const p=f[T.$skinnedMeshBoundsOffset];if((s.frames+p)%this.skinnedMeshAutoUpdateBoundsInterval===0){const v=j(f),B=f.geometry;v&&(f.geometry=v),f.computeBoundingBox(),f.geometry=B}}D=f.boundingBox}if(D){const f=t;if(e.geometry.attributes.color&&e.geometry.attributes.color.count<100&&e.geometry.boundingSphere){this._sphere.copy(e.geometry.boundingSphere),this._sphere.applyMatrix4(e.matrixWorld);const g=t.getWorldPosition(this._tempWorldPosition);if(this._sphere.containsPoint(g)){o.mesh_lod=0,o.texture_lod=0;return}}if(this._tempBox.copy(D),this._tempBox.applyMatrix4(e.matrixWorld),f.isPerspectiveCamera&&T.isInside(this._tempBox,this.projectionScreenMatrix)){o.mesh_lod=0,o.texture_lod=0;return}if(this._tempBox.applyMatrix4(this.projectionScreenMatrix),this.renderer.xr.enabled&&f.isPerspectiveCamera&&f.fov>70){const g=this._tempBox.min,y=this._tempBox.max;let O=g.x,S=g.y,G=y.x,K=y.y;const ee=2,ie=1.5,te=(g.x+y.x)*.5,se=(g.y+y.y)*.5;O=(O-te)*ee+te,S=(S-se)*ee+se,G=(G-te)*ee+te,K=(K-se)*ee+se;const Re=O<0&&G>0?0:Math.min(Math.abs(g.x),Math.abs(y.x)),Ie=S<0&&K>0?0:Math.min(Math.abs(g.y),Math.abs(y.y)),ae=Math.max(Re,Ie);s.lastCentrality=(ie-ae)*(ie-ae)*(ie-ae)}else s.lastCentrality=1;const p=this._tempBox.getSize(this._tempBoxSize);p.multiplyScalar(.5),screen.availHeight>0&&k>0&&p.multiplyScalar(k/screen.availHeight),t.isPerspectiveCamera?p.x*=t.aspect:t.isOrthographicCamera;const v=t.matrixWorldInverse,B=this._tempBox2;B.copy(D),B.applyMatrix4(e.matrixWorld),B.applyMatrix4(v);const b=B.getSize(this._tempBox2Size),V=Math.max(b.x,b.y);if(Math.max(p.x,p.y)!=0&&V!=0&&(p.z=b.z/Math.max(b.x,b.y)*Math.max(p.x,p.y)),s.lastScreenCoverage=Math.max(p.x,p.y,p.z),s.lastScreenspaceVolume.copy(p),s.lastScreenCoverage*=s.lastCentrality,R&&T.debugDrawLine){const g=this.tempMatrix.copy(this.projectionScreenMatrix);g.invert();const y=T.corner0,O=T.corner1,S=T.corner2,G=T.corner3;y.copy(this._tempBox.min),O.copy(this._tempBox.max),O.x=y.x,S.copy(this._tempBox.max),S.y=y.y,G.copy(this._tempBox.max);const K=(y.z+G.z)*.5;y.z=O.z=S.z=G.z=K,y.applyMatrix4(g),O.applyMatrix4(g),S.applyMatrix4(g),G.applyMatrix4(g),T.debugDrawLine(y,O,255),T.debugDrawLine(y,S,255),T.debugDrawLine(O,G,255),T.debugDrawLine(S,G,255)}let M=999;if(u&&s.lastScreenCoverage>0)for(let g=0;g<u.length;g++){const y=u[g],S=(y.densities?.[d]||y.density||1e-5)/s.lastScreenCoverage;if(d>0&&Me()&&!y.densities&&!globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]&&(window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]=!0,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.")),S<r){M=g;break}}M<l&&(l=M,a=!0)}if(a?o.mesh_lod=l:o.mesh_lod=s.lastLodLevel_Mesh,R&&o.mesh_lod!=s.lastLodLevel_Mesh){const p=u?.[o.mesh_lod];p&&console.log(`Mesh LOD changed: ${s.lastLodLevel_Mesh} → ${o.mesh_lod} (density: ${p.densities?.[d].toFixed(0)}) | ${e.name}`)}if(_){const f="saveData"in globalThis.navigator&&globalThis.navigator.saveData===!0;if(s.lastLodLevel_Texture<0){if(o.texture_lod=L.max_count-1,R){const p=L.lods[L.max_count-1];R&&console.log(`First Texture LOD ${o.texture_lod} (${p.max_height}px) - ${e.name}`)}}else{const p=s.lastScreenspaceVolume.x+s.lastScreenspaceVolume.y+s.lastScreenspaceVolume.z;let v=s.lastScreenCoverage*4;this.context?.engine==="model-viewer"&&(v*=1.5);const b=k/window.devicePixelRatio*v;let V=!1;for(let P=L.lods.length-1;P>=0;P--){const M=L.lods[P];if(!(f&&M.max_height>=2048)&&!(Le()&&M.max_height>4096)&&(M.max_height>b||!V&&P===0)){if(V=!0,o.texture_lod=P,R&&o.texture_lod<s.lastLodLevel_Texture){const w=M.max_height;console.log(`Texture LOD changed: ${s.lastLodLevel_Texture} → ${o.texture_lod} = ${w}px
8
- Screensize: ${b.toFixed(0)}px, Coverage: ${(100*s.lastScreenCoverage).toFixed(2)}%, Volume ${p.toFixed(1)}
9
- ${e.name}`)}break}}}}else o.texture_lod=0}};class st{frames=0;lastLodLevel_Mesh=-1;lastLodLevel_Texture=-1;lastScreenCoverage=0;lastScreenspaceVolume=new c.Vector3;lastCentrality=0}const ve=Symbol("NEEDLE_mesh_lod"),re=Symbol("NEEDLE_texture_lod");let ge=null;function Ae(){const i=rt();i&&(i.mapURLs(function(t){return De(),t}),De(),ge?.disconnect(),ge=new MutationObserver(t=>{t.forEach(e=>{e.addedNodes.forEach(s=>{s instanceof HTMLElement&&s.tagName.toLowerCase()==="model-viewer"&&ke(s)})})}),ge.observe(document,{childList:!0,subtree:!0}))}function rt(){if(typeof customElements>"u")return null;const i=customElements.get("model-viewer");return i||(customElements.whenDefined("model-viewer").then(()=>{console.debug("[gltf-progressive] model-viewer defined"),Ae()}),null)}function De(){if(typeof document>"u")return;document.querySelectorAll("model-viewer").forEach(t=>{ke(t)})}const be=new WeakSet;let ot=0;function ke(i){if(!i||be.has(i))return null;be.add(i),console.debug("[gltf-progressive] found new model-viewer..."+ ++ot+`
10
- `,i.getAttribute("src"));let t=null,e=null,s=null;for(let r=i;r!=null;r=Object.getPrototypeOf(r)){const o=Object.getOwnPropertySymbols(r),n=o.find(u=>u.toString()=="Symbol(renderer)"),l=o.find(u=>u.toString()=="Symbol(scene)"),a=o.find(u=>u.toString()=="Symbol(needsRender)");!t&&n!=null&&(t=i[n].threeRenderer),!e&&l!=null&&(e=i[l]),!s&&a!=null&&(s=i[a])}if(t&&e){let r=function(){if(s){let n=0,l=setInterval(()=>{if(n++>5){clearInterval(l);return}s?.call(i)},300)}};console.debug("[gltf-progressive] setup model-viewer");const o=Z.get(t,{engine:"model-viewer"});return Z.addPlugin(new nt),o.enable(),o.addEventListener("changed",()=>{s?.call(i)}),i.addEventListener("model-visibility",n=>{n.detail.visible&&s?.call(i)}),i.addEventListener("load",()=>{r()}),()=>{o.disable()}}return null}class nt{_didWarnAboutMissingUrl=!1;onBeforeUpdateLOD(t,e,s,r){this.tryParseMeshLOD(e,r),this.tryParseTextureLOD(e,r)}getUrl(t){if(!t)return null;let e=t.getAttribute("src");return e||(e=t.src),e||(this._didWarnAboutMissingUrl||console.warn("No url found in modelviewer",t),this._didWarnAboutMissingUrl=!0),e}tryGetCurrentGLTF(t){return t._currentGLTF}tryGetCurrentModelViewer(t){return t.element}tryParseTextureLOD(t,e){if(e[re]==!0)return;e[re]=!0;const s=this.tryGetCurrentGLTF(t),r=this.tryGetCurrentModelViewer(t),o=this.getUrl(r);if(o&&s&&e.material){let n=function(a){if(a[re]==!0)return;a[re]=!0,a.userData&&(a.userData.LOD=-1);const u=Object.keys(a);for(let d=0;d<u.length;d++){const x=u[d],L=a[x];if(L?.isTexture===!0){const _=L.userData?.associations?.textures;if(_==null)continue;const k=s.parser.json.textures[_];if(!k){console.warn("Texture data not found for texture index "+_);continue}if(k?.extensions?.[F]){const D=k.extensions[F];D&&o&&m.registerTexture(o,L,D.lods.length,_,D)}}}};const l=e.material;if(Array.isArray(l))for(const a of l)n(a);else n(l)}}tryParseMeshLOD(t,e){if(e[ve]==!0)return;e[ve]=!0;const s=this.tryGetCurrentModelViewer(t),r=this.getUrl(s);if(!r)return;const o=e.userData?.gltfExtensions?.[F];if(o&&r){const n=e.uuid;m.registerMesh(r,n,e,0,o.lods.length,o)}}}function it(...i){let t,e,s,r;switch(i.length){case 2:[s,e]=i,r={};break;case 3:[s,e,r]=i;break;case 4:[t,e,s,r]=i;break;default:throw new Error("Invalid arguments")}ne(e),xe(s),we(s,{progressive:!0,...r?.hints}),s.register(n=>new m(n));const o=Z.get(e);return r?.enableLODsManager!==!1&&o.enable(),o}Ae();if(!Ee){const i={gltfProgressive:{useNeedleProgressive:it,LODsManager:Z,configureLoader:we,getRaycastMesh:j,useRaycastMeshes:je}};if(!globalThis.Needle)globalThis.Needle=i;else for(const t in i)globalThis.Needle[t]=i[t]}exports.LODsManager=Z;exports.NEEDLE_progressive=m;exports.addDracoAndKTX2Loaders=xe;exports.configureLoader=we;exports.createLoaders=ne;exports.getRaycastMesh=j;exports.setDracoDecoderLocation=Te;exports.setKTX2TranscoderLocation=Se;