Needle Engine

Changes between version 3.47.7 and 3.47.8
Files changed (13) hide show
  1. src/engine-components/DragControls.ts +2 -0
  2. src/engine/engine_serialization_core.ts +1 -1
  3. src/engine/engine_three_utils.ts +1 -0
  4. src/engine-components/utils/LookAt.ts +11 -6
  5. src/engine/extensions/NEEDLE_techniques_webgl.ts +11 -3
  6. src/engine/webcomponents/needle menu/needle-menu.ts +39 -15
  7. src/engine/xr/NeedleXRSession.ts +12 -1
  8. src/engine/codegen/register_types.ts +2 -2
  9. src/engine-components/RendererInstancing.ts +44 -25
  10. src/engine-components/export/usdz/USDZExporter.ts +8 -4
  11. src/engine-components/webxr/WebXR.ts +13 -6
  12. src/engine/webcomponents/needle menu/dist/needle-menu.js +662 -0
  13. src/engine/xr/usdz.ts +31 -0
src/engine-components/DragControls.ts CHANGED
@@ -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;
src/engine/engine_serialization_core.ts CHANGED
@@ -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;
src/engine/engine_three_utils.ts CHANGED
@@ -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
src/engine-components/utils/LookAt.ts CHANGED
@@ -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 { getWorldPosition, getWorldQuaternion, lookAtObject, setWorldQuaternion } from "../../engine/engine_three_utils.js";
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
- lookAtObject(this.gameObject, target, this.keepUpDirection, this.copyTargetRotation);
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
  );
src/engine/extensions/NEEDLE_techniques_webgl.ts CHANGED
@@ -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("material " + index + " does not use NEEDLE_techniques_webgl");
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) return null;
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) return null;
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;
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -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"></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
- let needsSorting = false;
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
- for (const child of children) {
902
- this.options.appendChild(child);
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
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -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));
src/engine/codegen/register_types.ts CHANGED
@@ -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);
src/engine-components/RendererInstancing.ts CHANGED
@@ -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
- return;
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(_geometry: BufferGeometry): boolean {
496
+ private validateGeometry(geometry: BufferGeometry): boolean {
484
497
  const batchGeometry = this.geometry;
485
498
 
486
- // for (const attributeName in batchGeometry.attributes) {
487
- // if (attributeName === "batchId") {
488
- // continue;
489
- // }
490
- // if (!geometry.hasAttribute(attributeName)) {
491
- // // geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone());
492
- // return false;
493
- // // throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`);
494
- // }
495
- // // const srcAttribute = geometry.getAttribute(attributeName);
496
- // // const dstAttribute = batchGeometry.getAttribute(attributeName);
497
- // // if (srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized) {
498
- // // if (debugInstancing) throw new Error('BatchedMesh: All attributes must have a consistent itemSize and normalized value.');
499
- // // return false;
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 += 10;
706
- lod0Count += lod0Count * .05;
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);
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -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
- if (!sessionRoot) {
596
- const xr = GameObject.findObjectOfType(WebXR);
597
- if (xr) arScale = xr.arScale;
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
  }
src/engine-components/webxr/WebXR.ts CHANGED
@@ -263,12 +263,19 @@
263
263
 
264
264
  this.createLocalAvatar(args.xr);
265
265
 
266
- this._exitXRMenuButton = this.context.menu.appendChild({
267
- label: "Quit XR",
268
- onClick: () => this.exitXR(),
269
- icon: "exit_to_app",
270
- priority: 20_000,
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 {
src/engine/webcomponents/needle menu/dist/needle-menu.js ADDED
@@ -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);
src/engine/xr/usdz.ts ADDED
@@ -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
+ }