@needle-tools/engine 4.12.3 → 4.12.4-next.46bee95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-Bfpfaz84.umd.cjs → gltf-progressive-BqUnxvCx.umd.cjs} +1 -1
  4. package/dist/{gltf-progressive-hFPACYio.min.js → gltf-progressive-CSaX5HQb.min.js} +2 -2
  5. package/dist/{gltf-progressive-DPunMlEM.js → gltf-progressive-ChnIhDXx.js} +27 -27
  6. package/dist/{loader.worker-DWzfDpAl.js → loader.worker-C1GG9A7C.js} +6 -6
  7. package/dist/{needle-engine.bundle-CLPD2ttK.umd.cjs → needle-engine.bundle-CojFvJHR.umd.cjs} +291 -285
  8. package/dist/{needle-engine.bundle-B3ssYJS0.min.js → needle-engine.bundle-CvasmiEO.min.js} +288 -282
  9. package/dist/{needle-engine.bundle-u-rSDw6R.js → needle-engine.bundle-DGjkYJDl.js} +7387 -7322
  10. package/dist/needle-engine.d.ts +53 -28
  11. package/dist/needle-engine.js +4 -4
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/dist/{postprocessing-ClLv0reO.min.js → postprocessing-12-UW7je.min.js} +1 -1
  15. package/dist/{postprocessing-BHQvwehB.umd.cjs → postprocessing-B3Hu0Ryi.umd.cjs} +1 -1
  16. package/dist/{postprocessing-DLI2N3LL.js → postprocessing-R535krvT.js} +2 -2
  17. package/dist/{three-Bf2NBxAw.umd.cjs → three-BzxwLtUE.umd.cjs} +176 -176
  18. package/dist/{three-BCCkyCA5.js → three-D9pcFbxc.js} +4637 -4636
  19. package/dist/{three-W7zWTcfP.min.js → three-DMvLgxja.min.js} +176 -176
  20. package/dist/{three-examples-DB5Uoja4.min.js → three-examples-CIv2roOA.min.js} +1 -1
  21. package/dist/{three-examples-Djbk6WA4.umd.cjs → three-examples-CjSwCv_b.umd.cjs} +1 -1
  22. package/dist/{three-examples-D4rE49Ui.js → three-examples-F0MJj0vr.js} +1 -1
  23. package/dist/{three-mesh-ui-zsOOA5Pq.umd.cjs → three-mesh-ui-BLnJQzMl.umd.cjs} +1 -1
  24. package/dist/{three-mesh-ui-CIez6qJQ.min.js → three-mesh-ui-BllgajJz.min.js} +1 -1
  25. package/dist/{three-mesh-ui-3nSSizT4.js → three-mesh-ui-DYyiRn5Y.js} +1 -1
  26. package/dist/{vendor-tyBvnMF-.umd.cjs → vendor-BFgQSG2m.umd.cjs} +1 -1
  27. package/dist/{vendor-DMZcbVO1.js → vendor-BIFy-gRe.js} +1 -1
  28. package/dist/{vendor-sURMCFSI.min.js → vendor-ChgmXMYr.min.js} +1 -1
  29. package/lib/engine/debug/debug_overlay.js +13 -2
  30. package/lib/engine/debug/debug_overlay.js.map +1 -1
  31. package/lib/engine/engine_animation.d.ts +7 -0
  32. package/lib/engine/engine_animation.js +16 -0
  33. package/lib/engine/engine_animation.js.map +1 -1
  34. package/lib/engine/engine_input.js +5 -3
  35. package/lib/engine/engine_input.js.map +1 -1
  36. package/lib/engine/webcomponents/WebXRButtons.js +7 -0
  37. package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
  38. package/lib/engine/webcomponents/buttons.js +4 -0
  39. package/lib/engine/webcomponents/buttons.js.map +1 -1
  40. package/lib/engine/webcomponents/icons.js +44 -5
  41. package/lib/engine/webcomponents/icons.js.map +1 -1
  42. package/lib/engine/webcomponents/logo-element.js +0 -1
  43. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  44. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +14 -2
  45. package/lib/engine/webcomponents/needle menu/needle-menu.js +195 -142
  46. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  47. package/lib/engine/xr/NeedleXRSession.d.ts +2 -0
  48. package/lib/engine/xr/NeedleXRSession.js +19 -10
  49. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  50. package/lib/engine-components/Animation.js +2 -0
  51. package/lib/engine-components/Animation.js.map +1 -1
  52. package/lib/engine-components/AnimatorController.js +2 -0
  53. package/lib/engine-components/AnimatorController.js.map +1 -1
  54. package/lib/engine-components/Light.d.ts +17 -12
  55. package/lib/engine-components/Light.js +52 -36
  56. package/lib/engine-components/Light.js.map +1 -1
  57. package/lib/engine-components/webxr/WebXRImageTracking.js +9 -2
  58. package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
  59. package/package.json +3 -3
  60. package/plugins/common/license.js +3 -3
  61. package/src/engine/debug/debug_overlay.ts +15 -2
  62. package/src/engine/engine_animation.ts +19 -1
  63. package/src/engine/engine_input.ts +5 -3
  64. package/src/engine/webcomponents/WebXRButtons.ts +8 -0
  65. package/src/engine/webcomponents/buttons.ts +5 -0
  66. package/src/engine/webcomponents/icons.ts +47 -5
  67. package/src/engine/webcomponents/index.ts +1 -1
  68. package/src/engine/webcomponents/logo-element.ts +0 -1
  69. package/src/engine/webcomponents/needle menu/needle-menu.ts +211 -146
  70. package/src/engine/xr/NeedleXRSession.ts +23 -10
  71. package/src/engine-components/Animation.ts +4 -1
  72. package/src/engine-components/AnimatorController.ts +3 -0
  73. package/src/engine-components/Light.ts +50 -42
  74. package/src/engine-components/webxr/WebXRImageTracking.ts +12 -2
