@needle-tools/engine 4.10.0-beta → 4.10.0-beta.2

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 (96) hide show
  1. package/components.needle.json +1 -1
  2. package/dist/{needle-engine.bundle-Dj6faVbC.js → needle-engine.bundle-BTgC7uAm.js} +7236 -7012
  3. package/dist/{needle-engine.bundle-42AmEGfk.umd.cjs → needle-engine.bundle-OTBqjiCd.umd.cjs} +152 -141
  4. package/dist/{needle-engine.bundle-C6zhyLF5.min.js → needle-engine.bundle-g2_JEHcF.min.js} +170 -159
  5. package/dist/needle-engine.js +259 -257
  6. package/dist/needle-engine.min.js +1 -1
  7. package/dist/needle-engine.umd.cjs +1 -1
  8. package/lib/engine/codegen/register_types.js +2 -0
  9. package/lib/engine/codegen/register_types.js.map +1 -1
  10. package/lib/engine/engine_camera.d.ts +7 -1
  11. package/lib/engine/engine_camera.fit.d.ts +1 -1
  12. package/lib/engine/engine_camera.fit.js +3 -30
  13. package/lib/engine/engine_camera.fit.js.map +1 -1
  14. package/lib/engine/engine_camera.js +46 -6
  15. package/lib/engine/engine_camera.js.map +1 -1
  16. package/lib/engine/engine_context.d.ts +6 -0
  17. package/lib/engine/engine_context.js +48 -9
  18. package/lib/engine/engine_context.js.map +1 -1
  19. package/lib/engine/engine_lightdata.d.ts +3 -3
  20. package/lib/engine/engine_lightdata.js +10 -10
  21. package/lib/engine/engine_lightdata.js.map +1 -1
  22. package/lib/engine/engine_physics_rapier.js +4 -0
  23. package/lib/engine/engine_physics_rapier.js.map +1 -1
  24. package/lib/engine/engine_scenelighting.d.ts +1 -1
  25. package/lib/engine/engine_scenelighting.js +4 -5
  26. package/lib/engine/engine_scenelighting.js.map +1 -1
  27. package/lib/engine/engine_utils.d.ts +3 -1
  28. package/lib/engine/engine_utils.js +11 -0
  29. package/lib/engine/engine_utils.js.map +1 -1
  30. package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
  31. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  32. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  33. package/lib/engine/webcomponents/logo-element.js +29 -5
  34. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  35. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  36. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  37. package/lib/engine/webcomponents/needle-engine.js +22 -0
  38. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  39. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  40. package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
  41. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  42. package/lib/engine-components/OrbitControls.d.ts +4 -1
  43. package/lib/engine-components/OrbitControls.js +30 -6
  44. package/lib/engine-components/OrbitControls.js.map +1 -1
  45. package/lib/engine-components/Renderer.js +6 -1
  46. package/lib/engine-components/Renderer.js.map +1 -1
  47. package/lib/engine-components/Skybox.js +22 -4
  48. package/lib/engine-components/Skybox.js.map +1 -1
  49. package/lib/engine-components/codegen/components.d.ts +1 -0
  50. package/lib/engine-components/codegen/components.js +1 -0
  51. package/lib/engine-components/codegen/components.js.map +1 -1
  52. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
  53. package/lib/engine-components/timeline/PlayableDirector.js +7 -0
  54. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  55. package/lib/engine-components/timeline/TimelineModels.d.ts +9 -1
  56. package/lib/engine-components/timeline/TimelineTracks.js +4 -2
  57. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  58. package/lib/engine-components/utils/LookAt.js +5 -1
  59. package/lib/engine-components/utils/LookAt.js.map +1 -1
  60. package/lib/engine-components/web/Clickthrough.js +10 -2
  61. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  62. package/lib/engine-components/web/ScrollFollow.d.ts +22 -0
  63. package/lib/engine-components/web/ScrollFollow.js +159 -38
  64. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  65. package/lib/engine-components/web/ViewBox.d.ts +16 -0
  66. package/lib/engine-components/web/ViewBox.js +186 -0
  67. package/lib/engine-components/web/ViewBox.js.map +1 -0
  68. package/lib/engine-components/web/index.d.ts +1 -0
  69. package/lib/engine-components/web/index.js +1 -0
  70. package/lib/engine-components/web/index.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/engine/codegen/register_types.ts +2 -0
  73. package/src/engine/engine_camera.fit.ts +2 -32
  74. package/src/engine/engine_camera.ts +62 -8
  75. package/src/engine/engine_context.ts +50 -10
  76. package/src/engine/engine_lightdata.ts +11 -11
  77. package/src/engine/engine_physics_rapier.ts +3 -0
  78. package/src/engine/engine_scenelighting.ts +5 -6
  79. package/src/engine/engine_utils.ts +12 -0
  80. package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  81. package/src/engine/webcomponents/logo-element.ts +29 -4
  82. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  83. package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
  84. package/src/engine/webcomponents/needle-engine.ts +33 -6
  85. package/src/engine-components/OrbitControls.ts +40 -1
  86. package/src/engine-components/Renderer.ts +6 -1
  87. package/src/engine-components/Skybox.ts +26 -7
  88. package/src/engine-components/codegen/components.ts +1 -0
  89. package/src/engine-components/timeline/PlayableDirector.ts +9 -0
  90. package/src/engine-components/timeline/TimelineModels.ts +9 -1
  91. package/src/engine-components/timeline/TimelineTracks.ts +4 -2
  92. package/src/engine-components/utils/LookAt.ts +5 -1
  93. package/src/engine-components/web/Clickthrough.ts +11 -2
  94. package/src/engine-components/web/ScrollFollow.ts +190 -44
  95. package/src/engine-components/web/ViewBox.ts +202 -0
  96. package/src/engine-components/web/index.ts +2 -1
