@needle-tools/engine 4.16.4-alpha → 4.16.4-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/{gltf-progressive-LOFTyzy4.umd.cjs → gltf-progressive-BacJPTD6.umd.cjs} +8 -8
  3. package/dist/{gltf-progressive-DQa78GTA.min.js → gltf-progressive-BmXoz_HR.min.js} +8 -8
  4. package/dist/{gltf-progressive-_wvokUUu.js → gltf-progressive-CzxjNmG6.js} +116 -107
  5. package/dist/{needle-engine.bundle-LGUii273.js → needle-engine.bundle-5JsGk35A.js} +3113 -3074
  6. package/dist/{needle-engine.bundle-BQv3gp3d.umd.cjs → needle-engine.bundle-C8L0tAbe.umd.cjs} +114 -114
  7. package/dist/{needle-engine.bundle-C_iC-4wt.min.js → needle-engine.bundle-DISIqVWx.min.js} +125 -125
  8. package/dist/needle-engine.d.ts +15 -4
  9. package/dist/needle-engine.js +3 -3
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{postprocessing-C-WOZQC5.js → postprocessing-BUS23YkY.js} +2 -2
  13. package/dist/{postprocessing-CtXfLXvp.umd.cjs → postprocessing-C_736uMz.umd.cjs} +2 -2
  14. package/dist/{postprocessing-DXm8YKbQ.min.js → postprocessing-CaEfRjRY.min.js} +2 -2
  15. package/lib/engine/engine_lods.d.ts +9 -1
  16. package/lib/engine/engine_lods.js +9 -1
  17. package/lib/engine/engine_lods.js.map +1 -1
  18. package/lib/engine/engine_mainloop_utils.js +49 -60
  19. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  20. package/lib/engine/webcomponents/needle-engine.d.ts +6 -2
  21. package/lib/engine/webcomponents/needle-engine.js +56 -41
  22. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  23. package/package.json +3 -2
  24. package/src/engine/engine_lods.ts +10 -2
  25. package/src/engine/engine_mainloop_utils.ts +54 -62
  26. package/src/engine/webcomponents/needle-engine.ts +62 -47
  27. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -14,8 +14,10 @@ const _tempBox: Box3 = new Box3();
14
14
  const _tempSphere: Sphere = new Sphere();
15
15
 
16
16
  /**
17
- * Needle Engine LODs manager. Wrapper around the internal LODs manager.
18
- * It uses the @needle-tools/gltf-progressive package to manage LODs.
17
+ * Needle Engine LODs manager. Wrapper around the internal LODs manager.
18
+ * It uses the [@needle-tools/gltf-progressive](https://npmjs.com/package/@needle-tools/gltf-progressive) package to manage LODs.
19
+ *
20
+ * For lower-level control (e.g. configuring max concurrent loading tasks, queue settings, or other progressive loading specifics), use {@link NEEDLE_progressive} directly.
19
21
  * @link https://npmjs.com/package/@needle-tools/gltf-progressive
20
22
  */