@@ -1,6 +1,7 @@
1
1
  import { Texture } from "three";
2
2
 
3
3
 
4
+ const fontname = "Material Symbols Outlined";
4
5
 
5
6
  /** Returns a HTML element containing an icon. Using https://fonts.google.com/icons
6
7
  * As a string you should pass in the name of the icon, e.g. "add" or "delete"
@@ -14,6 +15,20 @@ export function getIconElement(str: string): HTMLElement {
14
15
  span.classList.add("material-symbols-outlined", "notranslate");
15
16
  span.setAttribute("translate", "no");
16
17
  span.innerText = str;
18
+ span.style.visibility = "hidden";
19
+ span.style.userSelect = "none";
20
+ fontReady(fontname).then(res => {
21
+ if (res) span.style.visibility = "";
22
+ else {
23
+ if (str === "more_vert") {
24
+ span.style.visibility = "";
25
+ span.innerText = "More";
26
+ }
27
+ else {
28
+ span.style.display = "none";
29
+ }
30
+ }
31
+ })
17
32
  return span;
18
33
  }
19
34
 
@@ -26,12 +41,8 @@ export function isIconElement(element: Node): boolean {
26
41
  const textures = new Map<string, Texture | null>();
27
42
 
28
43
  export async function getIconTexture(str: string): Promise<Texture | null> {
29
- const fontname = "Material Symbols Outlined";
30
44
  // check if font has loaded
31
- if (!document.fonts.check(`1em '${fontname}'`)) {
32
- console.log("Font not loaded yet");
33
- await document.fonts.ready;
34
- }
45
+ await fontReady(fontname);
35
46
  if (textures.has(str)) {
36
47
  return textures.get(str) as Texture | null;
37
48
  }
@@ -55,4 +66,35 @@ export async function getIconTexture(str: string): Promise<Texture | null> {
55
66
  }
56
67
  textures.set(str, null);
57
68
  return null;
69
+ }
70
+
71
+
72
+ const loadingPromises: Map<string, Promise<boolean>> = new Map();
73
+
74
+ async function fontReady(fontName: string, retries: number = 5, currentRetry: number = 0): Promise<boolean> {
75
+ const res = document.fonts.check(`1em '${fontName}'`);
76
+ if (!res) {
77
+ await document.fonts.ready;
78
+ }
79
+
80
+ // check again after waiting for fonts to be ready
81
+ // we cache this promise once but delete it after it has resolved (since we want to retry if it fails) so that we don't have multiple promises for the same font loading at the same time
82
+ const promise = loadingPromises.get(fontName) || document.fonts.load(`1em '${fontName}'`)
83
+ .then(res => res?.length > 0)
84
+ .finally(() => {
85
+ loadingPromises.delete(fontName);
86
+ });
87
+ loadingPromises.set(fontName, promise);
88
+ const loaded = await promise;
89
+ if (!loaded) {
90
+ if (currentRetry < retries) {
91
+ return new Promise(res => {
92
+ setTimeout(() => {
93
+ res(fontReady(fontName, retries, currentRetry + 1));
94
+ }, 1000);
95
+ });
96
+ }
97
+ return false;
98
+ }
99
+ return true;
58
100
  }
@@ -1,2 +1,2 @@
1
1
 
2
- export { NeedleMenu } from "./needle menu/needle-menu.js";
2
+ export { NeedleMenu } from "./needle menu/needle-menu.js";
@@ -44,7 +44,6 @@ export class NeedleLogoElement extends HTMLElement {
44
44
  img {
45
45
  height: 100%;
46
46
  align-self: end;
47
- margin-left: 0.6rem;
48
47
  transition: transform 0.2s;
49
48
  }
50
49
  img.with-text {
@@ -45,9 +45,17 @@ export declare type ButtonInfo = {
45
45
  label: string,
46
46
  /** Material icon name: https://fonts.google.com/icons */
