Needle Engine

Changes between version 3.36.6-beta and 3.36.6
Files changed (7) hide show
  1. src/engine/webcomponents/buttons.ts +36 -4
  2. src/engine-components/CameraUtils.ts +2 -3
  3. src/engine/engine_license.ts +1 -1
  4. src/engine/extensions/NEEDLE_progressive.ts +5 -3
  5. src/engine/webcomponents/needle menu/needle-menu.ts +38 -19
  6. src/engine-components/NeedleMenu.ts +9 -0
  7. src/engine-components/Renderer.ts +4 -3
src/engine/webcomponents/buttons.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { isDevEnvironment } from "../debug/debug.js";
1
2
  import { IContext } from "../engine_types.js";
2
- import { generateQRCode } from "../engine_utils.js";
3
+ import { generateQRCode, isMobileDevice } from "../engine_utils.js";
3
4
  import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
4
5
  import { getIconElement } from "./icons.js";
5
6
 
@@ -27,11 +28,26 @@
27
28
 
28
29
 
29
30
  private _fullscreenButton?: HTMLButtonElement;
31
+
32
+ /**
33
+ * Get the fullscreen button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createFullscreenButton} to get or create it
34
+ */
35
+ get fullscreenButton() {
36
+ return this._fullscreenButton;
37
+ }
38
+
30
39
  /** Create a fullscreen button (or return the existing one if it already exists) */
