@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.
- package/CHANGELOG.md +9 -0
- package/SKILL.md +64 -4
- package/dist/gltf-progressive-DLDvZ8Xm.umd.cjs +10 -0
- package/dist/{gltf-progressive-CzxjNmG6.js → gltf-progressive-DMK8kKVB.js} +352 -346
- package/dist/gltf-progressive-RPgYR6rv.min.js +10 -0
- package/dist/{needle-engine.bundle-a0Pw5htf.js → needle-engine.bundle-BfsnYTeP.js} +7329 -7105
- package/dist/{needle-engine.bundle-D_nuVSFb.umd.cjs → needle-engine.bundle-COr8D1vc.umd.cjs} +132 -132
- package/dist/{needle-engine.bundle-DlyYXu9v.min.js → needle-engine.bundle-DG1sGkxn.min.js} +131 -131
- package/dist/needle-engine.d.ts +60 -3
- package/dist/needle-engine.js +3 -3
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/three-examples.js +52 -52
- package/dist/three-examples.min.js +4 -4
- package/dist/three-examples.umd.cjs +2 -2
- package/lib/engine/api.d.ts +2 -1
- package/lib/engine/api.js +1 -6
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/debug/debug_spector.js +1 -1
- package/lib/engine/debug/debug_spector.js.map +1 -1
- package/lib/engine/engine_addressables.d.ts +32 -1
- package/lib/engine/engine_addressables.js +71 -9
- package/lib/engine/engine_addressables.js.map +1 -1
- package/lib/engine/engine_application.d.ts +2 -0
- package/lib/engine/engine_application.js +7 -3
- package/lib/engine/engine_application.js.map +1 -1
- package/lib/engine/engine_audio.d.ts +30 -1
- package/lib/engine/engine_audio.js +57 -13
- package/lib/engine/engine_audio.js.map +1 -1
- package/lib/engine/engine_context.js +25 -1
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_context_registry.js +1 -1
- package/lib/engine/engine_context_registry.js.map +1 -1
- package/lib/engine/engine_input.js +1 -1
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/engine_license.d.ts +1 -1
- package/lib/engine/engine_license.js +31 -5
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_lifecycle_functions_internal.js +20 -2
- package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
- package/lib/engine/engine_networking_streams.d.ts +5 -1
- package/lib/engine/engine_networking_streams.js +64 -17
- package/lib/engine/engine_networking_streams.js.map +1 -1
- package/lib/engine/engine_shims.js +1 -1
- package/lib/engine/engine_shims.js.map +1 -1
- package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +5 -0
- package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
- package/lib/engine/webcomponents/fonts.d.ts +1 -1
- package/lib/engine/webcomponents/fonts.js +7 -0
- package/lib/engine/webcomponents/fonts.js.map +1 -1
- package/lib/engine/webcomponents/icons.js +8 -1
- package/lib/engine/webcomponents/icons.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +5 -0
- package/lib/engine/webcomponents/needle menu/needle-menu.js +31 -13
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
- package/lib/engine/webcomponents/needle-engine.js +16 -4
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.d.ts +2 -0
- package/lib/engine/webcomponents/needle-engine.loading.js +10 -0
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine-components/AudioSource.d.ts +49 -2
- package/lib/engine-components/AudioSource.js +261 -66
- package/lib/engine-components/AudioSource.js.map +1 -1
- package/lib/engine-components/CameraUtils.js +1 -1
- package/lib/engine-components/CameraUtils.js.map +1 -1
- package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
- package/package.json +2 -2
- package/src/engine/api.ts +3 -1
- package/src/engine/debug/debug_spector.ts +1 -1
- package/src/engine/engine_addressables.ts +75 -9
- package/src/engine/engine_application.ts +7 -2
- package/src/engine/engine_audio.ts +68 -15
- package/src/engine/engine_context.ts +25 -1
- package/src/engine/engine_context_registry.ts +1 -1
- package/src/engine/engine_input.ts +2 -2
- package/src/engine/engine_license.ts +28 -5
- package/src/engine/engine_lifecycle_functions_internal.ts +21 -2
- package/src/engine/engine_networking_streams.ts +64 -19
- package/src/engine/engine_shims.ts +1 -1
- package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +8 -1
- package/src/engine/webcomponents/fonts.ts +6 -0
- package/src/engine/webcomponents/icons.ts +9 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +30 -12
- package/src/engine/webcomponents/needle-engine.loading.ts +13 -1
- package/src/engine/webcomponents/needle-engine.ts +15 -5
- package/src/engine-components/AudioSource.ts +244 -63
- package/src/engine-components/CameraUtils.ts +1 -1
- package/dist/gltf-progressive-BacJPTD6.umd.cjs +0 -10
- package/dist/gltf-progressive-BmXoz_HR.min.js +0 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
158
|
-
return this.
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
//
|
|
567
|
-
let needsLoading = !this.sound || (clip && clip !== this.
|
|
568
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
589
|
-
//
|
|
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
|
-
|
|
604
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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;
|