47
47
  icon?: string,
48
- /** "left" or "right" to place the icon on the left or right side of the button. Default is "left" */
48
+ /** "left" or "right" to place the icon on the left or right side of the button. Default is "left" */
49
49
  iconSide?: "left" | "right",
50
- /** Low priority is icon is on the left, high priority is icon is on the right. Default is undefined */
50
+ /**
51
+ * Priority controls the order of buttons in the menu.
52
+ * If not enough space is available to show all buttons - the highest priority elements will always be visible
53
+ *
54
+ * **Sorting**
55
+ * Low priority is icon is on the left,
56
+ * high priority is icon is on the right.
57
+ * @default undefined
58
+ */
51
59
  priority?: number;
52
60
  /** Experimental. Allows to put two buttons in one row for the compact layout */
53
61
  class?: "row2";
@@ -101,6 +109,20 @@ export declare type ButtonInfo = {
101
109
  * @category HTML
102
110
  */
103
111
  export class NeedleMenu {
112
+
113
+ static setElementPriority(button: HTMLElement, priority: number) {
114
+ button.setAttribute("priority", String(priority));
115
+ }
116
+
117
+ static getElementPriority(button: HTMLElement): number | undefined {
118
+ const priority = button.getAttribute("priority");
119
+ if (priority) {
120
+ const val = Number.parseFloat(priority);
121
+ if (!Number.isNaN(val)) return val;
122
+ }
123
+ return undefined;
124
+ }
125
+
104
126
  private readonly _context: Context;
105
127
  private readonly _menu: NeedleMenuElement;
106
128
  private readonly _spatialMenu: NeedleSpatialMenu;
@@ -274,6 +296,8 @@ export class NeedleMenu {
274
296
 
275
297
  }
276
298
 
299
+ // #region Web component
300
+
277
301
  /**
278
302
  * `<needle-menu>` web component — lightweight menu used by Needle Engine.
279
303
  *
@@ -333,141 +357,144 @@ export class NeedleMenuElement extends HTMLElement {
333
357
  pointer-events: none;
334
358
  }
335
359
 
336
- #root {
337
- position: absolute;
338
- width: auto;
339
- max-width: 95%;
340
- left: 50%;
341
- transform: translateX(-50%);
342
- top: min(20px, 10vh);
343
- padding: 0.3rem;
344
- display: flex;
345
- visibility: visible;
346
- flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */
347
- pointer-events: all;
348
- z-index: 1000;
349
- }
360
+ /** we put base styles in a layer to allow overrides more easily (e.g. the button.mode requested animation should override the base styles) */
361
+ @layer base {
350
362
 
351
- /** hide the menu if it's empty **/
352
- #root.has-no-options.logo-hidden {
353
- display: none;
354
- }
363
+ #root {
364
+ position: absolute;
365
+ width: auto;
366
+ max-width: 95%;
367
+ left: 50%;
368
+ transform: translateX(-50%);
369
+ top: min(20px, 10vh);
370
+ padding: 0.3rem;
371
+ display: flex;
372
+ visibility: visible;
373
+ flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */
374
+ pointer-events: all;
375
+ z-index: 1000;
376
+ }
355
377
 