31
- createFullscreenButton(_ctx: IContext) {
40
+ createFullscreenButton(ctx: IContext) : HTMLButtonElement | null {
32
41
  if (this._fullscreenButton) {
33
42
  return this._fullscreenButton;
34
43
  }
44
+
45
+ // check for fullscreen support
46
+ if (!document.fullscreenEnabled) {
47
+ if (isDevEnvironment()) console.warn("NeedleMenu: Fullscreen button could not be created, device doesn't support the Fullscreen API");
48
+ return null;
49
+ }
50
+
35
51
  const button = document.createElement("button");
36
52
  this._fullscreenButton = button;
37
53
  button.classList.add("fullscreen-button");
@@ -43,7 +59,10 @@
43
59
  if (document.fullscreenElement) {
44
60
  document.exitFullscreen();
45
61
  } else {
46
- document.documentElement.requestFullscreen();
62
+ if("webkitRequestFullscreen" in ctx.domElement && typeof ctx.domElement["webkitRequestFullscreen"] === "function")
63
+ ctx.domElement["webkitRequestFullscreen"]();
64
+ else if ("requestFullscreen" in ctx.domElement)
65
+ ctx.domElement.requestFullscreen();
47
66
  }
48
67
  };
49
68
  document.addEventListener("fullscreenchange", () => {
@@ -68,6 +87,10 @@
68
87
  }
69
88
 
70
89
  private _muteButton?: HTMLButtonElement;
90
+
91
+ /** Get the mute button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createMuteButton} to get or create it */
92
+ get muteButton() { return this._muteButton; }
93
+
71
94
  /** Create a mute button (or return the existing one if it already exists) */
72
95
  createMuteButton(ctx: IContext) {
73
96
  if (this._muteButton) {
@@ -112,6 +135,12 @@
112
135
 
113
136
 
114
137
  private _qrButton?: HTMLButtonElement;
138
+
139
+ /**
140
+ * Get the QR code button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createQRCode} to get or create it
141
+ */
142
+ get qrButton() { return this._qrButton; }
143
+
115
144
  /** Create a QR code button (or return the existing one if it already exists)
116
145
  * The QR code button will show a QR code that can be scanned to open the current page on a phone
117
146
  * The QR code will be generated with the current URL when the button is clicked
@@ -127,8 +156,10 @@
127
156
  qrCodeButton.prepend(getIconElement("qr_code"));
128
157
  qrCodeButton.title = "Scan this QR code with your phone to open this page";
129
158
  this.hideElementDuringXRSession(qrCodeButton);
159
+ if (isMobileDevice()) {
160
+ qrCodeButton.style.display = "none";
161
+ }
130
162
 
131
-
132
163
  const qrCodeContainer = document.createElement("div");
133
164
  qrCodeContainer.style.position = "fixed";
134
165
  qrCodeContainer.style.display = "inline-block";
@@ -148,6 +179,7 @@
148
179
  showQRCode();
149
180
  });
150
181
 
182
+
151
183
  /** shows the QRCode near the button */
152
184
  async function showQRCode() {
153
185
  // generate the qr code when the button is clicked
src/engine-components/CameraUtils.ts CHANGED
@@ -57,11 +57,10 @@
57
57
  return;
58
58
  }
59
59
 
60
- // check if the <needle-engine controls> is not set to false
60
+ // check if <needle-engine camera-controls> attribute is present or enabled
61
61
  const engineElement = evt.context.domElement as NeedleEngineHTMLElement
62
+ if (engineElement?.cameraControls == true) {
62
63
 
63
- if (engineElement?.cameraControls != false) {
64
-
65
64
  // Check if something else already acts as a camera controller
66
65
  const existing = getCameraController(evt.context.mainCamera);
67
66
  if (existing?.isCameraController == true) {
src/engine/engine_license.ts CHANGED
@@ -133,7 +133,7 @@
133
133
  text-shadow: 0 0 2px black;
134
134
  `;
135
135
  const expectedTextStyle = text.style.cssText;
136
- const forbiddenText = applicationForbiddenText?.length > 1 ? applicationForbiddenText : "This web application has been blacklisted. You might be in violation of the Needle Engine terms of use.<br/>Please contact the Needle support if you think this is a mistake.";
136
+ const forbiddenText = applicationForbiddenText?.length > 1 ? applicationForbiddenText : "This web application has been paused.<br/>You might be in violation of the Needle Engine terms of use.<br/>Please contact the Needle support if you think this is a mistake.";
137
137
  text.innerHTML = forbiddenText;
138
138
  setInterval(() => {
139
139
  if (text.innerHTML !== forbiddenText) text.innerHTML = forbiddenText;
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -593,13 +593,12 @@
593
593
 
594
594
  const parser = gltf.parser;
595
595
  if (debugverbose) console.log("Loading finished " + lod_url, ext.guid);
596
- let index = -1;
596
+ let index = 0;
597
597
 
598
598
  if (gltf.parser.json.textures) {
599
599
  let found = false;
600
600
  for (const tex of gltf.parser.json.textures) {
601
601
  // find the texture index
602
- index++;
603
602
  if (tex?.extensions) {
604
603
  const other: NEEDLE_progressive_model = tex?.extensions[EXTENSION_NAME];
605
604
  if (other?.guid) {
@@ -609,6 +608,7 @@
609
608
  }
610
609
  }
611
610
  }
611
+ index++;
612
612
  }
613
613
  if (found) {
614
614
  let tex = await parser.getDependency("texture", index) as Texture;
@@ -622,11 +622,12 @@
622
622
  }
623
623
  }
624
624
 
625
+ index = 0;
626
+
625
627
  if (gltf.parser.json.meshes) {
626
628
  let found = false;
627
629
  for (const mesh of gltf.parser.json.meshes) {
628
630
  // find the mesh index
629
- index++;
630
631
  if (mesh?.extensions) {
631
632
  const other: NEEDLE_progressive_model = mesh?.extensions[EXTENSION_NAME];
632
633
  if (other?.guid) {
@@ -636,6 +637,7 @@
636
637
  }
637
638
  }
638
639
  }
640
+ index++;
639
641
  }
640
642
  if (found) {
641
643
  const mesh = await parser.getDependency("mesh", index) as Mesh | Group;
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -119,6 +119,24 @@
119
119
  this._spatialMenu.setEnabled(enabled);
120
120
  }
121
121
 
122
+ /**
123
+ * Call to add or remove a button to the menu to show a QR code for the current page
124
+ * If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked.
125
+ */
126
+ showQRCodeButton(enabled: boolean): HTMLButtonElement | null {
127
+ if (!enabled) {
128
+ const button = ButtonsFactory.getOrCreate().qrButton;
129
+ if (button) button.style.display = "none";
130
+ return button ?? null;
131
+ }
132
+ else {
133
+ const button = ButtonsFactory.getOrCreate().createQRCode();
134
+ button.style.display = "";
135
+ this._menu.appendChild(button);
136
+ return button;
137
+ }
138
+ }
139
+
122
140
  /** Call to add or remove a button to the menu to mute or unmute the application
123
141
  * Clicking the button will mute or unmute the application
124
142
  */
@@ -140,10 +158,12 @@
140
158
  return;
141
159
  }
142
160
  this._fullscreenButton = ButtonsFactory.getOrCreate().createFullscreenButton(this._context);
143
- this._fullscreenButton.setAttribute("priority", "150");
144
- this._menu.appendChild(this._fullscreenButton);
161
+ if (this._fullscreenButton) {
162
+ this._fullscreenButton.setAttribute("priority", "150");
163
+ this._menu.appendChild(this._fullscreenButton);
164
+ }
145
165
  }
146
- private _fullscreenButton?: HTMLButtonElement;
166
+ private _fullscreenButton?: HTMLButtonElement | null;
147
167
 
148
168
 
149
169
 
@@ -427,17 +447,17 @@
427
447
  this.onChangeDetected(mutations);
428
448
 
429
449
  // ensure the menu is not hidden or removed
430
- const parent = this?.parentNode;
431
- if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || parent != this._domElement?.shadowRoot) {
450
+ const requiredParent = this?.parentNode;
451
+ if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || requiredParent != this._domElement?.shadowRoot) {
432
452
  if (!hasProLicense()) {
433
453
  clearInterval(showInterval);
434
454
  showInterval = setInterval(() => {
435
455
  if (context?.isInAR && context.arOverlayElement) {
436
- if (parent != context.arOverlayElement) {
456
+ if (requiredParent != context.arOverlayElement) {
437
457
  context.arOverlayElement.appendChild(this);
438
458
  }
439
459
  }
440
- else if (parent != this._domElement?.shadowRoot)
460
+ else if (this.parentNode != this._domElement?.shadowRoot)
441
461
  this._domElement?.shadowRoot?.appendChild(this);
442
462
  this.style.display = "flex";
443
463
  this.style.visibility = "visible";
@@ -472,9 +492,11 @@
472
492
  private _userRequestedLogoVisible?: boolean = undefined;
473
493
  showNeedleLogo(visible: boolean) {
474
494
  this._userRequestedLogoVisible = visible;
475
- if (!hasProLicense() || debugNonCommercial) {
476
- if (isDevEnvironment()) console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
477
- return;
495
+ if (!visible) {
496
+ if (!hasProLicense() || debugNonCommercial) {
497
+ if (isDevEnvironment()) console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
498
+ return;
499
+ }
478
500
  }
479
501
  this.logoContainer.style.display = visible ? "" : "none";
480
502
  }
@@ -543,15 +565,7 @@
543
565
  }
544
566
 
545
567
  private onOptionsChildrenChanged(_mut: MutationRecord) {
546
- let anyVisibleOptions = false;
547
- for (let i = 0; i < this.options.children.length; i++) {
548
- const child = this.options.children[i] as HTMLElement;
549
- if (child.style.display != "none") {
550
- anyVisibleOptions = true;
551
- break;
552
- }
553
- }
554
- this.logoContainer.classList.toggle("any-options", anyVisibleOptions);
568
+ this.logoContainer.classList.toggle("any-options", this.hasAnyVisibleOptions);
555
569
  this.handleSizeChange();
556
570
 
557
571
  if (_mut.type === "childList") {
@@ -592,12 +606,17 @@
592
606
  } else {
593
607
  this.root.style.display = "none";
594
608
  }
609
+ this.logoContainer.classList.toggle("any-options", this.hasAnyVisibleOptions);
595
610
  }
596
611
 
597
612
  /** @returns true if we have any content OR a logo */
598
613
  get hasAnyContent() {
599
614
  // is the logo visible?
600
615
  if (this.logoContainer.style.display != "none") return true;
616
+ if(this.hasAnyVisibleOptions) return true;
617
+ return false;
618
+ }
619
+ get hasAnyVisibleOptions() {
601
620
  // do we have any visible buttons?
602
621
  for (let i = 0; i < this.options.children.length; i++) {
603
622
  const child = this.options.children[i] as HTMLElement;
src/engine-components/NeedleMenu.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { serializable } from '../engine/engine_serialization.js';
2
+ import { isMobileDevice } from '../engine/engine_utils.js';
2
3
  import { Behaviour } from './Component.js';
3
4
 
4
5
  /**
@@ -25,6 +26,9 @@
25
26
  @serializable()
26
27
  createMuteButton: boolean = true;
27
28
 
29
+ @serializable()
30
+ createQRCodeButton: boolean = true;
31
+
28
32
  onEnable() {
29
33
  this.applyOptions();
30
34
  }
@@ -38,6 +42,11 @@
38
42
  if (this.createMuteButton)
39
43
  this.context.menu.showAudioPlaybackOption(true);
40
44
  this.context.menu.showSpatialMenu(this.showSpatialMenu);
45
+ if (this.createQRCodeButton) {
46
+ if (!isMobileDevice()) {
47
+ this.context.menu.showQRCodeButton(true);
48
+ }
49
+ }
41
50
  }
42
51
 
43
52
  }
src/engine-components/Renderer.ts CHANGED
@@ -856,7 +856,7 @@
856
856
  if (this.context.mainCamera) {
857
857
 
858
858
  const isLowPerformanceDevice = isMobileDevice();
859
- const lod_0_threshold = isLowPerformanceDevice ? .6 : .4;
859
+ const lod_0_threshold = isLowPerformanceDevice ? .8 : .6;
860
860
 
861
861
 
862
862
  // TODO: we should save the LOD level in the shared mesh and not just calculate one level per renderer
@@ -872,8 +872,9 @@
872
872
  // TODO: the mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
873
873
  const lodsInfo = NEEDLE_progressive.getMeshLODInformation(mesh.geometry);
874
874
  // TODO: the substraction here is not clear - it should be some sort of mesh density value
875
- if (lodsInfo?.density != undefined)
876
- meshDensity = (Math.log2(lodsInfo.density || 0) / 2) - 6;
875
+ if (lodsInfo?.lods && lodsInfo?.lods.length > 0)
876
+ meshDensity = (Math.log2(lodsInfo.lods[0].density || 0) / 2) - 8;
877
+ else if(debugProgressiveLoading) console.warn("No LOD information found...", mesh.name);
877
878
 
878
879
  // TODO: we can skip all this if we dont have any LOD information - we can ask the progressive extension for that
879
880
  const frustum = this.context.mainCameraComponent?.getFrustum();