@needle-tools/engine 4.10.0-next.f0ec242 → 4.10.0

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 (124) hide show
  1. package/CHANGELOG.md +7 -3
  2. package/README.md +2 -1
  3. package/components.needle.json +1 -1
  4. package/dist/needle-engine.bundle-BSq-d_16.min.js +1652 -0
  5. package/dist/{needle-engine.bundle-dgNq9Vsa.umd.cjs → needle-engine.bundle-C2kVfQq6.umd.cjs} +153 -140
  6. package/dist/{needle-engine.bundle-BC-0Ex9m.js → needle-engine.bundle-CIuhf7-t.js} +7388 -7113
  7. package/dist/needle-engine.d.ts +15 -15
  8. package/dist/needle-engine.js +259 -257
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
  12. package/dist/vendor-DPCU8cUF.min.js +1121 -0
  13. package/dist/vendor-MBoqSyFm.js +16240 -0
  14. package/lib/engine/codegen/register_types.js +2 -0
  15. package/lib/engine/codegen/register_types.js.map +1 -1
  16. package/lib/engine/engine_camera.d.ts +7 -1
  17. package/lib/engine/engine_camera.js +46 -6
  18. package/lib/engine/engine_camera.js.map +1 -1
  19. package/lib/engine/engine_context.d.ts +6 -0
  20. package/lib/engine/engine_context.js +48 -9
  21. package/lib/engine/engine_context.js.map +1 -1
  22. package/lib/engine/engine_gizmos.d.ts +11 -10
  23. package/lib/engine/engine_gizmos.js +24 -10
  24. package/lib/engine/engine_gizmos.js.map +1 -1
  25. package/lib/engine/engine_license.js +1 -1
  26. package/lib/engine/engine_license.js.map +1 -1
  27. package/lib/engine/engine_lightdata.d.ts +3 -3
  28. package/lib/engine/engine_lightdata.js +10 -10
  29. package/lib/engine/engine_lightdata.js.map +1 -1
  30. package/lib/engine/engine_physics_rapier.js +4 -0
  31. package/lib/engine/engine_physics_rapier.js.map +1 -1
  32. package/lib/engine/engine_scenelighting.d.ts +1 -1
  33. package/lib/engine/engine_scenelighting.js +4 -5
  34. package/lib/engine/engine_scenelighting.js.map +1 -1
  35. package/lib/engine/engine_utils.d.ts +3 -1
  36. package/lib/engine/engine_utils.js +11 -0
  37. package/lib/engine/engine_utils.js.map +1 -1
  38. package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
  39. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  40. package/lib/engine/extensions/extension_utils.js +1 -1
  41. package/lib/engine/extensions/extension_utils.js.map +1 -1
  42. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  43. package/lib/engine/webcomponents/logo-element.js +29 -5
  44. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  45. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  46. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  47. package/lib/engine/webcomponents/needle-engine.js +22 -0
  48. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  49. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  50. package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
  51. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  52. package/lib/engine/xr/NeedleXRController.d.ts +3 -3
  53. package/lib/engine/xr/NeedleXRController.js +28 -0
  54. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  55. package/lib/engine-components/CameraUtils.js +2 -1
  56. package/lib/engine-components/CameraUtils.js.map +1 -1
  57. package/lib/engine-components/Renderer.js +6 -1
  58. package/lib/engine-components/Renderer.js.map +1 -1
  59. package/lib/engine-components/Skybox.js +22 -4
  60. package/lib/engine-components/Skybox.js.map +1 -1
  61. package/lib/engine-components/codegen/components.d.ts +1 -0
  62. package/lib/engine-components/codegen/components.js +1 -0
  63. package/lib/engine-components/codegen/components.js.map +1 -1
  64. package/lib/engine-components/debug/LogStats.d.ts +1 -0
  65. package/lib/engine-components/debug/LogStats.js +1 -0
  66. package/lib/engine-components/debug/LogStats.js.map +1 -1
  67. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
  68. package/lib/engine-components/timeline/PlayableDirector.js +8 -1
  69. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  70. package/lib/engine-components/timeline/TimelineModels.d.ts +11 -1
  71. package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
  72. package/lib/engine-components/timeline/TimelineTracks.js +30 -25
  73. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  74. package/lib/engine-components/utils/LookAt.js +5 -1
  75. package/lib/engine-components/utils/LookAt.js.map +1 -1
  76. package/lib/engine-components/web/Clickthrough.js +10 -2
  77. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  78. package/lib/engine-components/web/ScrollFollow.d.ts +24 -0
  79. package/lib/engine-components/web/ScrollFollow.js +169 -42
  80. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  81. package/lib/engine-components/web/ViewBox.d.ts +43 -0
  82. package/lib/engine-components/web/ViewBox.js +258 -0
  83. package/lib/engine-components/web/ViewBox.js.map +1 -0
  84. package/lib/engine-components/web/index.d.ts +1 -0
  85. package/lib/engine-components/web/index.js +1 -0
  86. package/lib/engine-components/web/index.js.map +1 -1
  87. package/lib/engine-components-experimental/Presentation.d.ts +1 -0
  88. package/lib/engine-components-experimental/Presentation.js +1 -0
  89. package/lib/engine-components-experimental/Presentation.js.map +1 -1
  90. package/package.json +3 -2
  91. package/src/engine/codegen/register_types.ts +2 -0
  92. package/src/engine/engine_camera.ts +61 -9
  93. package/src/engine/engine_context.ts +50 -10
  94. package/src/engine/engine_gizmos.ts +37 -23
  95. package/src/engine/engine_license.ts +1 -1
  96. package/src/engine/engine_lightdata.ts +11 -11
  97. package/src/engine/engine_physics_rapier.ts +3 -0
  98. package/src/engine/engine_scenelighting.ts +5 -6
  99. package/src/engine/engine_utils.ts +12 -0
  100. package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  101. package/src/engine/extensions/extension_utils.ts +1 -1
  102. package/src/engine/webcomponents/logo-element.ts +29 -4
  103. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  104. package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
  105. package/src/engine/webcomponents/needle-engine.ts +33 -6
  106. package/src/engine/xr/NeedleXRController.ts +36 -4
  107. package/src/engine-components/CameraUtils.ts +1 -1
  108. package/src/engine-components/Renderer.ts +6 -1
  109. package/src/engine-components/Skybox.ts +26 -7
  110. package/src/engine-components/codegen/components.ts +1 -0
  111. package/src/engine-components/debug/LogStats.ts +1 -0
  112. package/src/engine-components/timeline/PlayableDirector.ts +10 -1
  113. package/src/engine-components/timeline/TimelineModels.ts +11 -1
  114. package/src/engine-components/timeline/TimelineTracks.ts +30 -25
  115. package/src/engine-components/utils/LookAt.ts +5 -1
  116. package/src/engine-components/web/Clickthrough.ts +11 -2
  117. package/src/engine-components/web/ScrollFollow.ts +205 -51
  118. package/src/engine-components/web/ViewBox.ts +278 -0
  119. package/src/engine-components/web/index.ts +2 -1
  120. package/src/engine-components-experimental/Presentation.ts +1 -0
  121. package/dist/needle-engine.bundle-BSh7dSEx.min.js +0 -1639
  122. package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
  123. package/dist/vendor-DU8tJyl_.js +0 -14366
  124. package/dist/vendor-JyrX4DVM.min.js +0 -1121