356
- /** using a div here because then we can change the class for placement **/
357
- #root.bottom {
358
- top: auto;
359
- bottom: min(30px, 10vh);
360
- }
361
- #root.top {
362
- top: calc(.7rem + env(safe-area-inset-top));
363
- }
364
-
365
- .wrapper {
366
- position: relative;
367
- display: flex;
368
- flex-direction: row;
369
- justify-content: center;
370
- align-items: stretch;
371
- gap: 0px;
372
- padding: 0 0rem;
373
- }
378
+ /** hide the menu if it's empty **/
379
+ #root.has-no-options.logo-hidden {
380
+ display: none;
381
+ }
374
382
 
375
- .wrapper > *, .options > button, .options > select, ::slotted(*) {
376
- position: relative;
377
- border: none;
378
- border-radius: 0;
379
- outline: 1px solid rgba(0,0,0,0);
380
- display: flex;
381
- justify-content: center;
382
- align-items: center;
383
- max-height: 2.3rem;
384
- max-width: 100%;
383
+ /** using a div here because then we can change the class for placement **/
384
+ #root.bottom {
385
+ top: auto;
386
+ bottom: min(30px, 10vh);
387
+ }
388
+ #root.top {
389
+ top: calc(.7rem + env(safe-area-inset-top));
390
+ }
391
+
392
+ .wrapper {
393
+ position: relative;
394
+ display: flex;
395
+ flex-direction: row;
396
+ justify-content: center;
397
+ align-items: stretch;
398
+ gap: 0px;
399
+ padding: 0 0rem;
400
+ }
385
401
 