21
23
  export class LODsManager implements NEEDLE_progressive_plugin {
@@ -37,6 +39,11 @@ export class LODsManager implements NEEDLE_progressive_plugin {
37
39
  return this._lodsManager;
38
40
  }
39
41
 
42
+ /**
43
+ * The interval (in seconds) at which the bounding volumes of skinned meshes are automatically updated.
44
+ * If set to 0, automatic updates are disabled and bounding volumes will only be updated when the mesh is loaded or when the `updateSkinnedMeshBounds` method is called manually.
45
+ * @default 0
46
+ */
40
47
  get skinnedMeshAutoUpdateBoundsInterval() {
41
48
  return this._lodsManager?.skinnedMeshAutoUpdateBoundsInterval || this._settings.skinnedMeshAutoUpdateBoundsInterval || 0;
42
49
  }
@@ -57,6 +64,7 @@ export class LODsManager implements NEEDLE_progressive_plugin {
57
64
  this.applySettings();
58
65
  }
59
66
 
67
+ /** @internal */
60
68
  constructor(context: Context) {
61
69
  this.context = context;
62
70
  }
@@ -270,6 +270,10 @@ export function isNeedleXRSessionEventReceiver(script: any, mode: XRSessionMode
270
270
  }
271
271
 
272
272
 
273
+
274
+
275
+ // #region activeInHierarchy
276
+
273
277
  let needsUpdate = true;
274
278
  export function markHierarchyDirty() {
275
279
  needsUpdate = true;
@@ -304,7 +308,7 @@ export function updateIsActive(obj?: Object3D, force: boolean = false) {
304
308
  }
305
309
  }
306
310
 
307
- function updateIsActiveInHierarchyRecursiveRuntime(go: Object3D, activeInHierarchy: boolean, allowEventCall: boolean, level: number = 0) {
311
+ function updateIsActiveInHierarchyRecursiveRuntime(go: Object3D, activeInHierarchy: boolean, allowEventCall: boolean, level: number = 0): boolean {
308
312
  if (level > 1000) {
309
313
  console.warn("Hierarchy is too deep (> 1000 level) - will abort updating active state");
310
314
  return false;
@@ -336,74 +340,70 @@ function updateIsActiveInHierarchyRecursiveRuntime(go: Object3D, activeInHierarc
336
340
  go[constants.activeInHierarchyFieldName] = activeInHierarchy;
337
341
 
338
342
  if (debugHierarchy)
339
- console.warn("ACTIVE CHANGE", go.name, activeSelf, go.visible, activeInHierarchy, "changed?" + changed, go);
343
+ console.warn("ACTIVE CHANGE", { name: go.name, activeSelf, visible: go.visible, activeInHierarchy, changed, go });
340
344
  if (allowEventCall) {
341
- perComponent(go, comp => {
342
- if (activeInHierarchy) {
343
- if (comp.enabled) {
344
- safeInvoke(comp.__internalAwake.bind(comp));
345
+ const components = go.userData?.components;
346
+ if (components) {
347
+ for (let ci = 0, cl = components.length; ci < cl; ci++) {
348
+ const comp = components[ci];
349
+ if (activeInHierarchy) {
345
350
  if (comp.enabled) {
346
- comp.__internalEnable();
351
+ try { comp.__internalAwake(); }
352
+ catch (err) { console.error(err); }
353
+ if (comp.enabled) {
354
+ comp.__internalEnable();
355
+ }
347
356
  }
348
357
  }
349
- }
350
- else {
351
- if (comp["__didAwake"] && comp.enabled) {
352
- comp["__didEnable"] = false;
353
- comp.onDisable();
358
+ else {
359
+ if (comp["__didAwake"] && comp.enabled) {
360
+ comp["__didEnable"] = false;
361
+ comp.onDisable();
362
+ }
354
363
  }
355
364
  }
356
- });
365
+ }
357
366
  }
358
367
  }
359
-
360
- let success = true;
361
- if (go.children) {
362
- for (const ch of go.children) {
363
- const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall, level + 1);
364
- if (res === false) success = false;
368
+ const children = go.children;
369
+ if (children) {
370
+ // When this node is inactive and hasn't changed, skip children that are already
371
+ // marked inactive. Only recurse into children that still have a stale active=true
372
+ // (e.g. after reparenting into this inactive subtree).
373
+ if (!changed && !activeInHierarchy) {
374
+ let success = true;
375
+ for (let i = 0, l = children.length; i < l; i++) {
376
+ const ch = children[i];
377
+ if (ch[constants.activeInHierarchyFieldName] !== false) {
378
+ if (updateIsActiveInHierarchyRecursiveRuntime(ch, false, allowEventCall, level + 1) === false)
379
+ success = false;
380
+ }
381
+ }
382
+ return success;
365
383
  }
384
+ let success = true;
385
+ for (let i = 0, l = children.length; i < l; i++) {
386
+ if (updateIsActiveInHierarchyRecursiveRuntime(children[i], activeInHierarchy, allowEventCall, level + 1) === false)
387
+ success = false;
388
+ }
389
+ return success;
366
390
  }
367
- return success;
391
+ return true;
368
392
  }
369
393
 
370
- // function tryFindActiveStateInParent(obj: Object3D): boolean {
371
- // let current: Object3D | undefined | null = obj;
372
- // while (current) {
373
- // const activeState = current[constants.activeInHierarchyFieldName];
374
- // if (activeState !== undefined) return activeState;
375
- // if (current instanceof Scene && !current.parent) {
376
- // return true;
377
- // }
378
- // current = current.parent;
379
- // }
380
- // return false;
381
- // }
382
-
383
- // let isRunning = false;
384
- // // Prevent: https://github.com/needle-tools/needle-tiny/issues/641
385
- // const temporyChildArrayBuffer: Array<Array<Object3D>> = [];
386
- // export function* iterateChildrenSafe(obj: Object3D) {
387
- // if (!obj || !obj.children) yield null;
388
- // // if(isRunning) return;
389
- // // isRunning = true;
390
- // const arr = temporyChildArrayBuffer.pop() || [];
391
- // arr.push(...obj.children);
392
- // for (const ch of arr) {
393
- // yield ch;
394
- // }
395
- // // isRunning = false;
396
- // arr.length = 0;
397
- // temporyChildArrayBuffer.push(arr);
398
- // }
394
+
395
+
399
396
 
400
397
  /** @internal */
401
398
  export function updateActiveInHierarchyWithoutEventCall(go: Object3D) {
399
+ if (!go) {
400
+ console.error("GO is null");
401
+ return;
402
+ }
402
403
  let activeInHierarchy = true;
404
+ let foundScene = false;
403
405
  let current: Object3D | null = go;
404
- let foundScene: boolean = false;
405
406
  while (current) {
406
- if (!current) break;
407
407
  if (current.type === "Scene") foundScene = true;
408
408
  if (!isActiveSelf(current)) {
409
409
  activeInHierarchy = false;
@@ -411,22 +411,14 @@ export function updateActiveInHierarchyWithoutEventCall(go: Object3D) {
411
411
  }
412
412
  current = current.parent;
413
413
  }
414
- if (!go) {
415
- console.error("GO is null");
416
- return;
417
- }
418
414
  go[constants.activeInHierarchyFieldName] = activeInHierarchy && foundScene;
419
415
  }
420
416
 
421
- function perComponent(go: Object3D, evt: (comp: IComponent) => void) {
422
- if (go.userData?.components) {
423
- for (const comp of go.userData.components) {
424
- evt(comp);
425
- }
426
- }
427
- }
428
417
 
429
418
 
419
+
420
+ // #region prewarm
421
+
430
422
  const prewarmList: Map<IContext, Object3D[]> = new Map();
431
423
  const $prewarmedFlag = Symbol("prewarmFlag");
432
424
  const $waitingForPrewarm = Symbol("waitingForPrewarm");
@@ -168,7 +168,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
168
168
  */
169
169
  public get context() { return this._context; }
170
170
 
171
- private _context!: Context;
171
+ private _context?: Context;
172
172
  private _overlay_ar!: AROverlayHandler;
173
173
  private _loadingProgress01: number = 0;
174
174
  private _loadingView?: ILoadingViewHandler;
@@ -298,12 +298,10 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
298
298
 
299
299
 
300
300
  this._overlay_ar = new AROverlayHandler();
301
- this._context ??= new Context({ domElement: this }); // we might have one already if onLoad is running
301
+ this.getOrCreateContext();
302
302
  this.addEventListener("xr-session-started", this.onXRSessionStarted);
303
303
  this.onSetupDesktop();
304
304
 
305
-
306
-
307
305
  if (!this.getAttribute("src")) {
308
306
  const global = (globalThis as any)["needle:codegen_files"] as unknown as string;
309
307
  if (debug) console.log("src is null, trying to load from globalThis[\"needle:codegen_files\"]", global);
@@ -418,18 +416,23 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
418
416
  case "focus-rect":
419
417
  {
420
418
  const focus_rect = this.getAttribute("focus-rect") as HTMLElement | string | null;
421
- if (focus_rect && this._context) {
419
+ if (focus_rect) {
420
+ const context = this.getOrCreateContext();
421
+
422
422
  if (focus_rect === null) {
423
- this._context.setCameraFocusRect(null);
423
+ context.setCameraFocusRect(null);
424
424
  }
425
425
  else if (typeof focus_rect === "string" && focus_rect.length > 0) {
426
426
  const element = document.querySelector(focus_rect);
427
- this._context.setCameraFocusRect(element instanceof HTMLElement ? element : null);
427
+ if (!element) console.warn(`No element found for focus-rect selector: ${focus_rect}`);
428
+ context.setCameraFocusRect(element instanceof HTMLElement ? element : null);
428
429
  }
429
430
  else if (focus_rect instanceof HTMLElement) {
430
- this._context.setCameraFocusRect(focus_rect);
431
+ context.setCameraFocusRect(focus_rect);
432
+ }
433
+ else {
434
+ console.warn("Invalid focus-rect value. Expected a CSS selector string or an HTMLElement.", focus_rect);
431
435
  }
432
-
433
436
  }
434
437
  }
435
438
  break;
@@ -447,19 +450,22 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
447
450
  private _lastSourceFiles: Array<string> | null = null;
448
451
  private _createContextPromise: Promise<boolean> | null = null;
449
452
 
450
- private async onLoad() {
451
-
452
- if (!this.isConnected) return;
453
-
453
+ /**
454
+ * Check if we have a context. If not a new one is created.
455
+ */
456
+ private getOrCreateContext() {
454
457
  if (!this._context) {
455
458
  if (debug) console.warn("Create new context");
456
459
  this._context = new Context({ domElement: this });
457
460
  }
461
+ return this._context;
462
+ }
458
463
 
459
- if (!this._context) {
460
- console.error("Needle Engine: Context not initialized");
461
- return;
462
- }
464
+ private async onLoad() {
465
+
466
+ if (!this.isConnected) return;
467
+
468
+ const context = this.getOrCreateContext();
463
469
 
464
470
  const filesToLoad = this.getSourceFiles();
465
471
  if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
@@ -477,7 +483,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
477
483
 
478
484
  if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
479
485
  if (debug) console.warn("Clear scene", filesToLoad);
480
- this._context.clear();
486
+ context.clear();
481
487
  if (loadId !== this._loadId) return;
482
488
  }
483
489
 
@@ -491,7 +497,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
491
497
  this.ensureLoadStartIsRegistered();
492
498
  let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
493
499
  detail: {
494
- context: this._context,
500
+ context: context,
495
501
  alias: alias
496
502
  },
497
503
  cancelable: true
@@ -581,9 +587,9 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
581
587
 
582
588
  const currentHash = this.getAttribute("hash");
583
589
  if (currentHash !== null && currentHash !== undefined)
584
- this._context.hash = currentHash;
585
- this._context.alias = alias;
586
- this._createContextPromise = this._context.create(args);
590
+ context.hash = currentHash;
591
+ context.alias = alias;
592
+ this._createContextPromise = context.create(args);
587
593
  const success = await this._createContextPromise;
588
594
  this.applyAttributes();
589
595
 
@@ -615,42 +621,45 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
615
621
 
616
622
  // #region applyAttributes
617
623
  private applyAttributes() {
624
+
625
+ const context = this.getOrCreateContext();
626
+
618
627
  // set tonemapping if configured
619
- if (this._context?.renderer) {
628
+ if (context.renderer) {
620
629
  const threeTonemapping = nameToThreeTonemapping(this.toneMapping);
621
630
  if (threeTonemapping !== undefined) {
622
- this._context.renderer.toneMapping = threeTonemapping;
631
+ context.renderer.toneMapping = threeTonemapping;
623
632
  }
624
633
  const exposure = this.getAttribute("tone-mapping-exposure");
625
634
  if (exposure !== null && exposure !== undefined) {
626
635
  const value = parseFloat(exposure);
627
636
  if (!isNaN(value))
628
- this._context.renderer.toneMappingExposure = value;
637
+ context.renderer.toneMappingExposure = value;
629
638
  }
630
639
  }
631
640
 
632
641
  const backgroundBlurriness = this.getAttribute("background-blurriness");
633
642
  if (backgroundBlurriness !== null && backgroundBlurriness !== undefined) {
634
643
  const value = parseFloat(backgroundBlurriness);
635
- if (!isNaN(value) && this._context) {
636
- this._context.scene.backgroundBlurriness = value;
644
+ if (!isNaN(value)) {
645
+ context.scene.backgroundBlurriness = value;
637
646
  }
638
647
  }
639
648
 
640
649
  const environmentIntensity = this.getAttribute("environment-intensity");
641
- if (environmentIntensity != undefined && this._context) {
650
+ if (environmentIntensity != undefined) {
642
651
  const value = parseFloat(environmentIntensity);
643
- if (!isNaN(value) && this._context)
644
- this._context.scene.environmentIntensity = value;
652
+ if (!isNaN(value))
653
+ context.scene.environmentIntensity = value;
645
654
  }
646
655
 
647
656
  const backgroundColor = this.getAttribute("background-color");
648
- if (this._context?.renderer) {
657
+ if (context.renderer) {
649
658
  if (typeof backgroundColor === "string" && backgroundColor.length > 0) {
650
659
  const rgbaColor = RGBAColor.fromColorRepresentation(backgroundColor);
651
660
  if (debug) console.debug("<needle-engine> background-color changed, str:", backgroundColor, "→", rgbaColor)
652
- this._context.renderer.setClearColor(rgbaColor, rgbaColor.alpha);
653
- this.context.scene.background = null;
661
+ context.renderer.setClearColor(rgbaColor, rgbaColor.alpha);
662
+ context.scene.background = null;
654
663
  }
655
664
  // HACK: if we set background-color to a color and then back to null we want the background-image attribute to re-apply
656
665
  else if (this.getAttribute("background-image")) {
@@ -660,19 +669,21 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
660
669
  }
661
670
 
662
671
  private onXRSessionStarted = () => {
663
- const xrSessionMode = this.context.xrSessionMode;
672
+ const context = this.getOrCreateContext();
673
+
674
+ const xrSessionMode = context.xrSessionMode;
664
675
  if (xrSessionMode === "immersive-ar")
665
- this.onEnterAR(this.context.xrSession!);
676
+ this.onEnterAR(context.xrSession!);
666
677
  else if (xrSessionMode === "immersive-vr")
667
- this.onEnterVR(this.context.xrSession!);
678
+ this.onEnterVR(context.xrSession!);
668
679
 
669
680
  // handle session end:
670
- this.context.xrSession?.addEventListener("end", () => {
671
- this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: this.context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
681
+ context.xrSession?.addEventListener("end", () => {
682
+ this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
672
683
  if (xrSessionMode === "immersive-ar")
673
- this.onExitAR(this.context.xrSession!);
684
+ this.onExitAR(context.xrSession!);
674
685
  else if (xrSessionMode === "immersive-vr")
675
- this.onExitVR(this.context.xrSession!);
686
+ this.onExitVR(context.xrSession!);
676
687
  });
677
688
  };
678
689
 
@@ -741,7 +752,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
741
752
  if (typeof fn === "function") {
742
753
  this._previouslyRegisteredMap.set(eventName, fn);
743
754
  // @ts-ignore // not sure how to type this properly
744
- this.addEventListener(eventName, evt => fn?.call(globalThis, this._context, evt));
755
+ this.addEventListener(eventName, evt => fn?.call(globalThis, this.getOrCreateContext(), evt));
745
756
  }
746
757
  }
747
758
  catch (err) {
@@ -783,35 +794,39 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
783
794
  * @internal
784
795
  */
785
796
  onEnterAR(session: XRSession) {
797
+ const context = this.getOrCreateContext();
786
798
  this.onSetupAR();
787
799
  const overlayContainer = this.getAROverlayContainer();
788
- this._overlay_ar.onBegin(this._context!, overlayContainer, session);
789
- this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
800
+ this._overlay_ar.onBegin(context, overlayContainer, session);
801
+ this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: context, htmlContainer: this._overlay_ar?.ARContainer } }));
790
802
  }
791
803
 
792
804
  /**
793
805
  * @internal
794
806
  */
795
807
  onExitAR(session: XRSession) {
796
- this._overlay_ar.onEnd(this._context!);
808
+ const context = this.getOrCreateContext();
809
+ this._overlay_ar.onEnd(context);
797
810
  this.onSetupDesktop();
798
- this.dispatchEvent(new CustomEvent("exit-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
811
+ this.dispatchEvent(new CustomEvent("exit-ar", { detail: { session: session, context: context, htmlContainer: this._overlay_ar?.ARContainer } }));
799
812
  }
800
813
 
801
814
  /**
802
815
  * @internal
803
816
  */
804
817
  onEnterVR(session: XRSession) {
818
+ const context = this.getOrCreateContext();
805
819
  this.onSetupVR();
806
- this.dispatchEvent(new CustomEvent("enter-vr", { detail: { session: session, context: this._context } }));
820
+ this.dispatchEvent(new CustomEvent("enter-vr", { detail: { session: session, context: context } }));
807
821
  }
808
822
 
809
823
  /**
810
824
  * @internal
811
825
  */
812
826
  onExitVR(session: XRSession) {
827
+ const context = this.getOrCreateContext();
813
828
  this.onSetupDesktop();
814
- this.dispatchEvent(new CustomEvent("exit-vr", { detail: { session: session, context: this._context } }));
829
+ this.dispatchEvent(new CustomEvent("exit-vr", { detail: { session: session, context: context } }));
815
830
  }
816
831
 
817
832
  private onSetupAR() {