@@ -381,6 +381,14 @@ export function resolveUrl(source: SourceIdentifier | undefined, uri: string): s
381
381
  }
382
382
  return uri;
383
383
  }
384
+
385
+ export function toSourceId(src: string | null): SourceIdentifier | undefined {
386
+ if (!src) return undefined;
387
+ src = src.trim();
388
+ src = src.split("?")[0]?.split("#")[0];
389
+ return src;
390
+ }
391
+
384
392
  // export function getPath(glbLocation: SourceIdentifier | undefined, path: string) {
385
393
  // if (path && glbLocation && !path.includes("/")) {
386
394
  // // get directory of glb and prepend it to the audio file path
@@ -793,6 +801,7 @@ const mutationObserverMap = new WeakMap<HTMLElement, HtmlElementExtra>();
793
801
  /**
794
802
  * Register a callback when an {@link HTMLElement} attribute changes.
795
803
  * This is used, for example, by the Skybox component to watch for changes to the environment-* and skybox-* attributes.
804
+ * @returns A function that can be used to unregister the callback
796
805
  */
797
806
  export function addAttributeChangeCallback(domElement: HTMLElement, name: string, callback: AttributeChangeCallback) {
798
807
  if (!mutationObserverMap.get(domElement)) {
@@ -811,6 +820,9 @@ export function addAttributeChangeCallback(domElement: HTMLElement, name: string
811
820
  listeners.set(name, []);
812
821
  }
813
822
  listeners.get(name)!.push(callback);
823
+ return () => {
824
+ removeAttributeChangeCallback(domElement, name, callback);
825
+ }
814
826
  };
815
827
 
816
828
  /**
@@ -107,7 +107,7 @@ export class NEEDLE_lightmaps implements GLTFLoaderPlugin {
107
107
 
108
108
  private resolveTexture(entry: LightmapInfo, res: any) {
109
109
  const tex: Texture = res as unknown as Texture;
110
- if (debug) console.log("Lightmap loaded:", tex);
110
+ if (debug) console.log("Light Texture loaded:", tex);
111
111
  if (tex?.isTexture) {
112
112
  if (!this.registry)
113
113
  console.log(LightmapType[entry.type], entry.pointer, tex);
@@ -89,7 +89,7 @@ function internalResolve(paths: DependencyInfo[], parser: GLTFParserWithCache, o
89
89
  for (let i = 0; i < val.length; i++) {
90
90
  const entry = val[i];
91
91
  const ext = resolveExtension(parser, entry);
92
- if (ext !== null) {
92
+ if (ext !== null && ext !== undefined) {
93
93
  if (typeof ext.then === "function")
94
94
  promises.push(ext.then(res => val[i] = res));
95
95
  else val[i] = ext;
@@ -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
 
@@ -23,10 +23,10 @@ const debugCustomGesture = getParam("debugcustomgesture");
23
23
  // let _didReceiveSelectStartEvent = false;
24
24
 
25
25
  // https://github.com/immersive-web/webxr-input-profiles/blob/4484a05e30bcd43fe86bb4e06b7a707861a26796/packages/registry/profiles/meta/meta-quest-touch-plus.json
26
- declare type ControllerAxes = "xr-standard-thumbstick";
27
- declare type StickName = "xr-standard-thumbstick";
26
+ declare type ControllerAxes = "xr-standard-thumbstick" | "xr-standard-touchpad";
27
+ declare type StickName = "xr-standard-thumbstick" | "xr-standard-touchpad";
28
28
  declare type Mapping = "xr-standard";
29
- declare type ComponentType = "button" | "thumbstick" | "squeeze";
29
+ declare type ComponentType = "button" | "thumbstick" | "squeeze" | "touchpad";
30
30
  declare type GamepadKey = "button" | "xAxis" | "yAxis";
31
31
 
32
32
  declare type NeedleXRControllerButtonName = ButtonName | "primary-button" | "primary";
@@ -407,6 +407,16 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
407
407
  gamepadStr += "\n[axes " + gp.axes.length + "]: " + gp.axes.map(a => a.toPrecision(1)).join(",");
408
408
  debugStr += "\n" + gamepadStr;
409
409
  }
410
+ if (this._layout) {
411
+ debugStr += "\nLayout: ";
412
+ for (const component of Object.keys(this._layout.components || {})) {
413
+ const val = this.getStick(component as StickName);
414
+ const indices = this._layout.components[component]?.gamepadIndices;
415
+ const indicesAsString = indices ? Object.entries(indices).map(e => e[0][0].toUpperCase() + e[0].slice(1) + "=" + e[1]).join(",") : "";
416
+ debugStr += `\n ${component}: ${this._layout.components[component]?.type} [${indicesAsString}] (${val.x.toPrecision(2)},${val.y.toPrecision(2)})`;
417
+ }
418
+ }
419
+
410
420
  Gizmos.DrawLabel(debugLabelPosition, debugStr, .006);
411
421
  }
412
422
 
@@ -730,6 +740,7 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
730
740
  if (componentModel?.gamepadIndices) {
731
741
  switch (componentModel.type) {
732
742
  case "thumbstick":
743
+ case "touchpad":
733
744
  if (this.inputSource.gamepad) {
734
745
  const xIndex = componentModel.gamepadIndices!.xAxis!;
735
746
  const yIndex = componentModel.gamepadIndices!.yAxis!;
@@ -760,7 +771,11 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
760
771
  this._isMetaQuestTouchController = this.profiles.includes("meta-quest-touch-plus") || this.profiles.includes("oculus-touch-v3");
761
772
 
762
773
  // Proper profile starting with v69 and browser 35.1
763
- this._isMxInk = this.profiles.includes("logitech-mx-ink")
774
+ this._isMxInk = this.profiles.includes("logitech-mx-ink");
775
+
776
+ // For debugging to see ALL available profiles
777
+ /** @ts-ignore */
778
+ // fetchProfilesList(DEFAULT_PROFILES_PATH).then(list => console.log("Available controller profiles", list));
764
779
 
765
780
  if (!this._layout) {
766
781
  // Ignore transient-pointer since we likely don't want to spawn a controller visual just for a temporary pointer.
@@ -780,6 +795,8 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
780
795
  res.assetPath || ""
781
796
  );
782
797
 
798
+ // const overrideProfile = await fetch(DEFAULT_PROFILES_PATH + "/htc-vive-focus-3/profile.json").then(r => r.json());
799
+
783
800
  const profile = res.profile as InputDeviceProfile;
784
801
  const layout = profile.layouts[this.inputSource.handedness];
785
802
  this._layout = layout;
@@ -791,6 +808,21 @@ C:${this.connected ? "x" : "-"} T:${this.isTracking ? "x" : "-"} Hand:${this.inp
791
808
  this._layout.gamepad[component.gamepadIndices!.button!] = key as XRControllerButtonName;
792
809
  }
793
810
  }