386
- /** basic font settings for all entries **/
387
- font-size: 1rem;
388
- font-family: 'Roboto Flex', sans-serif;
389
- font-optical-sizing: auto;
390
- font-weight: 500;
391
- font-weight: 200;
392
- font-variation-settings: "wdth" 100;
393
- color: rgb(20,20,20);
394
- }
395
-
396
- .options > select[multiple]:hover {
397
- max-height: 300px;
398
- }
399
-
400
- .floating-panel-style {
401
- background: rgba(255, 255, 255, .4);
402
- outline: rgb(0 0 0 / 5%) 1px solid;
403
- border: 1px solid rgba(255, 255, 255, .1);
404
- box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
405
- border-radius: 1.5rem;
406
- /**
407
- * to make nested background filter work
408
- * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome
409
- **/
410
- &::before {
411
- content: '';
412
- position: absolute;
413
- width: 100%;
414
- height: 100%;
415
- top: 0;
416
- left: 0;
417
- z-index: -1;
402
+ .wrapper > *, .options > button, .options > select, ::slotted(*) {
403
+ position: relative;
404
+ border: none;
405
+ border-radius: 0;
406
+ outline: 1px solid rgba(0,0,0,0);
407
+ display: flex;
408
+ justify-content: center;
409
+ align-items: center;
410
+ max-height: 2.3rem;
411
+ max-width: 100%;
412
+
413
+ /** basic font settings for all entries **/
414
+ font-size: 1rem;
415
+ font-family: 'Roboto Flex', sans-serif;
416
+ font-optical-sizing: auto;
417
+ font-weight: 400;
418
+ font-variation-settings: "wdth" 100;
419
+ color: rgb(20,20,20);
420
+ }
421
+
422
+ .options > select[multiple]:hover {
423
+ max-height: 300px;
424
+ }
425
+
426
+ .floating-panel-style {
427
+ background: rgba(255, 255, 255, .4);
428
+ outline: rgb(0 0 0 / 5%) 1px solid;
429
+ border: 1px solid rgba(255, 255, 255, .1);
430
+ box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
418
431
  border-radius: 1.5rem;
419
- -webkit-backdrop-filter: blur(8px);
420
- backdrop-filter: blur(8px);
432
+ /**
433
+ * to make nested background filter work
434
+ * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome
435
+ **/
436
+ &::before {
437
+ content: '';
438
+ position: absolute;
439
+ width: 100%;
440
+ height: 100%;
441
+ top: 0;
442
+ left: 0;
443
+ z-index: -1;
444
+ border-radius: 1.5rem;
445
+ -webkit-backdrop-filter: blur(8px);
446
+ backdrop-filter: blur(8px);
447
+ }
421
448
  }
422
- }
423
449
 
424
- a {
425
- color: inherit;
426
- text-decoration: none;
427
- }
450
+ a {
451
+ color: inherit;
452
+ text-decoration: none;
453
+ }
428
454
 
429
- .options {
430
- display: flex;
431
- flex-direction: row;
432
- align-items: center;
433
- }
455
+ .options {
456
+ display: flex;
457
+ flex-direction: row;
458
+ align-items: center;
459
+ }
434
460
 
435
- .options > *, ::slotted(*) {
436
- max-height: 2.25rem;
437
- padding: .4rem .5rem;
438
- }
461
+ .options > *, ::slotted(*) {
462
+ max-height: 2.25rem;
463
+ padding: .4rem .5rem;
464
+ }
439
465
 
440
- :host .options > *, ::slotted(*) {
441
- background: transparent;
442
- border: none;
443
- white-space: nowrap;
444
- transition: all 0.1s linear .02s;
445
- border-radius: 1.5rem;
446
- user-select: none;
447
- }
448
- :host .options > *:hover, ::slotted(*:hover) {
449
- cursor: pointer;
450
- color: black;
451
- background: rgba(245, 245, 245, .8);
452
- box-shadow: inset 0 0 1rem rgba(0,0,30,.2);
453
- outline: rgba(0,0,0,.1) 1px solid;
454
- }
455
- :host .options > *:active, ::slotted(*:active) {
456
- background: rgba(255, 255, 255, .8);
457
- 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);
458
- transition: all 0.05s linear;
459
- }
460
- :host .options > *:focus, ::slotted(*:focus) {
461
- outline: rgba(255,255,255,.5) 1px solid;
462
- }
463
- :host .options > *:focus-visible, ::slotted(*:focus-visible) {
464
- outline: rgba(0,0,0,.5) 1px solid;
465
- }
466
+ :host .options > *, ::slotted(*) {
467
+ background: transparent;
468
+ border: none;
469
+ white-space: nowrap;
470
+ transition: all 0.1s linear .02s;
471
+ border-radius: 1.5rem;
472
+ user-select: none;
473
+ }
474
+ :host .options > *:hover, ::slotted(*:hover) {
475
+ cursor: pointer;
476
+ color: black;
477
+ background: rgba(245, 245, 245, .8);
478
+ box-shadow: inset 0 0 1rem rgba(0,0,30,.2);
479
+ outline: rgba(0,0,0,.1) 1px solid;
480
+ }
481
+ :host .options > *:active, ::slotted(*:active) {
482
+ background: rgba(255, 255, 255, .8);
483
+ 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);
484
+ transition: all 0.05s linear;
485
+ }
486
+ :host .options > *:focus, ::slotted(*:focus) {
487
+ outline: rgba(255,255,255,.5) 1px solid;
488
+ }
489
+ :host .options > *:focus-visible, ::slotted(*:focus-visible) {
490
+ outline: rgba(0,0,0,.5) 1px solid;
491
+ }
466
492
 