@@ -32,10 +32,22 @@ export class NeedleLogoElement extends HTMLElement {
32
32
  cursor: pointer;
33
33
  }
34
34
  img {
35
- width: 95px;
36
35
  height: 100%;
37
36
  align-self: end;
38
37
  margin-left: 0.6rem;
38
+ transition: transform 0.2s;
39
+ }
40
+ img.with-text {
41
+ width: 11.5ch;
42
+ &:hover {
43
+ transform: scale(1.02);
44
+ }
45
+ }
46
+ img.compact {
47
+ width: 1.7em;
48
+ &:hover {
49
+ transform: scale(1.1);
50
+ }
39
51
  }
40
52
  span {
41
53
  font-size: 1rem;
@@ -43,12 +55,14 @@ export class NeedleLogoElement extends HTMLElement {
43
55
  }
44
56
  </style>
45
57
  <div class="wrapper">
46
- <img class="logo" src=${needleLogoSVG} />
58
+ <img class="logo with-text" src=${needleLogoSVG} />
47
59
  </div>
48
60
  `;
49
61
  this._root.appendChild(template.content.cloneNode(true));
50
62
  this.wrapper = this._root.querySelector(".wrapper") as HTMLDivElement;
51
63
  this._root.appendChild(this.wrapper);
64
+ this.logoElement = this._root.querySelector("img.logo") as HTMLImageElement;
65
+
52
66
  // this.wrapper.classList.add("wrapper");
53
67
 
54
68
  // this.wrapper.appendChild(this.logoElement);
@@ -67,13 +81,24 @@ export class NeedleLogoElement extends HTMLElement {
67
81
 
68
82
  private readonly _root: ShadowRoot;
69
83
  private readonly wrapper: HTMLDivElement;
70
- private readonly logoElement: HTMLImageElement = document.createElement("img");
71
- private readonly textElement: HTMLSpanElement = document.createElement("span");
84
+ private readonly logoElement: HTMLImageElement;
72
85
 
73
86
  setLogoVisible(val: boolean) {
74
87
  this.logoElement.style.display = val ? "block" : "none";
75
88
  }
76
89
 
90
+ setType(type: "full" | "compact") {
91
+ if (type === "full") {
92
+ this.logoElement.src = needleLogoSVG;
93
+ this.logoElement.classList.remove("with-text");
94
+ this.logoElement.classList.remove("compact");
95
+ } else {
96
+ this.logoElement.src = needleLogoOnlySVG;
97
+ this.logoElement.classList.add("with-text");
98
+ this.logoElement.classList.add("compact");
99
+ }
100
+ }
101
+
77
102
  }
78
103
  if (!customElements.get(elementName))
79
104
  customElements.define(elementName, NeedleLogoElement);
@@ -459,7 +459,7 @@ export class NeedleMenuElement extends HTMLElement {
459
459
 
460
460
  .logo {
461
461
  cursor: pointer;
462
- padding-left: 0.6rem;
462
+ padding-left: 0.0rem;
463
463
  padding-bottom: .02rem;
464
464
  margin-right: 0.5rem;
465
465
  }
@@ -664,8 +664,8 @@ export class NeedleMenuElement extends HTMLElement {
664
664
  <slot name="end"></slot>
665
665
  </div>
666
666
  </div>
667
- <div style="user-select:none" class="logo">
668
- <span class="madewith notranslate">powered by</span>
667
+ <div style="user-select:none;" class="logo">
668
+ <span class="madewith notranslate" style="display:none;">powered by</span>
669
669
  </div>
670
670
  </div>
671
671
  <button class="compact-menu-button">
@@ -698,6 +698,7 @@ export class NeedleMenuElement extends HTMLElement {
698
698
  this.wrapper.classList.add("wrapper");
699
699
 
700
700
  const logo = NeedleLogoElement.create();
701
+ logo.setType("compact");
701
702
  logo.style.minHeight = "1rem";
702
703
  this.logoContainer.append(logo);
703
704
  this.logoContainer.addEventListener("click", () => {
@@ -374,40 +374,40 @@ export class EngineLoadingView implements ILoadingViewHandler {
374
374
  // }
375
375
  // }
376
376
 
377
- this.handleRuntimeLicense(this._loadingElement);
377
+ // this.handleRuntimeLicense(this._loadingElement);
378
378
 
379
379
  return this._loadingElement;
380
380
  }
381
381
 
382
- private async handleRuntimeLicense(loadingElement: HTMLElement) {
383
- // First check if we have a commercial license
384
- let commercialLicense = hasCommercialLicense();
385
- // if it's the case then we don't need to perform a runtime check
386
- if (commercialLicense) return;
387
-
388
- // If we don't have a commercial license, then we need to display our message
389
- if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
390
- const nonCommercialContainer = document.createElement("div");
391
- nonCommercialContainer.style.paddingTop = ".6em";
392
- nonCommercialContainer.style.fontSize = ".8em";
393
- nonCommercialContainer.style.textTransform = "uppercase";
394
- nonCommercialContainer.innerText = "NEEDLE ENGINE NON COMMERCIAL VERSION\nCLICK HERE TO GET A LICENSE";
395
- nonCommercialContainer.style.cursor = "pointer";
396
- nonCommercialContainer.style.userSelect = "none";
397
- nonCommercialContainer.style.textAlign = "center";
398
- nonCommercialContainer.style.pointerEvents = "all";
399
- nonCommercialContainer.addEventListener("click", () => window.open("https://needle.tools/pricing", "_self"));
400
- nonCommercialContainer.style.opacity = "0";
401
- loadingElement.appendChild(nonCommercialContainer);
402
-
403
- // Use the runtime license check
404
- if (!isDevEnvironment() && runtimeLicenseCheckPromise) {
405
- if (debugLicense) console.log("Waiting for runtime license check");
406
- await runtimeLicenseCheckPromise;
407
- commercialLicense = hasCommercialLicense();
408
- }
409
- if (commercialLicense) return;
410
- nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
411
- nonCommercialContainer.style.opacity = "1";
412
- }
382
+ // private async handleRuntimeLicense(loadingElement: HTMLElement) {
383
+ // // First check if we have a commercial license
384
+ // let commercialLicense = hasCommercialLicense();
385
+ // // if it's the case then we don't need to perform a runtime check
386
+ // if (commercialLicense) return;
387
+
388
+ // // If we don't have a commercial license, then we need to display our message
389
+ // if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense);
390
+ // const nonCommercialContainer = document.createElement("div");
391
+ // nonCommercialContainer.style.paddingTop = ".6em";
392
+ // nonCommercialContainer.style.fontSize = ".8em";
393
+ // nonCommercialContainer.style.textTransform = "uppercase";
394
+ // nonCommercialContainer.innerText = "NEEDLE ENGINE NON COMMERCIAL VERSION\nCLICK HERE TO GET A LICENSE";
395
+ // nonCommercialContainer.style.cursor = "pointer";
396
+ // nonCommercialContainer.style.userSelect = "none";
397
+ // nonCommercialContainer.style.textAlign = "center";
398
+ // nonCommercialContainer.style.pointerEvents = "all";
399
+ // nonCommercialContainer.addEventListener("click", () => window.open("https://needle.tools/pricing", "_self"));
400
+ // nonCommercialContainer.style.opacity = "0";
401
+ // loadingElement.appendChild(nonCommercialContainer);
402
+
403
+ // // Use the runtime license check
404
+ // if (!isDevEnvironment() && runtimeLicenseCheckPromise) {
405
+ // if (debugLicense) console.log("Waiting for runtime license check");
406
+ // await runtimeLicenseCheckPromise;
407
+ // commercialLicense = hasCommercialLicense();
408
+ // }
409
+ // if (commercialLicense) return;
410
+ // nonCommercialContainer.style.transition = "opacity .5s ease-in-out";
411
+ // nonCommercialContainer.style.opacity = "1";
412
+ // }
413
413
  }
@@ -38,14 +38,18 @@ const observedAttributes = [
38
38
  "loadstart",
39
39
  "progress",
40
40
  "loadfinished",
41
+
41
42
  "dracoDecoderPath",
42
43
  "dracoDecoderType",
43
44
  "ktx2DecoderPath",
45
+
44
46
  "tone-mapping",
45
47
  "tone-mapping-exposure",
46
48
  "background-blurriness",
47
49
  "background-color",
48
50
  "environment-intensity",
51
+
52
+ "focus-rect",
49
53
  ]
50
54
 
51
55
  // https://developers.google.com/web/fundamentals/web-components/customelements
@@ -95,7 +99,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
95
99
  if (value === null) this.removeAttribute("camera-controls");
96
100
  else this.setAttribute("camera-controls", value ? "true" : "false");
97
101
  }
98
-
102
+
99
103
 
100
104
  /**
101
105
  * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
@@ -331,17 +335,17 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
331
335
  if (debug) console.log("ktx2DecoderPath", newValue);
332
336
  setKtx2TranscoderPath(newValue);
333
337
  break;
334
-
338
+
335
339
  case "tonemapping":
336
340
  case "tone-mapping":
337
341
  case "tone-mapping-exposure":
338
342
  case "background-blurriness":
339
343
  case "background-color":
340
344
  case "environment-intensity":
341
- {
342
- this.applyAttributes();
343
- break;
344
- }
345
+ {
346
+ this.applyAttributes();
347
+ break;
348
+ }
345
349
  case "public-key": {
346
350
  if (newValue != PUBLIC_KEY)
347
351
  this.setPublicKey();
@@ -352,6 +356,25 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
352
356
  this.setVersion();
353
357
  break;
354
358
  }
359
+
360
+ case "focus-rect":
361
+ {
362
+ const focus_rect = this.getAttribute("focus-rect") as HTMLElement | string | null;
363
+ if (focus_rect && this._context) {
364
+ if (focus_rect === null) {
365
+ this._context.setCameraFocusRect(null);
366
+ }
367
+ else if (typeof focus_rect === "string" && focus_rect.length > 0) {
368
+ const element = document.querySelector(focus_rect);
369
+ this._context.setCameraFocusRect(element instanceof HTMLElement ? element : null);
370
+ }
371
+ else if (focus_rect instanceof HTMLElement) {
372
+ this._context.setCameraFocusRect(focus_rect);
373
+ }
374
+
375
+ }
376
+ }
377
+ break;
355
378
  }
356
379
  }
357
380
 
@@ -570,6 +593,10 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
570
593
  this._context.renderer.setClearColor(rgbaColor, rgbaColor.alpha);
571
594
  this.context.scene.background = null;
572
595
  }
596
+ // HACK: if we set background-color to a color and then back to null we want the background-image attribute to re-apply
597
+ else if (this.getAttribute("background-image")) {
598
+ this.setAttribute("background-image", this.getAttribute("background-image")!);
599
+ }
573
600
  }
574
601
  }
575
602
 
@@ -993,11 +993,50 @@ export class OrbitControls extends Behaviour implements ICameraController {
993
993
 
994
994
  // Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
995
995
  // Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
996
+
996
997
  /**
997
998
  * Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
999
+ * @param options The options for fitting the camera. Use to provide objects to fit to, fit direction and size and other settings.
998
1000
  */
999
- fitCamera(options?: OrbitFitCameraOptions): void {
1001
+ fitCamera(options?: OrbitFitCameraOptions);
1002
+ /** @deprecated Use fitCamera(options) */
1003
+ fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
1004
+ fitCamera(objectsOrOptions?: Object3D | Array<Object3D> | OrbitFitCameraOptions, options?: OrbitFitCameraOptions): void {
1005
+
1006
+
1007
+ let objects: Object3D | Array<Object3D> | undefined = undefined;
1008
+ // If the user passed in an array as first argument
1009
+ if (Array.isArray(objectsOrOptions)) {
1010
+ objects = objectsOrOptions;
1011
+ }
1012
+ // If the user passed in an object as first argument
1013
+ else if (objectsOrOptions && "type" in objectsOrOptions) {
1014
+ objects = objectsOrOptions;
1015
+ }
1016
+ // If the user passed in an object as first argument and options as second argument
1017
+ else if (objectsOrOptions && typeof objectsOrOptions === "object") {
1018
+ if (!(objectsOrOptions instanceof Object3D) && !Array.isArray(objectsOrOptions)) {
1019
+ options = objectsOrOptions;
1020
+ objects = options.objects;
1021
+ }
1022
+ }
1023
+ // Ensure objects are setup correctly
1024
+ if (objects && !Array.isArray(objects)) {
1025
+ objects = [objects];
1026
+ }
1027
+ if (!Array.isArray(objects) || objects && objects.length <= 0) {
1028
+ objects = this.context.scene.children;
1029
+ }
1030
+
1031
+ // Make sure there's anything to fit to
1032
+ if (!Array.isArray(objects) || objects.length <= 0) {
1033
+ console.warn("No objects to fit camera to...");
1034
+ return;
1035
+ }
1036
+
1037
+
1000
1038
  const res = fitCamera({
1039
+ objects: [...objects],
1001
1040
  ...options,
1002
1041
  autoApply: false,
1003
1042
  context: this.context,
@@ -358,8 +358,11 @@ export class Renderer extends Behaviour implements IRenderer {
358
358
  //@ts-ignore
359
359
  get sharedMaterials(): SharedMaterialArray {
360
360
 
361
+ // @ts-ignore (original materials will be set during deserialization)
362
+ if(this._originalMaterials === undefined) return null;
363
+
361
364
  // @ts-ignore during deserialization code might access this property *before* the setter and then create an empty array
362
- if (!this.__didAwake) return null;
365
+ if (this.__isDeserializing === true) return null;
363
366
 
364
367
  if (!this._sharedMaterials || !this._sharedMaterials.is(this)) {
365
368
  if (!this._originalMaterials) this._originalMaterials = [];
@@ -725,6 +728,8 @@ export class Renderer extends Behaviour implements IRenderer {
725
728
  // If the material has a envMap and is NOT using a reflection probe we set the envMap to the scene environment
726
729
  if (mat && "envMap" in mat && "envMapIntensity" in mat && !ReflectionProbe.isUsingReflectionProbe(mat)) {
727
730
  mat.envMap = this.context.scene.environment;
731
+ mat.envMapIntensity = this.context.scene.environmentIntensity;
732
+ mat.envMapRotation = this.context.scene.environmentRotation;
728
733
  }
729
734
  }
730
735
  }
@@ -8,7 +8,7 @@ import { syncField } from "../engine/engine_networking_auto.js";
8
8
  import { loadPMREM } from "../engine/engine_pmrem.js";
9
9
  import { serializable } from "../engine/engine_serialization_decorator.js";
10
10
  import { type IContext } from "../engine/engine_types.js";
11
- import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback } from "../engine/engine_utils.js";
11
+ import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback, toSourceId } from "../engine/engine_utils.js";
12
12
  import { registerObservableAttribute } from "../engine/webcomponents/needle-engine.extras.js";
13
13
  import { Camera, ClearFlags } from "./Camera.js";
14
14
  import { Behaviour, GameObject } from "./Component.js";
@@ -27,15 +27,34 @@ function createRemoteSkyboxComponent(context: IContext, url: string, skybox: boo
27
27
  }
28
28
 
29
29
  const remote = new RemoteSkybox();
30
+ remote.sourceId = toSourceId(url);
30
31
  remote.allowDrop = false;
31
32
  remote.allowNetworking = false;
32
33
  remote.background = skybox;
33
34
  remote.environment = environment;
34
35
  GameObject.addComponent(context.scene, remote);
35
36
  const urlChanged = newValue => {
36
- if (typeof newValue !== "string") return;
37
- if (debug) console.log(attribute, "CHANGED TO", newValue)
38
- remote.setSkybox(newValue);
37
+ if (debug) console.log(attribute, "CHANGED TO", newValue);
38
+ if (newValue) {
39
+ if (typeof newValue !== "string") {
40
+ console.warn("Invalid attribute value for " + attribute);
41
+ return;
42
+ }
43
+ remote.setSkybox(newValue);
44
+ }
45
+ else {
46
+ if (remote.sourceId) {
47
+ if (environment) {
48
+ if (!context.sceneLighting.internalEnableReflection(remote.sourceId)) {
49
+ context.scene.environment = null;
50
+ }
51
+ }
52
+ if (skybox) {
53
+ const skybox = context.lightmaps.tryGetSkybox(remote.sourceId);
54
+ context.scene.background = skybox;
55
+ }
56
+ }
57
+ }
39
58
  };
40
59
  addAttributeChangeCallback(context.domElement, attribute, urlChanged);
41
60
  remote.addEventListener("destroy", () => {
@@ -50,7 +69,7 @@ ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, (args) => {
50
69
  const context = args.context;
51
70
  const backgroundImage = context.domElement.getAttribute("background-image");
52
71
  const environmentImage = context.domElement.getAttribute("environment-image");
53
-
72
+
54
73
  if (backgroundImage) {
55
74
  if (debug) console.log("Creating RemoteSkybox to load background " + backgroundImage);
56
75
  // if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
@@ -246,8 +265,8 @@ export class RemoteSkybox extends Behaviour {
246
265
  envMap.needsUpdate = true;
247
266
  }
248
267
 
249
- if(this.destroyed) return;
250
- if(!this.context) {
268
+ if (this.destroyed) return;
269
+ if (!this.context) {
251
270
  console.warn("RemoteSkybox: Context is not available - can not apply skybox.");
252
271
  return;
253
272
  }
@@ -208,6 +208,7 @@ export { ClickThrough } from "../web/Clickthrough.js";
208
208
  export { CursorFollow } from "../web/CursorFollow.js";
209
209
  export { HoverAnimation } from "../web/HoverAnimation.js";
210
210
  export { ScrollFollow } from "../web/ScrollFollow.js";
211
+ export { ViewBox } from "../web/ViewBox.js";
211
212
  export { Avatar } from "../webxr/Avatar.js";
212
213
  export { XRControllerFollow } from "../webxr/controllers/XRControllerFollow.js";
213
214
  export { XRControllerModel } from "../webxr/controllers/XRControllerModel.js";
@@ -66,10 +66,19 @@ export class PlayableDirector extends Behaviour {
66
66
  this.createTrackFunctions[type] = fn;
67
67
  }
68
68
 
69
+ /**
70
+ * The timeline asset that is played by this director.
71
+ */
69
72
  playableAsset?: Models.TimelineAssetModel;
73
+
70
74
  /** Set to true to start playing the timeline when the scene starts */
71
75
  @serializable()
72
76
  playOnAwake?: boolean;
77
+
78
+ /**
79
+ * Determines how the timeline behaves when it reaches the end of its duration.
80
+ * @default DirectorWrapMode.Loop
81
+ */
73
82
  @serializable()
74
83
  extrapolationMode: DirectorWrapMode = DirectorWrapMode.Loop;
75
84
 
@@ -92,4 +92,12 @@ export declare class SignalMarkerModel extends MarkerModel {
92
92
  asset: string;
93
93
  }
94
94
 
95
- export type ScrollMarkerModel = MarkerModel & { selector: string };
95
+ /**
96
+ * Marker with a name, used for scroll-driven timelines. It is used together with elements in your HTML to define what time in the timeline should be active when the element is in the scroll view.
97
+ *
98
+ * @example Mark html elements to define scroll positions
99
+ * ```html
100
+ * <div data-timeline-marker>...</div>
101
+ * ```
102
+ */
103
+ export type ScrollMarkerModel = MarkerModel & { name: string };
@@ -175,8 +175,10 @@ export class AnimationTrackHandler extends TrackHandler {
175
175
  // which means we want to notify the object that it's not animated anymore
176
176
  // and the animator can then take over
177
177
  onStateChanged() {
178
- if (this._animator)
179
- setObjectAnimated(this._animator.gameObject, this, this.director.isPlaying);
178
+ if (this._animator) {
179
+ // We can not check the *isPlaying* state here because the timeline might be paused and evaluated by e.g. ScrollFollow
180
+ setObjectAnimated(this._animator.gameObject, this, this.director.enabled && this.director.weight > 0);
181
+ }
180
182
  }
181
183
 
182
184
  createHooks(clipModel: Models.AnimationClipModel, clip) {
@@ -1,5 +1,6 @@
1
1
  import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
+ import { isDevEnvironment } from "../../engine/debug/index.js";
3
4
  import { serializable } from "../../engine/engine_serialization.js";
4
5
  import { lookAtObject } from "../../engine/engine_three_utils.js";
5
6
  import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
@@ -42,7 +43,10 @@ export class LookAt extends Behaviour implements UsdzBehaviour {
42
43
  /** @internal */
43
44
  onBeforeRender(): void {
44
45
  let target: Object3D | null | undefined = this.target;
45
- if (!target) target = this.context.mainCamera;
46
+ if (!target) {
47
+ target = this.context.mainCamera;
48
+ if (isDevEnvironment()) console.warn(`[LookAt] No target set on ${this.name}, using main camera as target.`);
49
+ }
46
50
  if (!target) return;
47
51
 
48
52
  let copyTargetRotation = this.copyTargetRotation;
@@ -1,12 +1,21 @@
1
1
  import { NEPointerEvent } from "../../engine/engine_input.js";
2
2
  import { onStart } from "../../engine/engine_lifecycle_api.js";
3
+ import { addAttributeChangeCallback } from "../../engine/engine_utils.js";
3
4
  import { Behaviour } from "../Component.js";
4
5
 
5
6
  // Automatically add ClickThrough component if "clickthrough" attribute is present on the needle-engine element
6
7
  onStart(ctx => {
7
8
  const attribute = ctx.domElement.getAttribute("clickthrough");
8
- if (attribute !== null && attribute !== "0" && attribute !== "false") {
9
- ctx.scene.addComponent(ClickThrough);
9
+ if (clickthroughEnabled(attribute)) {
10
+ const comp = ctx.scene.addComponent(ClickThrough);
11
+ addAttributeChangeCallback(ctx.domElement, "clickthrough", () => {
12
+ const attribute = ctx.domElement.getAttribute("clickthrough");
13
+ comp.enabled = clickthroughEnabled(attribute);
14
+ });
15
+ }
16
+
17
+ function clickthroughEnabled(val: string | null) {
18
+ return val !== null && val !== "0" && val !== "false";
10
19
  }
11
20
  });
12
21