811
+
812
+ // If we have 4 axes and no thumbstick defined, we define thumbstick for axis 3+4
813
+ // This is a workaround for HTC Vive Focus 3 controllers, which have the profile for Vive Focus Plus...
814
+ // This workaround fixes it for HTC Vive Focus 3 but does not change anything for Vive Focus Plus controllers
815
+ if (this.profiles.length >= 1 && this.profiles[0] === "htc-vive-focus-plus") {
816
+ if (this.inputSource.gamepad && this.inputSource.gamepad.axes.length === 4 && !this._layout.components["xr-standard-thumbstick"]) {
817
+ this._layout.components["xr-standard-thumbstick"] = {
818
+ type: "thumbstick",
819
+ gamepadIndices: {
820
+ xAxis: 2,
821
+ yAxis: 3,
822
+ }
823
+ }
824
+ }
825
+ }
794
826
  }
795
827
  // if (debug) console.log(this._layout, this.inputSource);
796
828
  // debugger;
@@ -122,7 +122,7 @@ function createDefaultCameraControls(context: IContext, cam?: ICamera) {
122
122
  orbit.autoRotate = autoRotate != "0" && autoRotate?.toLowerCase() != "false";
123
123
  const autoRotateSpeed = Number.parseFloat(autoRotate || ".5");
124
124
  orbit.autoRotateSpeed = !isNaN(autoRotateSpeed) ? autoRotateSpeed : .5;
125
- console.log("Auto-rotate", orbit.autoRotate, "speed:", orbit.autoRotateSpeed);
125
+ if(debug) console.log("Auto-rotate", orbit.autoRotate, "speed:", orbit.autoRotateSpeed);
126
126
  const autoFit = context.domElement.getAttribute("auto-fit");
127
127
  orbit.autoFit = autoFit !== "0" && autoFit?.toLowerCase() != "false";
128
128
  orbit.autoTarget = true;
@@ -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";
@@ -4,6 +4,7 @@ import { Behaviour } from "../../engine-components/Component.js";
4
4
 
5
5
  const debug = getParam("logstats");
6
6
 
7
+ /** @internal */
7
8
  export class LogStats extends Behaviour {
8
9
 
9
10
  onEnable(): void {
@@ -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
 
@@ -106,7 +115,7 @@ export class PlayableDirector extends Behaviour {
106
115
  /** @internal */
107
116
  awake(): void {
108
117
  if (debug)
109
- console.log(this, this.playableAsset?.tracks);
118
+ console.log(this, this.playableAsset);
110
119
 
111
120
  this.rebuildGraph();
112
121
 
@@ -92,4 +92,14 @@ 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
+ * @link [Example Project using ScrollMarker](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/)
104
+ */
105
+ export type ScrollMarkerModel = MarkerModel & { name: string };