467
- :host .options > *:disabled, ::slotted(*:disabled) {
468
- background: rgba(0,0,0,.05);
469
- color: rgba(60,60,60,.7);
470
- pointer-events: none;
493
+ :host .options > *:disabled, ::slotted(*:disabled) {
494
+ background: rgba(0,0,0,.05);
495
+ color: rgba(60,60,60,.7);
496
+ pointer-events: none;
497
+ }
471
498
  }
472
499
 
473
500
  button, ::slotted(button) {
@@ -475,6 +502,7 @@ export class NeedleMenuElement extends HTMLElement {
475
502
  }
476
503
 
477
504
  /** XR button animation **/
505
+
478
506
  :host button.this-mode-is-requested {
479
507
  background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
480
508
  background-size: 200% auto;
@@ -486,8 +514,12 @@ export class NeedleMenuElement extends HTMLElement {
486
514
  }
487
515
 
488
516
  @keyframes AnimationName {
489
- 0% { background-position: 0% 0 }
490
- 100% { background-position: -200% 0 }
517
+ 0% {
518
+ background-position: 0% 0
519
+ }
520
+ 100% {
521
+ background-position: -200% 0
522
+ }
491
523
  }
492
524
 
493
525
 
@@ -520,6 +552,10 @@ export class NeedleMenuElement extends HTMLElement {
520
552
 
521
553
  /** Hide the menu button normally **/
522
554
  .compact-menu-button { display: none; }
555
+
556
+ /** Hide the compact only options when not in compact mode */
557
+ .options.compact-only { display: none; }
558
+
523
559
  /** And show it when we're in compact mode **/
524
560
  .compact .compact-menu-button {
525
561
  position: relative;
@@ -639,12 +675,20 @@ export class NeedleMenuElement extends HTMLElement {
639
675
  padding: .6rem .5rem;
640
676
  width: 100%;
641
677
  }
678
+ .compact.has-options {
679
+ padding-left: 1rem;
680
+ }
642
681
  .compact.has-options .logo {
643
682
  border: none;
644
683
  padding-left: 0;
645
- margin-left: 1rem;
646
684
  margin-bottom: .02rem;
647
685
  }
686
+ .compact .options.compact-only {
687
+ display: initial;
688
+ & > * {
689
+ min-height: 1em;
690
+ }
691
+ }
648
692
  .compact .options {
649
693
  /** e.g. if we have a very wide menu item like a select with long option names we don't want to overflow **/
650
694
  max-width: 100%;
@@ -671,28 +715,15 @@ export class NeedleMenuElement extends HTMLElement {
671
715
  display: none !important;
672
716
  }
673
717
  }
674
-
675
- /* dark mode */
676
- /*
677
- @media (prefers-color-scheme: dark) {
678
- :host {
679
- background: rgba(0,0,0, .6);
680
- }
681
- :host button {
682
- color: rgba(200,200,200);
683
- }
684
- :host button:hover {
685
- background: rgba(100,100,100, .8);
686
- }
687
- }
688
- */
689
718
 
690
719
  </style>
691
720
 
692
721
  <div id="root" class="logo-hidden floating-panel-style bottom">
693
722
  <div class="wrapper">
723
+ <div class="options compact-only" part="options">
724
+ </div>
694
725
  <div class="foldout">
695
- <div class="options" part="options">
726
+ <div class="options main-container" part="options">
696
727
  <slot></slot>
697
728
  </div>
698
729
  <div class="options" part="options">
@@ -723,7 +754,8 @@ export class NeedleMenuElement extends HTMLElement {
723
754
  this.root = shadow.querySelector("#root") as HTMLDivElement;
724
755
 
725
756
  this.wrapper = this.root?.querySelector(".wrapper") as HTMLDivElement;
726
- this.options = this.root?.querySelector(".options") as HTMLDivElement;
757
+ this.options = this.root?.querySelector(".options.main-container") as HTMLDivElement;
758
+ this.optionsCompactMode = this.root?.querySelector(".options.compact-only") as HTMLDivElement;
727
759
  this.logoContainer = this.root?.querySelector(".logo") as HTMLDivElement;
728
760
  this.compactMenuButton = this.root?.querySelector(".compact-menu-button") as HTMLButtonElement;
729
761
  this.compactMenuButton.append(getIconElement("more_vert"));
@@ -932,6 +964,8 @@ export class NeedleMenuElement extends HTMLElement {
932
964
  private readonly wrapper: HTMLDivElement;
933
965
  /** @private contains the buttons and dynamic elements */
934
966
  private readonly options: HTMLDivElement;
967
+ /** @private contains options visible when in compact mode */
968
+ private readonly optionsCompactMode: HTMLDivElement;
935
969
  /** @private contains the needle-logo html element */
936
970
  private readonly logoContainer: HTMLDivElement;
937
971
  /** @private compact menu button element */
@@ -1171,6 +1205,37 @@ export class NeedleMenuElement extends HTMLElement {
1171
1205
  this.foldout.classList.add("floating-panel-style");
1172
1206
  }
1173
1207
  }
1208
+
1209
+ if (this.root.classList.contains("compact")) {
1210
+ this.optionsCompactMode.childNodes.forEach(element => {
1211
+ element.remove();
1212
+ });
1213
+ // Find items in the folding list with the highest priority
1214
+ // The one with the highest priority will be added to the visible container
1215
+ let priorityItem: HTMLElement | null = null;
1216
+ let priorityValue: number = -10000000;
1217
+ for (let i = 0; i < this.options.children.length; i++) {
1218
+ const element = this.options.children.item(i);
1219
+ if (element instanceof HTMLElement) {
1220
+ const priority = NeedleMenu.getElementPriority(element);
1221
+ if (priority !== undefined && priority > priorityValue) {
1222
+ // check if the element is hidden
1223
+ // @TODO: use computed styles
1224
+ const style = element.style;
1225
+ if (style.display === "none") continue;
1226
+ priorityItem = element;
1227
+ priorityValue = priority;
1228
+ }
1229
+ }
1230
+ }
1231
+ if (priorityItem) {
1232
+ const item = priorityItem;
1233
+ const clone = item.cloneNode(true);
1234
+ clone.addEventListener("click", () => item.click())
1235
+ this.optionsCompactMode.appendChild(clone);
1236
+ }
1237
+ }
1238
+
1174
1239
  }, 5) as unknown as number;
1175
1240
 
1176
1241
  const getCurrentWidth = () => {
@@ -454,6 +454,9 @@ export class NeedleXRSession implements INeedleXRSession {
454
454
  */
455
455
  static async start(mode: XRSessionMode | "ar" | "quicklook", init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
456
456
 
457
+ // setup session init args, make sure we have default values
458
+ if (!init) init = {};
459
+
457
460
  // handle iOS platform where "immersive-ar" is special:
458
461
  // - we either launch QuickLook
459
462
  // - or forward to the Needle App Clip experience for WebXR AR
@@ -477,7 +480,9 @@ export class NeedleXRSession implements INeedleXRSession {
477
480
  }
478
481
 
479
482
  if (!arSupported && (mode === "immersive-ar" || mode === "ar")) {
480
- // const debugAppClip = getParam("debugappclip")
483
+
484
+ this.invokeSessionRequestStart("immersive-ar", init);
485
+
481
486
  // Forward to the AppClip experience (Using the apple.com url the appclip overlay shows immediately)
482
487
  // const url =`https://appclip.needle.tools/ar?url=${(location.href)}`;
483
488
  const url = new URL("https://appclip.apple.com/id?p=tools.needle.launch-app.Clip");
@@ -511,6 +516,10 @@ export class NeedleXRSession implements INeedleXRSession {
511
516
  else window.location.href = urlStr;
512
517
  }
513
518
 
519
+ setTimeout(() => {
520
+ this.invokeSessionRequestEnd("immersive-ar", init || {}, null);
521
+ }, 3000);
522
+
514
523
  return null;
515
524
  }
516
525
  }
@@ -549,9 +558,6 @@ export class NeedleXRSession implements INeedleXRSession {
549
558
 
550
559
  //performance.mark('NeedleXRSession start');
551
560
 
552
- // setup session init args, make sure we have default values
553
- if (!init) init = {};
554
-
555
561
  switch (mode) {
556
562
 
557
563
  // Setup VR initialization parameters
@@ -615,9 +621,7 @@ export class NeedleXRSession implements INeedleXRSession {
615
621
  script.onBeforeXR(mode, init);
616
622
  }
617
623
  }
618
- for (const listener of this._sessionRequestStartListeners) {
619
- listener({ mode, init });
620
- }
624
+ this.invokeSessionRequestStart(mode, init);
621
625
  if (debug) showBalloonMessage("Requesting " + mode + " session (" + Date.now() + ")");
622
626
  Telemetry.sendEvent(Context.Current, "xr", {
623
627
  action: "session_request",
@@ -637,9 +641,7 @@ export class NeedleXRSession implements INeedleXRSession {
637
641
  });
638
642
  this._currentSessionRequest = undefined;
639
643
  this._currentSessionRequestMode = null;
640
- for (const listener of this._sessionRequestEndListeners) {
641
- listener({ mode, init, newSession: newSession || null });
642
- }
644
+ this.invokeSessionRequestEnd(mode, init, newSession);
643
645
  if (!newSession) {
644
646
  console.warn("XR Session request was rejected");
645
647
  return null;
@@ -650,6 +652,17 @@ export class NeedleXRSession implements INeedleXRSession {
650
652
  return session;
651
653
  }
652
654
 
655
+ private static invokeSessionRequestStart(mode: XRSessionMode, init: XRSessionInit) {
656
+ for (const listener of this._sessionRequestStartListeners) {
657
+ listener({ mode, init });
658
+ }
659
+ }
660
+ private static invokeSessionRequestEnd(mode: XRSessionMode, init: XRSessionInit, session: XRSession | null | undefined | void) {
661
+ for (const listener of this._sessionRequestEndListeners) {
662
+ listener({ mode, init, newSession: session || null });
663
+ }
664
+ }
665
+
653
666
  static setSession(mode: XRSessionMode, session: XRSession, init: XRSessionInit, context: Context) {
654
667
  if (this._activeSession) {
655
668
  console.error("A XRSession is already running");
@@ -1,4 +1,5 @@
1
1
  import { AnimationAction, AnimationClip, AnimationMixer, LoopOnce, LoopRepeat } from "three";
2
+ import { AnimationUtils } from "../engine/engine_animation.js";
2
3
 
3
4
  import { Mathf } from "../engine/engine_math.js";
4
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -447,7 +448,7 @@ export class Animation extends Behaviour implements IAnimationComponent {
447
448
  action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
448
449
  }
449
450
  // If the animation is at the end, reset the time
450
- else if(action.time >= action.getClip().duration) {
451
+ else if (action.time >= action.getClip().duration) {
451
452
  action.time = 0;
452
453
  }
453
454
 
@@ -473,6 +474,8 @@ export class Animation extends Behaviour implements IAnimationComponent {
473
474
  action.paused = false;
474
475
  action.play();
475
476
 
477
+ window.requestAnimationFrame(() => AnimationUtils.testIfRootCanAnimate(action));
478
+
476
479
  if (debug) console.log("PLAY", action.getClip().name, action)
477
480
  const handle = new AnimationHandle(action, this.mixer!, options, _ => {
478
481
  this._handles.splice(this._handles.indexOf(handle), 1);
@@ -1,6 +1,7 @@
1
1
  import { AnimationAction, AnimationClip, AnimationMixer, AxesHelper, Euler, KeyframeTrack, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment } from "../engine/debug/index.js";
4
+ import { AnimationUtils } from "../engine/engine_animation.js";
4
5
  import { Mathf } from "../engine/engine_math.js";
5
6
  import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
6
7
  import { assign, SerializationContext, TypeSerializer } from "../engine/engine_serialization_core.js";
@@ -775,6 +776,8 @@ export class AnimatorController {
775
776
  else action.weight = 1;
776
777
  action.play();
777
778
 
779
+ window.requestAnimationFrame(() => AnimationUtils.testIfRootCanAnimate(action));
780
+
778
781
  if (this.rootMotionHandler) {
779
782
  this.rootMotionHandler.onStart(action);
780
783
  }