@@ -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(
|
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
|
-
|
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
|
@@ -57,11 +57,10 @@
|
|
57
57
|
return;
|
58
58
|
}
|
59
59
|
|
60
|
-
// check if
|
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) {
|
@@ -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
|
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;
|
@@ -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 =
|
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;
|
@@ -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
|
144
|
-
|
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
|
431
|
-
if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" ||
|
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 (
|
456
|
+
if (requiredParent != context.arOverlayElement) {
|
437
457
|
context.arOverlayElement.appendChild(this);
|
438
458
|
}
|
439
459
|
}
|
440
|
-
else if (
|
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 (!
|
476
|
-
if (
|
477
|
-
|
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
|
-
|
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;
|
@@ -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
|
}
|
@@ -856,7 +856,7 @@
|
|
856
856
|
if (this.context.mainCamera) {
|
857
857
|
|
858
858
|
const isLowPerformanceDevice = isMobileDevice();
|
859
|
-
const lod_0_threshold = isLowPerformanceDevice ? .
|
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?.
|
876
|
-
meshDensity = (Math.log2(lodsInfo.density || 0) / 2) -
|
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();
|