@@ -117,6 +117,8 @@
|
|
117
117
|
const wasKinematicKey = "_rigidbody-was-kinematic";
|
118
118
|
if (this._rigidbody?.[wasKinematicKey] === false) {
|
119
119
|
this._rigidbody.isKinematic = false;
|
120
|
+
this._rigidbody[wasKinematicKey] = undefined;
|
121
|
+
|
120
122
|
}
|
121
123
|
|
122
124
|
this._rigidbody = null;
|
@@ -412,7 +412,7 @@
|
|
412
412
|
}
|
413
413
|
}
|
414
414
|
if (typeof value === "string") {
|
415
|
-
if (serialized.endsWith(".gltf") || serialized.endsWith(".glb")) {
|
415
|
+
if (typeof serialized === "string" && (serialized.endsWith(".gltf") || serialized.endsWith(".glb"))) {
|
416
416
|
addLog(LogType.Warn, `<strong>Missing serialization for object reference!</strong>\n\nPlease change to: \n@serializable(AssetReference)\n${key}? : AssetReference;\n\nin script ${typeName}.ts\n<a href="https://docs.needle.tools/serializable" target="_blank">documentation</a>`);
|
417
417
|
console.warn(typeName, key, obj[key], obj);
|
418
418
|
continue;
|
@@ -46,6 +46,7 @@
|
|
46
46
|
const forwardPoint = lookFrom.sub(getWorldDirection(object));
|
47
47
|
forwardPoint.y = ypos;
|
48
48
|
object.lookAt(forwardPoint);
|
49
|
+
object.quaternion.multiply(flipYQuat);
|
49
50
|
}
|
50
51
|
|
51
52
|
// sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { serializable } from "../../engine/engine_serialization.js";
|
4
|
-
import {
|
4
|
+
import { lookAtObject } from "../../engine/engine_three_utils.js";
|
5
5
|
import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
|
6
6
|
import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
7
7
|
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
@@ -45,7 +45,12 @@
|
|
45
45
|
if (!target) target = this.context.mainCamera;
|
46
46
|
if (!target) return;
|
47
47
|
|
48
|
-
|
48
|
+
let copyTargetRotation = this.copyTargetRotation;
|
49
|
+
// Disable copy target rotation during VR/AR session: https://linear.app/needle/issue/NE-5634
|
50
|
+
if (this.context.isInVR || this.context.isInPassThrough) {
|
51
|
+
copyTargetRotation = false;
|
52
|
+
}
|
53
|
+
lookAtObject(this.gameObject, target, this.keepUpDirection, copyTargetRotation);
|
49
54
|
|
50
55
|
if (this.invertForward)
|
51
56
|
this.gameObject.quaternion.multiply(LookAt.flipYQuat);
|
@@ -55,7 +60,7 @@
|
|
55
60
|
createBehaviours(ext, model: USDObject, _context) {
|
56
61
|
if (model.uuid === this.gameObject.uuid) {
|
57
62
|
let alignmentTarget = model;
|
58
|
-
|
63
|
+
|
59
64
|
// not entirely sure why we need to do this - looks like LookAt with up vector doesn't work properly in
|
60
65
|
// QuickLook, so we need to introduce an empty parent and rotate the model by 90° around Y
|
61
66
|
if (this.keepUpDirection) {
|
@@ -72,9 +77,9 @@
|
|
72
77
|
const lookAt = new BehaviorModel("lookat " + this.name,
|
73
78
|
TriggerBuilder.sceneStartTrigger(),
|
74
79
|
ActionBuilder.lookAtCameraAction(
|
75
|
-
alignmentTarget,
|
76
|
-
undefined,
|
77
|
-
this.invertForward ? USDVec3.back : USDVec3.forward,
|
80
|
+
alignmentTarget,
|
81
|
+
undefined,
|
82
|
+
this.invertForward ? USDVec3.back : USDVec3.forward,
|
78
83
|
this.keepUpDirection ? USDVec3.up : USDVec3.zero
|
79
84
|
),
|
80
85
|
);
|
@@ -354,13 +354,21 @@
|
|
354
354
|
return null;
|
355
355
|
}
|
356
356
|
if (!mat.extensions || !mat.extensions[NEEDLE_TECHNIQUES_WEBGL_NAME]) {
|
357
|
-
if (debug) console.log(
|
357
|
+
if (debug) console.log(`Material ${index} does not use NEEDLE_techniques_webgl`);
|
358
358
|
return null;
|
359
359
|
}
|
360
|
+
if(debug) console.log(`Material ${index} uses NEEDLE_techniques_webgl`, mat);
|
360
361
|
const techniqueIndex = mat.extensions[NEEDLE_TECHNIQUES_WEBGL_NAME].technique;
|
361
|
-
if (techniqueIndex < 0)
|
362
|
+
if (techniqueIndex < 0) {
|
363
|
+
console.debug(`Material ${index} does not have a valid technique index`);
|
364
|
+
return null;
|
365
|
+
}
|
362
366
|
const shaders: SHADERDATA.ShaderData = this.parser.json.extensions[NEEDLE_TECHNIQUES_WEBGL_NAME];
|
363
|
-
if (!shaders)
|
367
|
+
if (!shaders) {
|
368
|
+
if(debug) console.error("Missing shader data", this.parser.json.extensions);
|
369
|
+
else console.debug("Missing custom shader data in parser.json.extensions");
|
370
|
+
return null;
|
371
|
+
}
|
364
372
|
if (debug) console.log(shaders);
|
365
373
|
const technique: SHADERDATA.Technique = shaders.techniques[techniqueIndex];
|
366
374
|
if (!technique) return null;
|
@@ -139,6 +139,9 @@
|
|
139
139
|
this._menu["previousParent"] = this._menu.parentNode;
|
140
140
|
this._context.arOverlayElement.appendChild(this._menu);
|
141
141
|
args.session.session.addEventListener("end", this.onExitXR);
|
142
|
+
|
143
|
+
// Close the foldout if it's open on entering AR
|
144
|
+
this._menu.closeFoldout();
|
142
145
|
}
|
143
146
|
}
|
144
147
|
|
@@ -367,8 +370,8 @@
|
|
367
370
|
white-space: nowrap;
|
368
371
|
transition: all 0.1s linear .02s;
|
369
372
|
border-radius: 0.8rem;
|
373
|
+
user-select: none;
|
370
374
|
}
|
371
|
-
|
372
375
|
:host .options > *:hover, ::slotted(*:hover) {
|
373
376
|
cursor: pointer;
|
374
377
|
color: black;
|
@@ -440,6 +443,7 @@
|
|
440
443
|
.compact-menu-button { display: none; }
|
441
444
|
/** And show it when we're in compact mode **/
|
442
445
|
.compact .compact-menu-button {
|
446
|
+
position: relative;
|
443
447
|
display: block;
|
444
448
|
background: none;
|
445
449
|
border: none;
|
@@ -449,6 +453,8 @@
|
|
449
453
|
padding: 0 .3rem;
|
450
454
|
padding-top: .2rem;
|
451
455
|
|
456
|
+
z-index: 100;
|
457
|
+
|
452
458
|
color: #000;
|
453
459
|
|
454
460
|
&:hover {
|
@@ -458,6 +464,14 @@
|
|
458
464
|
&:focus {
|
459
465
|
outline: 1px solid rgba(255,255,255,.5);
|
460
466
|
}
|
467
|
+
& .expanded-click-area {
|
468
|
+
position: absolute;
|
469
|
+
left: 0;
|
470
|
+
right: 0;
|
471
|
+
top: 10%;
|
472
|
+
bottom: 10%;
|
473
|
+
transform: scale(1.8);
|
474
|
+
}
|
461
475
|
}
|
462
476
|
.has-no-options .compact-menu-button {
|
463
477
|
display: none;
|
@@ -601,7 +615,9 @@
|
|
601
615
|
<span class="madewith notranslate">powered by</span>
|
602
616
|
</div>
|
603
617
|
</div>
|
604
|
-
<button class="compact-menu-button"
|
618
|
+
<button class="compact-menu-button">
|
619
|
+
<div class="expanded-click-area"></div>
|
620
|
+
</button>
|
605
621
|
</div>
|
606
622
|
`;
|
607
623
|
|
@@ -799,6 +815,13 @@
|
|
799
815
|
this.style.display = visible ? "flex" : "none";
|
800
816
|
}
|
801
817
|
|
818
|
+
/**
|
819
|
+
* If the menu is in compact mode and the foldout is currently open (to show all menu options) then this will close the foldout
|
820
|
+
*/
|
821
|
+
closeFoldout() {
|
822
|
+
this.root.classList.remove("open");
|
823
|
+
}
|
824
|
+
|
802
825
|
// private _root: ShadowRoot | null = null;
|
803
826
|
private readonly root: HTMLDivElement;
|
804
827
|
/** wraps the whole content */
|
@@ -881,26 +904,27 @@
|
|
881
904
|
this.handleSizeChange(undefined, true);
|
882
905
|
|
883
906
|
if (_mut.type === "childList") {
|
884
|
-
|
885
|
-
const now = Date.now();
|
886
|
-
// sort children by priority only when necessary
|
887
|
-
for (let i = 0; i < _mut.addedNodes.length; i++) {
|
888
|
-
const child = _mut.addedNodes[i] as HTMLElement;
|
889
|
-
const lastTime = this._didSort.get(child);
|
890
|
-
if (typeof lastTime === "number" && now - lastTime < 100) continue;
|
891
|
-
this._didSort.set(child, now);
|
892
|
-
needsSorting = true;
|
893
|
-
}
|
894
|
-
if (needsSorting) {
|
907
|
+
if (_mut.addedNodes.length > 0) {
|
895
908
|
const children = Array.from(this.options.children);
|
896
909
|
children.sort((a, b) => {
|
897
910
|
const p1 = parseInt(a.getAttribute("priority") || "0");
|
898
911
|
const p2 = parseInt(b.getAttribute("priority") || "0");
|
899
912
|
return p1 - p2;
|
900
913
|
});
|
901
|
-
|
902
|
-
|
914
|
+
let sortingChanged = false;
|
915
|
+
for (let i = 0; i < children.length; i++) {
|
916
|
+
const existing = this.options.children[i];
|
917
|
+
const child = children[i];
|
918
|
+
if (existing !== child) {
|
919
|
+
sortingChanged = true;
|
920
|
+
break;
|
921
|
+
}
|
903
922
|
}
|
923
|
+
if (sortingChanged) {
|
924
|
+
for (const child of children) {
|
925
|
+
this.options.appendChild(child);
|
926
|
+
}
|
927
|
+
}
|
904
928
|
}
|
905
929
|
}
|
906
930
|
}
|
@@ -8,13 +8,14 @@
|
|
8
8
|
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
9
9
|
import { getBoundingBox, getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
10
10
|
import type { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
11
|
-
import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
|
11
|
+
import { delay, getParam, isDesktop, isiOS, isQuest } from "../engine_utils.js";
|
12
12
|
import { invokeXRSessionEnd, invokeXRSessionStart } from "./events.js"
|
13
13
|
import { flipForwardMatrix, flipForwardQuaternion, ImplictXRRig } from "./internal.js";
|
14
14
|
import { NeedleXRController } from "./NeedleXRController.js";
|
15
15
|
import { NeedleXRSync } from "./NeedleXRSync.js";
|
16
16
|
import { SceneTransition } from "./SceneTransition.js";
|
17
17
|
import { SessionInfo, TemporaryXRContext } from "./TempXRContext.js";
|
18
|
+
import { InternalUSDZRegistry } from "./usdz.js";
|
18
19
|
import type { IXRRig } from "./XRRig.js";
|
19
20
|
|
20
21
|
const measure_SessionStartedMarker = "NeedleXRSession onStart";
|
@@ -368,6 +369,16 @@
|
|
368
369
|
* @param context The Needle Engine context to use
|
369
370
|
*/
|
370
371
|
static async start(mode: XRSessionMode, init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
|
372
|
+
|
373
|
+
// handle iOS platform where "immersive-ar" is not supported
|
374
|
+
// TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
|
375
|
+
if (mode === "immersive-ar" && isiOS()) {
|
376
|
+
if (InternalUSDZRegistry.exportAndOpen()) {
|
377
|
+
return null;
|
378
|
+
}
|
379
|
+
}
|
380
|
+
|
381
|
+
|
371
382
|
if (isDevEnvironment() && getParam("debugxrpreroom")) {
|
372
383
|
console.warn("Debug: Starting temporary XR session");
|
373
384
|
await TemporaryXRContext.start(mode, init || NeedleXRSession.getDefaultSessionInit(mode));
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -28,10 +28,20 @@
|
|
28
28
|
renderer.applySettings(obj);
|
29
29
|
const res = this.tryCreateOrAddInstance(obj, context, args);
|
30
30
|
if (res) {
|
31
|
-
NEEDLE_progressive.assignTextureLOD(res.renderer.material, 0);
|
32
|
-
// renderer.loadProgressiveMeshes(res.instancer.mesh, 0);
|
33
31
|
if (handlesArray === null) handlesArray = [];
|
34
32
|
handlesArray.push(res);
|
33
|
+
// load texture lods
|
34
|
+
NEEDLE_progressive.assignTextureLOD(res.renderer.material, 0);
|
35
|
+
|
36
|
+
// Load mesh lods
|
37
|
+
for (const mesh of renderer.sharedMeshes) {
|
38
|
+
const geometry = mesh.geometry;
|
39
|
+
NEEDLE_progressive.assignMeshLOD(mesh, 0).then(lod => {
|
40
|
+
if (lod && renderer.activeAndEnabled && geometry != lod) {
|
41
|
+
res.setGeometry(lod);
|
42
|
+
}
|
43
|
+
})
|
44
|
+
}
|
35
45
|
}
|
36
46
|
|
37
47
|
else if (level <= 0 && obj.type !== "Mesh") {
|
@@ -192,11 +202,11 @@
|
|
192
202
|
setGeometry(geo: BufferGeometry) {
|
193
203
|
if (this.__instanceIndex < 0) return false;
|
194
204
|
if (this.vertexCount > this.__reservedVertexRange) {
|
195
|
-
console.error(`Cannot update geometry, reserved range is too small: ${this.__reservedVertexRange} < ${this.vertexCount} vertices for ${this.name}`);
|
205
|
+
console.error(`Cannot update geometry, reserved vertex range is too small: ${this.__reservedVertexRange} < ${this.vertexCount} vertices for ${this.name}`);
|
196
206
|
return false;
|
197
207
|
}
|
198
208
|
if (this.indexCount > this.__reservedIndexRange) {
|
199
|
-
console.error(`Cannot update geometry, reserved range is too small: ${this.__reservedIndexRange} < ${this.indexCount} indices for ${this.name}`);
|
209
|
+
console.error(`Cannot update geometry, reserved index range is too small: ${this.__reservedIndexRange} < ${this.indexCount} indices for ${this.name}`);
|
200
210
|
return false;
|
201
211
|
}
|
202
212
|
return this.renderer.updateGeometry(geo, this.__instanceIndex);
|
@@ -403,7 +413,8 @@
|
|
403
413
|
|
404
414
|
if (!this.validateGeometry(geo)) {
|
405
415
|
if (debugInstancing) console.error("Cannot add instance, invalid geometry", this.name, geo);
|
406
|
-
|
416
|
+
else console.warn("Cannot add instance, invalid geometry: " + handle.name);
|
417
|
+
return false;
|
407
418
|
}
|
408
419
|
|
409
420
|
if (this.mustGrow(geo)) {
|
@@ -412,7 +423,7 @@
|
|
412
423
|
}
|
413
424
|
else {
|
414
425
|
console.error("Cannot add instance, max count reached", this.name, this.count, this._maxInstanceCount);
|
415
|
-
return;
|
426
|
+
return false;
|
416
427
|
}
|
417
428
|
}
|
418
429
|
|
@@ -425,6 +436,8 @@
|
|
425
436
|
|
426
437
|
if (this._currentInstanceCount > 0)
|
427
438
|
this._batchedMesh.visible = true;
|
439
|
+
|
440
|
+
return true;
|
428
441
|
}
|
429
442
|
|
430
443
|
remove(handle: InstanceHandle, delete_: boolean) {
|
@@ -480,25 +493,26 @@
|
|
480
493
|
this._batchedMesh.layers.disableAll();
|
481
494
|
}
|
482
495
|
|
483
|
-
private validateGeometry(
|
496
|
+
private validateGeometry(geometry: BufferGeometry): boolean {
|
484
497
|
const batchGeometry = this.geometry;
|
485
498
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
499
|
+
for (const attributeName in batchGeometry.attributes) {
|
500
|
+
if (attributeName === "batchId") {
|
501
|
+
continue;
|
502
|
+
}
|
503
|
+
if (!geometry.hasAttribute(attributeName)) {
|
504
|
+
console.warn(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
|
505
|
+
// geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone());
|
506
|
+
return false;
|
507
|
+
// throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
|
508
|
+
}
|
509
|
+
// const srcAttribute = geometry.getAttribute(attributeName);
|
510
|
+
// const dstAttribute = batchGeometry.getAttribute(attributeName);
|
511
|
+
// if (srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized) {
|
512
|
+
// if (debugInstancing) throw new Error('BatchedMesh: All attributes must have a consistent itemSize and normalized value.');
|
513
|
+
// return false;
|
514
|
+
// }
|
515
|
+
}
|
502
516
|
return true;
|
503
517
|
}
|
504
518
|
|
@@ -555,6 +569,11 @@
|
|
555
569
|
this._batchedMesh = newInst;
|
556
570
|
this._maxInstanceCount = newSize;
|
557
571
|
|
572
|
+
// since we have a new batched mesh we need to re-add all the instances
|
573
|
+
// fixes https://linear.app/needle/issue/NE-5711
|
574
|
+
this._usedBuckets.length = 0;
|
575
|
+
this._availableBuckets.length = 0;
|
576
|
+
|
558
577
|
// add current instances to new instanced mesh
|
559
578
|
for (const handle of this._handles) {
|
560
579
|
if (handle && handle.__instanceIndex >= 0) {
|
@@ -702,8 +721,8 @@
|
|
702
721
|
let lod0Count = lod0.vertexCount;
|
703
722
|
let lod0IndexCount = lod0.indexCount;
|
704
723
|
// add some wiggle room: https://linear.app/needle/issue/NE-4505
|
705
|
-
lod0Count
|
706
|
-
lod0Count +=
|
724
|
+
const extra = Math.min(128, Math.ceil(lod0Count * .15));
|
725
|
+
lod0Count += extra;
|
707
726
|
lod0IndexCount += 20;
|
708
727
|
vertexCount = Math.max(vertexCount, lod0Count);
|
709
728
|
indexCount = Math.max(indexCount, lod0IndexCount);
|
@@ -7,6 +7,7 @@
|
|
7
7
|
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
8
8
|
import { getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils.js";
|
9
9
|
import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
|
10
|
+
import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"
|
10
11
|
import { Behaviour, GameObject } from "../../Component.js";
|
11
12
|
import { Renderer } from "../../Renderer.js"
|
12
13
|
import { SpriteRenderer } from "../../SpriteRenderer.js";
|
@@ -174,6 +175,7 @@
|
|
174
175
|
showBalloonMessage("USDZ Exporter enabled: " + this.name);
|
175
176
|
|
176
177
|
document.getElementById("open-in-ar")?.addEventListener("click", this.onClickedOpenInARElement);
|
178
|
+
InternalUSDZRegistry.registerExporter(this);
|
177
179
|
}
|
178
180
|
|
179
181
|
/** @internal */
|
@@ -189,6 +191,7 @@
|
|
189
191
|
showBalloonMessage("USDZ Exporter disabled: " + this.name);
|
190
192
|
|
191
193
|
document.getElementById("open-in-ar")?.removeEventListener("click", this.onClickedOpenInARElement);
|
194
|
+
InternalUSDZRegistry.unregisterExporter(this);
|
192
195
|
}
|
193
196
|
|
194
197
|
private onClickedOpenInARElement = (evt) => {
|
@@ -585,6 +588,7 @@
|
|
585
588
|
getARScaleAndTarget(): { scale: number, _invertForward: boolean, target: Object3D, sessionRoot: Object3D | null} {
|
586
589
|
if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject, sessionRoot: null};
|
587
590
|
|
591
|
+
const xr = GameObject.findObjectOfType(WebXR);
|
588
592
|
let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot);
|
589
593
|
if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
|
590
594
|
|
@@ -592,11 +596,11 @@
|
|
592
596
|
let _invertForward = false;
|
593
597
|
const target = this.objectToExport;
|
594
598
|
|
595
|
-
|
596
|
-
|
597
|
-
|
599
|
+
// Note: when USDZ is exported, SessionRoot might not have the correct AR scale, since that is populated upon EnterXR from WebXR.
|
600
|
+
if (xr) {
|
601
|
+
arScale = xr.arScale;
|
598
602
|
}
|
599
|
-
else {
|
603
|
+
else if (sessionRoot) {
|
600
604
|
arScale = sessionRoot.arScale;
|
601
605
|
_invertForward = sessionRoot.invertForward;
|
602
606
|
}
|
@@ -263,12 +263,19 @@
|
|
263
263
|
|
264
264
|
this.createLocalAvatar(args.xr);
|
265
265
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
266
|
+
// for mobile screen AR we have the "X" button to exit and don't need to add an extra menu button to close
|
267
|
+
// https://linear.app/needle/issue/NE-5716
|
268
|
+
if (args.xr.isScreenBasedAR) {
|
269
|
+
|
270
|
+
}
|
271
|
+
else {
|
272
|
+
this._exitXRMenuButton = this.context.menu.appendChild({
|
273
|
+
label: "Quit XR",
|
274
|
+
onClick: () => this.exitXR(),
|
275
|
+
icon: "exit_to_app",
|
276
|
+
priority: 20_000,
|
277
|
+
});
|
278
|
+
}
|
272
279
|
}
|
273
280
|
|
274
281
|
onUpdateXR(_args: NeedleXREventArgs): void {
|
@@ -0,0 +1,662 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __extends = (this && this.__extends) || (function () {
|
3
|
+
var extendStatics = function (d, b) {
|
4
|
+
extendStatics = Object.setPrototypeOf ||
|
5
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
6
|
+
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
7
|
+
return extendStatics(d, b);
|
8
|
+
};
|
9
|
+
return function (d, b) {
|
10
|
+
extendStatics(d, b);
|
11
|
+
function __() { this.constructor = d; }
|
12
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
13
|
+
};
|
14
|
+
})();
|
15
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
|
16
|
+
if (!privateMap.has(receiver)) {
|
17
|
+
throw new TypeError("attempted to get private field on non-instance");
|
18
|
+
}
|
19
|
+
return privateMap.get(receiver);
|
20
|
+
};
|
21
|
+
var _onClick;
|
22
|
+
exports.__esModule = true;
|
23
|
+
exports.NeedleMenuElement = exports.NeedleMenu = void 0;
|
24
|
+
var engine_license_js_1 = require("../../engine_license.js");
|
25
|
+
var engine_networking_utils_js_1 = require("../../engine_networking_utils.js");
|
26
|
+
var engine_utils_js_1 = require("../../engine_utils.js");
|
27
|
+
var events_js_1 = require("../../xr/events.js");
|
28
|
+
var buttons_js_1 = require("../buttons.js");
|
29
|
+
var fonts_js_1 = require("../fonts.js");
|
30
|
+
var icons_js_1 = require("../icons.js");
|
31
|
+
var logo_element_js_1 = require("../logo-element.js");
|
32
|
+
var needle_menu_spatial_js_1 = require("./needle-menu-spatial.js");
|
33
|
+
var elementName = "needle-menu";
|
34
|
+
var debug = engine_utils_js_1.getParam("debugmenu");
|
35
|
+
var debugNonCommercial = engine_utils_js_1.getParam("debugnoncommercial");
|
36
|
+
/**
|
37
|
+
* The NeedleMenu is a menu that can be displayed in the needle engine webcomponent or in VR/AR sessions.
|
38
|
+
* The menu can be used to add buttons to the needle engine that can be used to interact with the application.
|
39
|
+
* The menu can be positioned at the top or the bottom of the needle engine webcomponent
|
40
|
+
*
|
41
|
+
* @example Create a button using the NeedleMenu
|
42
|
+
* ```typescript
|
43
|
+
* onStart(ctx => {
|
44
|
+
* ctx.menu.appendChild({
|
45
|
+
* label: "Open Google",
|
46
|
+
* icon: "google",
|
47
|
+
* onClick: () => { window.open("https://www.google.com", "_blank") }
|
48
|
+
* });
|
49
|
+
* })
|
50
|
+
* ```
|
51
|
+
*
|
52
|
+
* Buttons can be added to the menu using the {@link NeedleMenu#appendChild} method or by sending a postMessage event to the needle engine with the type "needle:menu". Use the {@link NeedleMenuPostMessageModel} model to create buttons with postMessage.
|
53
|
+
* @example Create a button using a postmessage
|
54
|
+
* ```javascript
|
55
|
+
* window.postMessage({
|
56
|
+
* type: "needle:menu",
|
57
|
+
* button: {
|
58
|
+
* label: "Open Google",
|
59
|
+
* icon: "google",
|
60
|
+
* onclick: "https://www.google.com",
|
61
|
+
* target: "_blank",
|
62
|
+
* }
|
63
|
+
* }, "*");
|
64
|
+
* ```
|
65
|
+
*/
|
66
|
+
var NeedleMenu = /** @class */ (function () {
|
67
|
+
function NeedleMenu(context) {
|
68
|
+
var _this = this;
|
69
|
+
this.onPostMessage = function (e) {
|
70
|
+
// lets just allow the same origin for now
|
71
|
+
if (e.origin !== globalThis.location.origin)
|
72
|
+
return;
|
73
|
+
if (typeof e.data === "object") {
|
74
|
+
var data = e.data;
|
75
|
+
var type = data.type;
|
76
|
+
if (type === "needle:menu") {
|
77
|
+
var buttoninfo_1 = data.button;
|
78
|
+
if (buttoninfo_1) {
|
79
|
+
if (!buttoninfo_1.label)
|
80
|
+
return console.error("NeedleMenu: buttoninfo.label is required");
|
81
|
+
if (!buttoninfo_1.onclick)
|
82
|
+
return console.error("NeedleMenu: buttoninfo.onclick is required");
|
83
|
+
var button = document.createElement("button");
|
84
|
+
button.textContent = buttoninfo_1.label;
|
85
|
+
if (buttoninfo_1.icon) {
|
86
|
+
var icon = icons_js_1.getIconElement(buttoninfo_1.icon);
|
87
|
+
button.prepend(icon);
|
88
|
+
}
|
89
|
+
if (buttoninfo_1.priority) {
|
90
|
+
button.setAttribute("priority", buttoninfo_1.priority.toString());
|
91
|
+
}
|
92
|
+
button.onclick = function () {
|
93
|
+
if (buttoninfo_1.onclick) {
|
94
|
+
var isLink = buttoninfo_1.onclick.startsWith("http") || buttoninfo_1.onclick.startsWith("www.");
|
95
|
+
var target = buttoninfo_1.target || "_blank";
|
96
|
+
if (isLink) {
|
97
|
+
globalThis.open(buttoninfo_1.onclick, target);
|
98
|
+
}
|
99
|
+
else
|
100
|
+
console.error("NeedleMenu: onclick is not a valid link", buttoninfo_1.onclick);
|
101
|
+
}
|
102
|
+
};
|
103
|
+
_this._menu.appendChild(button);
|
104
|
+
}
|
105
|
+
else if (debug)
|
106
|
+
console.error("NeedleMenu: unknown postMessage event", data);
|
107
|
+
}
|
108
|
+
else if (debug)
|
109
|
+
console.warn("NeedleMenu: unknown postMessage type", type, data);
|
110
|
+
}
|
111
|
+
};
|
112
|
+
this.onStartXR = function (args) {
|
113
|
+
if (args.session.isScreenBasedAR) {
|
114
|
+
_this._menu["previousParent"] = _this._menu.parentNode;
|
115
|
+
_this._context.arOverlayElement.appendChild(_this._menu);
|
116
|
+
args.session.session.addEventListener("end", _this.onExitXR);
|
117
|
+
}
|
118
|
+
};
|
119
|
+
this.onExitXR = function () {
|
120
|
+
if (_this._menu["previousParent"]) {
|
121
|
+
_this._menu["previousParent"].appendChild(_this._menu);
|
122
|
+
delete _this._menu["previousParent"];
|
123
|
+
}
|
124
|
+
};
|
125
|
+
this._menu = NeedleMenuElement.getOrCreate(context.domElement, context);
|
126
|
+
this._context = context;
|
127
|
+
this._spatialMenu = new needle_menu_spatial_js_1.NeedleSpatialMenu(context, this._menu);
|
128
|
+
window.addEventListener("message", this.onPostMessage);
|
129
|
+
events_js_1.onXRSessionStart(this.onStartXR);
|
130
|
+
}
|
131
|
+
/** @ignore internal method */
|
132
|
+
NeedleMenu.prototype.onDestroy = function () {
|
133
|
+
window.removeEventListener("message", this.onPostMessage);
|
134
|
+
this._menu.remove();
|
135
|
+
this._spatialMenu.onDestroy();
|
136
|
+
};
|
137
|
+
/** Experimental: Change the menu position to be at the top or the bottom of the needle engine webcomponent
|
138
|
+
* @param position "top" or "bottom"
|
139
|
+
*/
|
140
|
+
NeedleMenu.prototype.setPosition = function (position) {
|
141
|
+
this._menu.setPosition(position);
|
142
|
+
};
|
143
|
+
/**
|
144
|
+
* Call to show or hide the menu.
|
145
|
+
* NOTE: Hiding the menu is a PRO feature and requires a needle engine license. Hiding the menu will not work in production without a license.
|
146
|
+
*/
|
147
|
+
NeedleMenu.prototype.setVisible = function (visible) {
|
148
|
+
this._menu.setVisible(visible);
|
149
|
+
};
|
150
|
+
/** When set to false, the Needle Engine logo will be hidden. Hiding the logo requires a needle engine license */
|
151
|
+
NeedleMenu.prototype.showNeedleLogo = function (visible) {
|
152
|
+
var _a;
|
153
|
+
this._menu.showNeedleLogo(visible);
|
154
|
+
(_a = this._spatialMenu) === null || _a === void 0 ? void 0 : _a.showNeedleLogo(visible);
|
155
|
+
// setTimeout(()=>this.showNeedleLogo(!visible), 1000);
|
156
|
+
};
|
157
|
+
/** When enabled=true the menu will be visible in VR/AR sessions */
|
158
|
+
NeedleMenu.prototype.showSpatialMenu = function (enabled) {
|
159
|
+
this._spatialMenu.setEnabled(enabled);
|
160
|
+
};
|
161
|
+
/**
|
162
|
+
* Call to add or remove a button to the menu to show a QR code for the current page
|
163
|
+
* If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked.
|
164
|
+
*/
|
165
|
+
NeedleMenu.prototype.showQRCodeButton = function (enabled) {
|
166
|
+
if (enabled === "desktop-only") {
|
167
|
+
enabled = !engine_utils_js_1.isMobileDevice();
|
168
|
+
}
|
169
|
+
if (!enabled) {
|
170
|
+
var button = buttons_js_1.ButtonsFactory.getOrCreate().qrButton;
|
171
|
+
if (button)
|
172
|
+
button.style.display = "none";
|
173
|
+
return button !== null && button !== void 0 ? button : null;
|
174
|
+
}
|
175
|
+
else {
|
176
|
+
var button = buttons_js_1.ButtonsFactory.getOrCreate().createQRCode();
|
177
|
+
button.style.display = "";
|
178
|
+
this._menu.appendChild(button);
|
179
|
+
return button;
|
180
|
+
}
|
181
|
+
};
|
182
|
+
/** Call to add or remove a button to the menu to mute or unmute the application
|
183
|
+
* Clicking the button will mute or unmute the application
|
184
|
+
*/
|
185
|
+
NeedleMenu.prototype.showAudioPlaybackOption = function (visible) {
|
186
|
+
var _a;
|
187
|
+
if (!visible) {
|
188
|
+
(_a = this._muteButton) === null || _a === void 0 ? void 0 : _a.remove();
|
189
|
+
return;
|
190
|
+
}
|
191
|
+
this._muteButton = buttons_js_1.ButtonsFactory.getOrCreate().createMuteButton(this._context);
|
192
|
+
this._muteButton.setAttribute("priority", "100");
|
193
|
+
this._menu.appendChild(this._muteButton);
|
194
|
+
};
|
195
|
+
NeedleMenu.prototype.showFullscreenOption = function (visible) {
|
196
|
+
var _a;
|
197
|
+
if (!visible) {
|
198
|
+
(_a = this._fullscreenButton) === null || _a === void 0 ? void 0 : _a.remove();
|
199
|
+
return;
|
200
|
+
}
|
201
|
+
this._fullscreenButton = buttons_js_1.ButtonsFactory.getOrCreate().createFullscreenButton(this._context);
|
202
|
+
if (this._fullscreenButton) {
|
203
|
+
this._fullscreenButton.setAttribute("priority", "150");
|
204
|
+
this._menu.appendChild(this._fullscreenButton);
|
205
|
+
}
|
206
|
+
};
|
207
|
+
NeedleMenu.prototype.appendChild = function (child) {
|
208
|
+
return this._menu.appendChild(child);
|
209
|
+
};
|
210
|
+
return NeedleMenu;
|
211
|
+
}());
|
212
|
+
exports.NeedleMenu = NeedleMenu;
|
213
|
+
var NeedleMenuElement = /** @class */ (function (_super) {
|
214
|
+
__extends(NeedleMenuElement, _super);
|
215
|
+
function NeedleMenuElement() {
|
216
|
+
var _a, _b, _c, _d, _e, _f;
|
217
|
+
var _this = _super.call(this) || this;
|
218
|
+
_this._domElement = null;
|
219
|
+
_this._context = null;
|
220
|
+
_onClick.set(_this, function (e) {
|
221
|
+
// detect a click outside the opened foldout to automatically close it
|
222
|
+
if (!e.defaultPrevented
|
223
|
+
&& e.target == _this._domElement
|
224
|
+
&& (e instanceof PointerEvent && e.button === 0)
|
225
|
+
&& _this.root.classList.contains("open")) {
|
226
|
+
// The menu is open, it's a click outside the foldout?
|
227
|
+
var rect = _this.foldout.getBoundingClientRect();
|
228
|
+
var pointerEvent = e;
|
229
|
+
if (!(pointerEvent.clientX > rect.left
|
230
|
+
&& pointerEvent.clientX < rect.right
|
231
|
+
&& pointerEvent.clientY > rect.top
|
232
|
+
&& pointerEvent.clientY < rect.bottom)) {
|
233
|
+
_this.root.classList.toggle("open", false);
|
234
|
+
}
|
235
|
+
}
|
236
|
+
});
|
237
|
+
_this._userRequestedLogoVisible = undefined;
|
238
|
+
_this._userRequestedMenuVisible = undefined;
|
239
|
+
_this._isHandlingChange = false;
|
240
|
+
_this._didSort = new Map();
|
241
|
+
_this._lastAvailableWidthChange = 0;
|
242
|
+
_this._timeoutHandle = 0;
|
243
|
+
_this.handleSizeChange = function (_evt, forceOrEvent) {
|
244
|
+
if (!_this._domElement)
|
245
|
+
return;
|
246
|
+
var width = _this._domElement.clientWidth;
|
247
|
+
if (width < 500) {
|
248
|
+
clearTimeout(_this._timeoutHandle);
|
249
|
+
_this.root.classList.add("compact");
|
250
|
+
_this.foldout.classList.add("floating-panel-style");
|
251
|
+
return;
|
252
|
+
}
|
253
|
+
var padding = 20 * 2;
|
254
|
+
var availableWidth = width - padding;
|
255
|
+
// if the available width has not changed significantly then we can skip the rest
|
256
|
+
if (!forceOrEvent && Math.abs(availableWidth - _this._lastAvailableWidthChange) < 1)
|
257
|
+
return;
|
258
|
+
_this._lastAvailableWidthChange = availableWidth;
|
259
|
+
clearTimeout(_this._timeoutHandle);
|
260
|
+
_this._timeoutHandle = setTimeout(function () {
|
261
|
+
var spaceLeft = getSpaceLeft();
|
262
|
+
if (spaceLeft < 0) {
|
263
|
+
_this.root.classList.add("compact");
|
264
|
+
_this.foldout.classList.add("floating-panel-style");
|
265
|
+
}
|
266
|
+
else if (spaceLeft > 0) {
|
267
|
+
_this.root.classList.remove("compact");
|
268
|
+
_this.foldout.classList.remove("floating-panel-style");
|
269
|
+
if (getSpaceLeft() < 0) {
|
270
|
+
// ensure we still have enough space left
|
271
|
+
_this.root.classList.add("compact");
|
272
|
+
_this.foldout.classList.add("floating-panel-style");
|
273
|
+
}
|
274
|
+
}
|
275
|
+
}, 5);
|
276
|
+
var getCurrentWidth = function () {
|
277
|
+
return _this.options.clientWidth + _this.logoContainer.clientWidth;
|
278
|
+
};
|
279
|
+
var getSpaceLeft = function () {
|
280
|
+
return availableWidth - getCurrentWidth();
|
281
|
+
};
|
282
|
+
};
|
283
|
+
var template = document.createElement('template');
|
284
|
+
// TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space
|
285
|
+
template.innerHTML = "<style>\n\n #root {\n position: absolute;\n width: auto;\n max-width: 95%;\n left: 50%;\n transform: translateX(-50%);\n top: min(20px, 10vh);\n padding: 0.3rem;\n display: flex;\n visibility: visible;\n flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */\n pointer-events: all;\n z-index: 1000;\n }\n\n /** hide the menu if it's empty **/\n #root.has-no-options.logo-hidden {\n display: none; \n }\n\n /** using a div here because then we can change the class for placement **/\n #root.bottom {\n top: auto;\n bottom: min(30px, 10vh);\n }\n \n .wrapper {\n position: relative;\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: stretch;\n gap: 0px;\n padding: 0 .3rem;\n }\n\n .wrapper > *, .options > button, ::slotted(*) {\n position: relative;\n border: none;\n border-radius: 0;\n outline: 1px solid rgba(0,0,0,0);\n display: flex;\n justify-content: center;\n align-items: center;\n max-height: 2.3rem;\n\n /** basic font settings for all entries **/\n font-size: 1rem;\n font-family: 'Roboto Flex', sans-serif;\n font-optical-sizing: auto;\n font-weight: 500;\n font-weight: 200;\n font-variation-settings: \"wdth\" 100;\n color: rgb(20,20,20);\n }\n\n .floating-panel-style {\n background: rgba(255, 255, 255, .4);\n outline: rgb(0 0 0 / 5%) 1px solid;\n border: 1px solid rgba(255, 255, 255, .1);\n box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);\n border-radius: 1.1999999999999993rem;\n /** \n * to make nested background filter work \n * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome \n **/\n &::before {\n content: '';\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n z-index: -1;\n border-radius: 1.1999999999999993rem;\n -webkit-backdrop-filter: blur(8px);\n backdrop-filter: blur(8px);\n }\n }\n\n a {\n color: inherit;\n text-decoration: none;\n }\n\n .options {\n display: flex;\n flex-direction: row;\n }\n\n .options > button, ::slotted(button) {\n height: 2.25rem;\n padding: .4rem .5rem;\n }\n \n :host .options > button, ::slotted(*) {\n background: transparent;\n border: none;\n white-space: nowrap;\n transition: all 0.1s linear .02s;\n border-radius: 0.8rem;\n user-select: none;\n }\n :host .options > *:hover, ::slotted(*:hover) {\n cursor: pointer;\n color: black;\n background: rgba(245, 245, 245, .8);\n box-shadow: inset 0 0 1rem rgba(0,0,30,.2);\n outline: rgba(0,0,0,.1) 1px solid;\n }\n :host .options > *:active, ::slotted(*:active) {\n background: rgba(255, 255, 255, .8);\n box-shadow: inset 0px 1px 1px rgba(255,255,255,.5), inset 0 0 2rem rgba(0,0,30,.2), inset 0px 2px 4px rgba(0,0,20,.5);\n transition: all 0.05s linear;\n }\n :host .options > *:focus, ::slotted(*:focus) {\n outline: rgba(255,255,255,.5) 1px solid;\n }\n\n :host .options > *:disabled, ::slotted(*:disabled) {\n background: rgba(0,0,0,.05);\n color: rgba(60,60,60,.7);\n pointer-events: none;\n }\n\n button, ::slotted(button) {\n gap: 0.3rem;\n }\n\n /** XR button animation **/\n :host button.this-mode-is-requested {\n background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);\n background-size: 200% auto;\n background-position: 0 100%;\n animation: AnimationName .7s ease infinite forwards;\n }\n :host button.other-mode-is-requested {\n opacity: .5;\n }\n \n @keyframes AnimationName {\n 0% { background-position: 0% 0 }\n 100% { background-position: -200% 0 }\n }\n\n\n\n\n .logo {\n cursor: pointer;\n padding-left: 0.6rem;\n }\n .logo-hidden {\n .logo {\n display: none;\n }\n }\n :host .has-options .logo {\n border-left: 1px solid rgba(40,40,40,.4);\n margin-left: 0.3rem;\n }\n\n .logo > span {\n white-space: nowrap;\n }\n\n\n\n /** COMPACT */\n\n /** Hide the menu button normally **/\n .compact-menu-button { display: none; }\n /** And show it when we're in compact mode **/\n .compact .compact-menu-button {\n display: block;\n background: none;\n border: none;\n border-radius: 2rem;\n\n margin: 0;\n padding: 0 .3rem;\n padding-top: .2rem;\n\n color: #000;\n\n &:hover {\n background: rgba(255,255,255,.2);\n cursor: pointer;\n }\n &:focus {\n outline: 1px solid rgba(255,255,255,.5);\n }\n } \n .has-no-options .compact-menu-button {\n display: none;\n }\n .open .compact-menu-button {\n background: rgba(255,255,255,.2);\n }\n .logo-visible .compact-menu-button { \n margin-left: .2rem;\n }\n \n /** Open and hide menu **/\n .compact .foldout { \n display: none;\n }\n .open .options, .open .foldout {\n display: flex;\n }\n .compact .wrapper {\n padding: 0;\n }\n .compact .wrapper, .compact .options {\n height: auto;\n max-height: initial;\n flex-direction: row;\n gap: .12rem;\n }\n .compact .options { \n flex-wrap: wrap;\n gap: .3rem;\n }\n .compact .top .options {\n height: auto;\n flex-direction: row;\n }\n .compact .bottom .wrapper {\n height: auto;\n flex-direction: column;\n }\n\n .compact .foldout {\n max-height: min(100ch, calc(100vh - 100px));\n overflow: auto;\n overflow-x: hidden;\n align-items: center;\n\n position: fixed;\n bottom: calc(100% + 5px);\n z-index: 100;\n width: auto;\n left: .2rem;\n right: .2rem;\n padding: .2rem;\n\n }\n .compact.logo-hidden .foldout {\n /** for when there's no logo we want to center the foldout **/\n min-width: 24ch;\n margin-left: 50%;\n transform: translateX(calc(-50% - .2rem));\n }\n \n .compact.top .foldout {\n top: calc(100% + 5px);\n bottom: auto;\n }\n\n ::-webkit-scrollbar {\n max-width: 7px;\n background: rgba(100,100,100,.2);\n border-radius: .2rem;\n }\n ::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, .3);\n border-radius: .2rem;\n }\n ::-webkit-scrollbar-thumb:hover {\n background: rgb(150,150,150);\n }\n\n .compact .options > * {\n font-size: 1.2rem;\n padding: .6rem .5rem;\n }\n .compact.has-options .logo {\n border: none;\n padding-left: 0;\n margin-left: 1rem;\n margin-bottom: .02rem;\n }\n .compact .options > button {\n display: flex;\n flex-basis: 100%;\n min-height: 3rem;\n }\n .compact .options > button.row2 {\n //border: 1px solid red !important;\n display: flex;\n flex: 1;\n flex-basis: 30%;\n }\n\n /** If there's really not enough space then just hide all options **/\n @media (max-width: 100px) or (max-height: 100px){\n .foldout {\n display: none !important;\n }\n .compact-menu-button {\n display: none !important;\n }\n }\n \n /* dark mode */\n /*\n @media (prefers-color-scheme: dark) {\n :host {\n background: rgba(0,0,0, .6);\n }\n :host button {\n color: rgba(200,200,200);\n }\n :host button:hover {\n background: rgba(100,100,100, .8);\n }\n }\n */\n\n </style>\n \n <div id=\"root\" class=\"logo-visible floating-panel-style bottom\">\n <div class=\"wrapper\">\n <div class=\"foldout\">\n <div class=\"options\" part=\"options\">\n <slot></slot>\n </div>\n <div class=\"options\" part=\"options\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n <div style=\"user-select:none\" class=\"logo\">\n <span class=\"madewith notranslate\">powered by</span>\n </div>\n </div>\n <button class=\"compact-menu-button\"></button>\n </div>\n ";
|
286
|
+
// we dont need to expose the shadow root
|
287
|
+
var shadow = _this.attachShadow({ mode: 'open' });
|
288
|
+
// we need to add the icons to both the shadow dom as well as the HEAD to work
|
289
|
+
// https://github.com/google/material-design-icons/issues/1165
|
290
|
+
fonts_js_1.ensureFonts();
|
291
|
+
fonts_js_1.loadFont(fonts_js_1.iconFontUrl, { loadedCallback: function () { _this.handleSizeChange(); } });
|
292
|
+
fonts_js_1.loadFont(fonts_js_1.iconFontUrl, { element: shadow });
|
293
|
+
var content = template.content.cloneNode(true);
|
294
|
+
shadow === null || shadow === void 0 ? void 0 : shadow.appendChild(content);
|
295
|
+
_this.root = shadow.querySelector("#root");
|
296
|
+
_this.wrapper = (_a = _this.root) === null || _a === void 0 ? void 0 : _a.querySelector(".wrapper");
|
297
|
+
_this.options = (_b = _this.root) === null || _b === void 0 ? void 0 : _b.querySelector(".options");
|
298
|
+
_this.logoContainer = (_c = _this.root) === null || _c === void 0 ? void 0 : _c.querySelector(".logo");
|
299
|
+
_this.compactMenuButton = (_d = _this.root) === null || _d === void 0 ? void 0 : _d.querySelector(".compact-menu-button");
|
300
|
+
_this.compactMenuButton.append(icons_js_1.getIconElement("more_vert"));
|
301
|
+
_this.foldout = (_e = _this.root) === null || _e === void 0 ? void 0 : _e.querySelector(".foldout");
|
302
|
+
(_f = _this.root) === null || _f === void 0 ? void 0 : _f.appendChild(_this.wrapper);
|
303
|
+
_this.wrapper.classList.add("wrapper");
|
304
|
+
var logo = logo_element_js_1.NeedleLogoElement.create();
|
305
|
+
logo.style.minHeight = "1rem";
|
306
|
+
_this.logoContainer.append(logo);
|
307
|
+
_this.logoContainer.addEventListener("click", function () {
|
308
|
+
globalThis.open("https://needle.tools", "_blank");
|
309
|
+
});
|
310
|
+
// if the user has a license then we CAN hide the needle logo
|
311
|
+
engine_license_js_1.onLicenseCheckResultChanged(function (res) {
|
312
|
+
if (res == true && engine_license_js_1.hasCommercialLicense() && !debugNonCommercial) {
|
313
|
+
var visible = _this._userRequestedLogoVisible;
|
314
|
+
if (visible === undefined)
|
315
|
+
visible = false;
|
316
|
+
_this..call(_this, visible);
|
317
|
+
}
|
318
|
+
});
|
319
|
+
_this.compactMenuButton.addEventListener("click", function (evt) {
|
320
|
+
evt.preventDefault();
|
321
|
+
_this.root.classList.toggle("open");
|
322
|
+
});
|
323
|
+
var context = _this._context;
|
324
|
+
// we need to assign it in the timeout because the reference is set *after* the constructor did run
|
325
|
+
setTimeout(function () { return context = _this._context; });
|
326
|
+
// watch changes
|
327
|
+
var changeEventCounter = 0;
|
328
|
+
var forceVisible = function (parent, visible) {
|
329
|
+
var _a, _b, _c;
|
330
|
+
if (debug)
|
331
|
+
console.log("Set menu visible", visible);
|
332
|
+
if ((context === null || context === void 0 ? void 0 : context.isInAR) && context.arOverlayElement) {
|
333
|
+
if (parent != context.arOverlayElement) {
|
334
|
+
context.arOverlayElement.appendChild(_this);
|
335
|
+
}
|
336
|
+
}
|
337
|
+
else if (_this.parentNode != ((_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.shadowRoot))
|
338
|
+
(_c = (_b = _this._domElement) === null || _b === void 0 ? void 0 : _b.shadowRoot) === null || _c === void 0 ? void 0 : _c.appendChild(_this);
|
339
|
+
_this.style.display = visible ? "flex" : "none";
|
340
|
+
_this.style.visibility = "visible";
|
341
|
+
_this.style.opacity = "1";
|
342
|
+
};
|
343
|
+
var isHandlingMutation = false;
|
344
|
+
var rootObserver = new MutationObserver(function (mutations) {
|
345
|
+
var _a;
|
346
|
+
if (isHandlingMutation) {
|
347
|
+
return;
|
348
|
+
}
|
349
|
+
try {
|
350
|
+
isHandlingMutation = true;
|
351
|
+
_this.onChangeDetected(mutations);
|
352
|
+
// ensure the menu is not hidden or removed
|
353
|
+
var requiredParent_1 = _this === null || _this === void 0 ? void 0 : _this.parentNode;
|
354
|
+
if (_this.style.display != "flex" || _this.style.visibility != "visible" || _this.style.opacity != "1" || requiredParent_1 != ((_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.shadowRoot)) {
|
355
|
+
if (!engine_license_js_1.hasCommercialLicense()) {
|
356
|
+
var change = changeEventCounter++;
|
357
|
+
// if a user doesn't have a local pro license *but* for development the menu is hidden then we show a warning
|
358
|
+
if (engine_networking_utils_js_1.isLocalNetwork() && _this._userRequestedMenuVisible === false) {
|
359
|
+
// set visible once so that the check above is not triggered again
|
360
|
+
if (change === 0) {
|
361
|
+
// if the user requested visible to false before this code is called for the first time we want to respect the choice just in this case
|
362
|
+
forceVisible(requiredParent_1, _this._userRequestedMenuVisible);
|
363
|
+
}
|
364
|
+
// warn only once
|
365
|
+
if (change === 1) {
|
366
|
+
console.warn("Needle Menu Warning: You need a PRO license to hide the Needle Engine menu \u2192 The menu will be visible in your deployed website if you don't have a PRO license. See https://needle.tools/pricing for details.");
|
367
|
+
}
|
368
|
+
}
|
369
|
+
else {
|
370
|
+
if (change === 0) {
|
371
|
+
forceVisible(requiredParent_1, true);
|
372
|
+
}
|
373
|
+
else {
|
374
|
+
setTimeout(function () { return forceVisible(requiredParent_1, true); }, 5);
|
375
|
+
}
|
376
|
+
}
|
377
|
+
}
|
378
|
+
}
|
379
|
+
}
|
380
|
+
finally {
|
381
|
+
isHandlingMutation = false;
|
382
|
+
}
|
383
|
+
});
|
384
|
+
rootObserver.observe(_this.root, { childList: true, subtree: true, attributes: true });
|
385
|
+
if (debug) {
|
386
|
+
_this.___insertDebugOptions();
|
387
|
+
}
|
388
|
+
return _this;
|
389
|
+
}
|
390
|
+
NeedleMenuElement.create = function () {
|
391
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#is
|
392
|
+
return document.createElement(elementName, { is: elementName });
|
393
|
+
};
|
394
|
+
NeedleMenuElement.getOrCreate = function (domElement, context) {
|
395
|
+
var element = domElement.querySelector(elementName);
|
396
|
+
if (!element && domElement.shadowRoot) {
|
397
|
+
element = domElement.shadowRoot.querySelector(elementName);
|
398
|
+
}
|
399
|
+
if (!element) {
|
400
|
+
element = NeedleMenuElement.create();
|
401
|
+
if (domElement.shadowRoot)
|
402
|
+
domElement.shadowRoot.appendChild(element);
|
403
|
+
else
|
404
|
+
domElement.appendChild(element);
|
405
|
+
}
|
406
|
+
element._domElement = domElement;
|
407
|
+
element._context = context;
|
408
|
+
return element;
|
409
|
+
};
|
410
|
+
NeedleMenuElement.prototype.connectedCallback = function () {
|
411
|
+
var _this = this;
|
412
|
+
window.addEventListener("resize", this.handleSizeChange);
|
413
|
+
this.handleMenuVisible();
|
414
|
+
this._sizeChangeInterval = setInterval(function () { return _this.handleSizeChange(undefined, true); }, 5000);
|
415
|
+
// the dom element is set after the constructor runs
|
416
|
+
setTimeout(function () {
|
417
|
+
var _a, _b;
|
418
|
+
(_a = _this._domElement) === null || _a === void 0 ? void 0 : _a.addEventListener("resize", _this.handleSizeChange);
|
419
|
+
(_b = _this._domElement) === null || _b === void 0 ? void 0 : _b.addEventListener("click", __classPrivateFieldGet(_this, _onClick));
|
420
|
+
}, 1);
|
421
|
+
};
|
422
|
+
NeedleMenuElement.prototype.disconnectedCallback = function () {
|
423
|
+
var _a, _b;
|
424
|
+
window.removeEventListener("resize", this.handleSizeChange);
|
425
|
+
clearInterval(this._sizeChangeInterval);
|
426
|
+
(_a = this._domElement) === null || _a === void 0 ? void 0 : _a.removeEventListener("resize", this.handleSizeChange);
|
427
|
+
(_b = this._context) === null || _b === void 0 ? void 0 : _b.domElement.removeEventListener("click", __classPrivateFieldGet(this, _onClick));
|
428
|
+
};
|
429
|
+
NeedleMenuElement.prototype.showNeedleLogo = function (visible) {
|
430
|
+
this._userRequestedLogoVisible = visible;
|
431
|
+
if (!visible) {
|
432
|
+
if (!engine_license_js_1.hasCommercialLicense() || debugNonCommercial) {
|
433
|
+
console.warn("Needle Menu: You need a PRO license to hide the Needle Engine logo.");
|
434
|
+
var localNetwork = engine_networking_utils_js_1.isLocalNetwork();
|
435
|
+
if (!localNetwork)
|
436
|
+
return;
|
437
|
+
}
|
438
|
+
}
|
439
|
+
this..call(this, visible);
|
440
|
+
};
|
441
|
+
NeedleMenuElement.prototype. = function (visible) {
|
442
|
+
this.logoContainer.style.display = "";
|
443
|
+
this.logoContainer.style.opacity = "1";
|
444
|
+
this.logoContainer.style.visibility = "visible";
|
445
|
+
if (visible) {
|
446
|
+
this.root.classList.remove("logo-hidden");
|
447
|
+
this.root.classList.add("logo-visible");
|
448
|
+
}
|
449
|
+
else {
|
450
|
+
this.root.classList.remove("logo-visible");
|
451
|
+
this.root.classList.add("logo-hidden");
|
452
|
+
}
|
453
|
+
};
|
454
|
+
NeedleMenuElement.prototype.setPosition = function (position) {
|
455
|
+
// ensure the position is of a known type:
|
456
|
+
if (position !== "top" && position !== "bottom") {
|
457
|
+
return console.error("NeedleMenu.setPosition: invalid position", position);
|
458
|
+
}
|
459
|
+
this.root.classList.remove("top", "bottom");
|
460
|
+
this.root.classList.add(position);
|
461
|
+
};
|
462
|
+
NeedleMenuElement.prototype.setVisible = function (visible) {
|
463
|
+
this._userRequestedMenuVisible = visible;
|
464
|
+
this.style.display = visible ? "flex" : "none";
|
465
|
+
};
|
466
|
+
NeedleMenuElement.prototype.append = function () {
|
467
|
+
var nodes = [];
|
468
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
469
|
+
nodes[_i] = arguments[_i];
|
470
|
+
}
|
471
|
+
for (var _a = 0, nodes_1 = nodes; _a < nodes_1.length; _a++) {
|
472
|
+
var node = nodes_1[_a];
|
473
|
+
if (typeof node === "string") {
|
474
|
+
var element = document.createTextNode(node);
|
475
|
+
this.options.appendChild(element);
|
476
|
+
}
|
477
|
+
else {
|
478
|
+
this.options.appendChild(node);
|
479
|
+
}
|
480
|
+
}
|
481
|
+
};
|
482
|
+
NeedleMenuElement.prototype.appendChild = function (node) {
|
483
|
+
var _a, _b;
|
484
|
+
if (!(node instanceof Node)) {
|
485
|
+
var button = document.createElement("button");
|
486
|
+
button.textContent = node.label;
|
487
|
+
button.onclick = node.onClick;
|
488
|
+
button.setAttribute("priority", (_b = (_a = node.priority) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "0");
|
489
|
+
if (node.icon) {
|
490
|
+
var icon = icons_js_1.getIconElement(node.icon);
|
491
|
+
if (node.iconSide === "right") {
|
492
|
+
button.appendChild(icon);
|
493
|
+
}
|
494
|
+
else {
|
495
|
+
button.prepend(icon);
|
496
|
+
}
|
497
|
+
}
|
498
|
+
if (node["class"]) {
|
499
|
+
button.classList.add(node["class"]);
|
500
|
+
}
|
501
|
+
node = button;
|
502
|
+
}
|
503
|
+
var res = this.options.appendChild(node);
|
504
|
+
return res;
|
505
|
+
};
|
506
|
+
NeedleMenuElement.prototype.prepend = function () {
|
507
|
+
var nodes = [];
|
508
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
509
|
+
nodes[_i] = arguments[_i];
|
510
|
+
}
|
511
|
+
for (var _a = 0, nodes_2 = nodes; _a < nodes_2.length; _a++) {
|
512
|
+
var node = nodes_2[_a];
|
513
|
+
if (typeof node === "string") {
|
514
|
+
var element = document.createTextNode(node);
|
515
|
+
this.options.prepend(element);
|
516
|
+
}
|
517
|
+
else {
|
518
|
+
this.options.prepend(node);
|
519
|
+
}
|
520
|
+
}
|
521
|
+
};
|
522
|
+
/** Called when any change in the web component is detected (including in children and child attributes) */
|
523
|
+
NeedleMenuElement.prototype.onChangeDetected = function (_mut) {
|
524
|
+
if (this._isHandlingChange)
|
525
|
+
return;
|
526
|
+
this._isHandlingChange = true;
|
527
|
+
try {
|
528
|
+
// if (debug) console.log("NeedleMenu.onChangeDetected", _mut);
|
529
|
+
this.handleMenuVisible();
|
530
|
+
for (var _i = 0, _mut_1 = _mut; _i < _mut_1.length; _i++) {
|
531
|
+
var mut = _mut_1[_i];
|
532
|
+
if (mut.target == this.options) {
|
533
|
+
this.onOptionsChildrenChanged(mut);
|
534
|
+
}
|
535
|
+
}
|
536
|
+
}
|
537
|
+
finally {
|
538
|
+
this._isHandlingChange = false;
|
539
|
+
}
|
540
|
+
};
|
541
|
+
NeedleMenuElement.prototype.onOptionsChildrenChanged = function (_mut) {
|
542
|
+
this.root.classList.toggle("has-options", this.hasAnyVisibleOptions);
|
543
|
+
this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions);
|
544
|
+
this.handleSizeChange(undefined, true);
|
545
|
+
if (_mut.type === "childList") {
|
546
|
+
var needsSorting = false;
|
547
|
+
var now = Date.now();
|
548
|
+
// sort children by priority only when necessary
|
549
|
+
for (var i = 0; i < _mut.addedNodes.length; i++) {
|
550
|
+
var child = _mut.addedNodes[i];
|
551
|
+
var lastTime = this._didSort.get(child);
|
552
|
+
if (typeof lastTime === "number" && now - lastTime < 100)
|
553
|
+
continue;
|
554
|
+
this._didSort.set(child, now);
|
555
|
+
needsSorting = true;
|
556
|
+
}
|
557
|
+
if (needsSorting) {
|
558
|
+
var children = Array.from(this.options.children);
|
559
|
+
children.sort(function (a, b) {
|
560
|
+
var p1 = parseInt(a.getAttribute("priority") || "0");
|
561
|
+
var p2 = parseInt(b.getAttribute("priority") || "0");
|
562
|
+
return p1 - p2;
|
563
|
+
});
|
564
|
+
for (var _i = 0, children_1 = children; _i < children_1.length; _i++) {
|
565
|
+
var child = children_1[_i];
|
566
|
+
this.options.appendChild(child);
|
567
|
+
}
|
568
|
+
}
|
569
|
+
}
|
570
|
+
};
|
571
|
+
/** checks if the menu has any content and should be rendered at all
|
572
|
+
* if we dont have any content and logo then we hide the menu
|
573
|
+
*/
|
574
|
+
NeedleMenuElement.prototype.handleMenuVisible = function () {
|
575
|
+
if (debug)
|
576
|
+
console.log("Update VisibleState: Any Content?", this.hasAnyContent);
|
577
|
+
if (this.hasAnyContent) {
|
578
|
+
this.root.style.display = "";
|
579
|
+
}
|
580
|
+
else {
|
581
|
+
this.root.style.display = "none";
|
582
|
+
}
|
583
|
+
this.root.classList.toggle("has-options", this.hasAnyVisibleOptions);
|
584
|
+
this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions);
|
585
|
+
};
|
586
|
+
Object.defineProperty(NeedleMenuElement.prototype, "hasAnyContent", {
|
587
|
+
/** @returns true if we have any content OR a logo */
|
588
|
+
get: function () {
|
589
|
+
// is the logo visible?
|
590
|
+
if (this.logoContainer.style.display != "none")
|
591
|
+
return true;
|
592
|
+
if (this.hasAnyVisibleOptions)
|
593
|
+
return true;
|
594
|
+
return false;
|
595
|
+
},
|
596
|
+
enumerable: false,
|
597
|
+
configurable: true
|
598
|
+
});
|
599
|
+
Object.defineProperty(NeedleMenuElement.prototype, "hasAnyVisibleOptions", {
|
600
|
+
get: function () {
|
601
|
+
// do we have any visible buttons?
|
602
|
+
for (var i = 0; i < this.options.children.length; i++) {
|
603
|
+
var child = this.options.children[i];
|
604
|
+
// is slot?
|
605
|
+
if (child.tagName === "SLOT") {
|
606
|
+
var slotElement = child;
|
607
|
+
var nodes = slotElement.assignedNodes();
|
608
|
+
for (var _i = 0, nodes_3 = nodes; _i < nodes_3.length; _i++) {
|
609
|
+
var node = nodes_3[_i];
|
610
|
+
if (node instanceof HTMLElement) {
|
611
|
+
if (node.style.display != "none")
|
612
|
+
return true;
|
613
|
+
}
|
614
|
+
}
|
615
|
+
}
|
616
|
+
else if (child.style.display != "none")
|
617
|
+
return true;
|
618
|
+
}
|
619
|
+
return false;
|
620
|
+
},
|
621
|
+
enumerable: false,
|
622
|
+
configurable: true
|
623
|
+
});
|
624
|
+
NeedleMenuElement.prototype.___insertDebugOptions = function () {
|
625
|
+
var _this = this;
|
626
|
+
window.addEventListener("keydown", function (e) {
|
627
|
+
if (e.key === "p") {
|
628
|
+
_this.setPosition(_this.root.classList.contains("top") ? "bottom" : "top");
|
629
|
+
}
|
630
|
+
});
|
631
|
+
var removeOptionsButton = document.createElement("button");
|
632
|
+
removeOptionsButton.textContent = "Hide Buttons";
|
633
|
+
removeOptionsButton.onclick = function () {
|
634
|
+
var optionsChildren = new Array(_this.options.children.length);
|
635
|
+
for (var i = 0; i < _this.options.children.length; i++) {
|
636
|
+
optionsChildren[i] = _this.options.children[i];
|
637
|
+
}
|
638
|
+
for (var _i = 0, optionsChildren_1 = optionsChildren; _i < optionsChildren_1.length; _i++) {
|
639
|
+
var child = optionsChildren_1[_i];
|
640
|
+
_this.options.removeChild(child);
|
641
|
+
}
|
642
|
+
setTimeout(function () {
|
643
|
+
for (var _i = 0, optionsChildren_2 = optionsChildren; _i < optionsChildren_2.length; _i++) {
|
644
|
+
var child = optionsChildren_2[_i];
|
645
|
+
_this.options.appendChild(child);
|
646
|
+
}
|
647
|
+
}, 1000);
|
648
|
+
};
|
649
|
+
this.appendChild(removeOptionsButton);
|
650
|
+
var anotherButton = document.createElement("button");
|
651
|
+
anotherButton.textContent = "Toggle Logo";
|
652
|
+
anotherButton.addEventListener("click", function () {
|
653
|
+
_this.logoContainer.style.display = _this.logoContainer.style.display === "none" ? "" : "none";
|
654
|
+
});
|
655
|
+
this.appendChild(anotherButton);
|
656
|
+
};
|
657
|
+
return NeedleMenuElement;
|
658
|
+
}(HTMLElement));
|
659
|
+
exports.NeedleMenuElement = NeedleMenuElement;
|
660
|
+
_onClick = new WeakMap();
|
661
|
+
if (!customElements.get(elementName))
|
662
|
+
customElements.define(elementName, NeedleMenuElement);
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
declare type USDZExporter = {
|
3
|
+
exportAndOpen(): Promise<any>,
|
4
|
+
}
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Internal registry for USDZ exporters. This is used by NeedleXRSession.start("immersive-ar")
|
8
|
+
*/
|
9
|
+
export namespace InternalUSDZRegistry {
|
10
|
+
|
11
|
+
const usdzExporter: USDZExporter[] = [];
|
12
|
+
|
13
|
+
export function exportAndOpen(): boolean {
|
14
|
+
if (!usdzExporter?.length) return false;
|
15
|
+
for (const exp of usdzExporter) {
|
16
|
+
exp.exportAndOpen();
|
17
|
+
}
|
18
|
+
return true;
|
19
|
+
}
|
20
|
+
export function registerExporter(exporter: USDZExporter) {
|
21
|
+
usdzExporter.push(exporter);
|
22
|
+
}
|
23
|
+
export function unregisterExporter(exporter: USDZExporter) {
|
24
|
+
if (!usdzExporter) return;
|
25
|
+
const index = usdzExporter.indexOf(exporter);
|
26
|
+
if (index >= 0) {
|
27
|
+
usdzExporter.splice(index, 1);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
}
|