Needle Engine

Changes between version 3.41.2-beta.3 and 3.42.0-beta
Files changed (14) hide show
  1. src/engine/webcomponents/api.ts +2 -1
  2. src/engine-components/codegen/components.ts +1 -1
  3. src/engine/engine_context.ts +3 -1
  4. src/engine/engine_element_attributes.ts +2 -1
  5. src/engine/engine_element.ts +11 -0
  6. src/engine/engine_tonemapping.ts +155 -21
  7. src/engine/webcomponents/fonts.ts +6 -0
  8. src/engine/webcomponents/needle menu/needle-menu.ts +31 -19
  9. src/engine/codegen/register_types.ts +3 -3
  10. src/engine-components/export/usdz/USDZExporter.ts +1 -1
  11. src/engine-components/webxr/WebXR.ts +1 -1
  12. src/engine-components/webxr/WebXRButtons.ts +0 -240
  13. src/engine/webcomponents/needle-button.ts +167 -0
  14. src/engine/webcomponents/WebXRButtons.ts +254 -0
src/engine/webcomponents/api.ts CHANGED
@@ -2,4 +2,5 @@
2
2
 
3
3
  export { ButtonsFactory } from "./buttons.js"
4
4
  export * from "./icons.js"
5
- export { type NeedleMenuPostMessageModel } from "./needle menu/needle-menu.js"
5
+ export { type NeedleMenuPostMessageModel } from "./needle menu/needle-menu.js"
6
+ export { NeedleButtonElement } from "./needle-button.js"
src/engine-components/codegen/components.ts CHANGED
@@ -204,7 +204,7 @@
204
204
  export { WebARCameraBackground } from "../webxr/WebARCameraBackground.js";
205
205
  export { WebARSessionRoot } from "../webxr/WebARSessionRoot.js";
206
206
  export { WebXR } from "../webxr/WebXR.js";
207
- export { WebXRButtonFactory } from "../webxr/WebXRButtons.js";
207
+ export { WebXRButtonFactory } from "../../engine/webcomponents/WebXRButtons.js";
208
208
  export { WebXRImageTracking } from "../webxr/WebXRImageTracking.js";
209
209
  export { WebXRImageTrackingModel } from "../webxr/WebXRImageTracking.js";
210
210
  export { WebXRPlaneTracking } from "../webxr/WebXRPlaneTracking.js";
src/engine/engine_context.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  BufferGeometry, Cache, Camera, Clock, Color, DepthTexture, Group,
4
4
  Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
5
5
  PerspectiveCamera, RGBAFormat, Scene, SRGBColorSpace,
6
- Texture, WebGLRenderer, type WebGLRendererParameters, WebGLRenderTarget, type WebXRArrayCamera
6
+ Texture, WebGLRenderer, type WebGLRendererParameters, WebGLRenderTarget, type WebXRArrayCamera,
7
+ AgXToneMapping
7
8
  } from 'three';
8
9
  import * as Stats from 'three/examples/jsm/libs/stats.module.js';
9
10
 
@@ -475,6 +476,7 @@
475
476
  this.renderer.shadowMap.type = PCFSoftShadowMap;
476
477
  this.renderer.setSize(this.domWidth, this.domHeight);
477
478
  this.renderer.outputColorSpace = SRGBColorSpace;
479
+ this.renderer.toneMapping = AgXToneMapping;
478
480
  this.lodsManager.setRenderer(this.renderer);
479
481
 
480
482
  this.input.bindEvents();
src/engine/engine_element_attributes.ts CHANGED
@@ -56,7 +56,8 @@
56
56
  export type TonemappingAttributeOptions = "none" | "linear" | "neutral" | "agx";
57
57
  type RenderingAttributes = {
58
58
  "contactshadows"?: boolean,
59
- "tone-mapping"?: TonemappingAttributeOptions
59
+ "tone-mapping"?: TonemappingAttributeOptions,
60
+ "tone-mapping-exposure"?: number,
60
61
  }
61
62
 
62
63
  /**
src/engine/engine_element.ts CHANGED
@@ -39,6 +39,7 @@
39
39
  "dracoDecoderType",
40
40
  "ktx2DecoderPath",
41
41
  "tone-mapping",
42
+ "tone-mapping-exposure",
42
43
  ]
43
44
 
44
45
  // https://developers.google.com/web/fundamentals/web-components/customelements
@@ -301,6 +302,9 @@
301
302
  this.applyTonemapping();
302
303
  break;
303
304
  }
305
+ case "tone-mapping-exposure": {
306
+ this.applyTonemapping();
307
+ }
304
308
  }
305
309
  }
306
310
 
@@ -487,6 +491,13 @@
487
491
  console.warn("Invalid tone-mapping attribute: " + attribute);
488
492
  }
489
493
  }
494
+
495
+ const exposure = this.getAttribute("tone-mapping-exposure");
496
+ if (exposure !== null && exposure !== undefined) {
497
+ const value = parseFloat(exposure);
498
+ if (!isNaN(value))
499
+ this._context.renderer.toneMappingExposure = value;
500
+ }
490
501
  }
491
502
 
492
503
  private onXRSessionStarted = () => {
src/engine/engine_tonemapping.ts CHANGED
@@ -1,13 +1,19 @@
1
- import { Mesh, ShaderChunk } from "three";
1
+ import { ShaderChunk } from "three";
2
2
  import type { Context } from "./engine_setup";
3
3
 
4
- const neutralTonemappingStart = `vec3 NeutralToneMapping( vec3 color ) {`
5
- const neutralTonemappingEnd = `return mix(color, vec3(1, 1, 1), g);
6
- }`;
4
+ let patchedTonemapping = false;
5
+ export function patchTonemapping(_ctx?: Context) {
6
+ if (patchedTonemapping) return;
7
+ patchedTonemapping = true;
8
+ patchNeutral();
9
+ patchAGX();
10
+ }
11
+ patchTonemapping();
7
12
 
8
- // From https://github.com/google/model-viewer/pull/4495
9
- // Emmett's new 3D Commerce tone mapping function
10
- const commerceToneMapping = `
13
+ function patchNeutral() {
14
+ // From https://github.com/google/model-viewer/pull/4495
15
+ // Emmett's new 3D Commerce tone mapping function
16
+ const commerceToneMapping = `
11
17
  float startCompression = 0.8;
12
18
  float desaturation = 0.5;
13
19
  // Patched tonemapping function
@@ -28,29 +34,157 @@
28
34
  return mix(color, vec3(1, 1, 1), g);
29
35
  }
30
36
  `;
37
+ const startStr = `vec3 NeutralToneMapping( vec3 color ) {`
38
+ const endStr = `return mix(color, vec3(1, 1, 1), g);
39
+ }`;
31
40
 
41
+ // Patch Neutral
42
+ const startIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(startStr);
43
+ const endIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(endStr, startIndex);
44
+ if (startIndex >= 0 && endIndex >= 0) {
45
+ // get the old tonemapping
46
+ const existing = ShaderChunk.tonemapping_pars_fragment.substring(startIndex, endIndex + endStr.length);
47
+ // replace it with the new one
48
+ ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace(existing, commerceToneMapping);
32
49
 
50
+ }
51
+ }
33
52
 
34
53
 
35
- let patchedTonemapping = false;
54
+ function patchAGX() {
55
+ // From https://iolite-engine.com/blog_posts/minimal_agx_implementation
56
+ // Found via https://github.com/google/filament/pull/7236/files#diff-5fe2be2d109db1d5040bc1d96d167c58db4db1b4793816d3a1e90a5d5304af2cR260
57
+ const agxToneMapping = `
58
+ // 0: Default, 1: Golden, 2: Punchy
59
+ #define AGX_LOOK 0
36
60
 
37
- export function patchTonemapping(_ctx?: Context) {
38
- if (patchedTonemapping) return;
39
- patchedTonemapping = true;
61
+ vec3 userSlope = vec3(1.0);
62
+ vec3 userOffset = vec3(0.0);
63
+ vec3 userPower = vec3(1.0);
64
+ float userSaturation = 1.0;
40
65
 
41
- const neutralTonemappingStartIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(neutralTonemappingStart);
42
- const neutralTonemappingEndIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(neutralTonemappingEnd, neutralTonemappingStartIndex);
43
- if (neutralTonemappingStartIndex >= 0 && neutralTonemappingEndIndex >= 0) {
44
- // get the old tonemapping
45
- const existingNeutralTonemapping = ShaderChunk.tonemapping_pars_fragment.substring(neutralTonemappingStartIndex, neutralTonemappingEndIndex + neutralTonemappingEnd.length);
46
- // replace it with the new one
47
- ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace(existingNeutralTonemapping, commerceToneMapping);
48
- // console.log("Patched tonemapping\n", ShaderChunk.tonemapping_pars_fragment);
49
- // if (_ctx) resetShaders(_ctx);
66
+ // Mean error^2: 3.6705141e-06
67
+ vec3 _agxDefaultContrastApprox(vec3 x) {
68
+ vec3 x2 = x * x;
69
+ vec3 x4 = x2 * x2;
70
+
71
+ return + 15.5 * x4 * x2
72
+ - 40.14 * x4 * x
73
+ + 31.96 * x4
74
+ - 6.868 * x2 * x
75
+ + 0.4298 * x2
76
+ + 0.1191 * x
77
+ - 0.00232;
78
+ }
79
+
80
+ vec3 _agx(vec3 val) {
81
+ const mat3 agx_mat = mat3(
82
+ 0.842479062253094, 0.0423282422610123, 0.0423756549057051,
83
+ 0.0784335999999992, 0.878468636469772, 0.0784336,
84
+ 0.0792237451477643, 0.0791661274605434, 0.879142973793104);
85
+
86
+ const float min_ev = -12.47393f;
87
+ const float max_ev = 4.026069f;
88
+
89
+ // val = pow(val, vec3(2.2));
90
+
91
+ // Input transform (inset)
92
+ val = agx_mat * val;
93
+
94
+ // Log2 space encoding
95
+ val = clamp(log2(val), min_ev, max_ev);
96
+ val = (val - min_ev) / (max_ev - min_ev);
97
+
98
+ // Apply sigmoid function approximation
99
+ val = _agxDefaultContrastApprox(val);
100
+
101
+ return val;
102
+ }
103
+
104
+ vec3 _agxEotf(vec3 val) {
105
+ const mat3 agx_mat_inv = mat3(
106
+ 1.19687900512017, -0.0528968517574562, -0.0529716355144438,
107
+ -0.0980208811401368, 1.15190312990417, -0.0980434501171241,
108
+ -0.0990297440797205, -0.0989611768448433, 1.15107367264116);
109
+
110
+ // Inverse input transform (outset)
111
+ val = agx_mat_inv * val;
112
+
113
+ // sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display
114
+ // NOTE: We're linearizing the output here. Comment/adjust when
115
+ // *not* using a sRGB render target
116
+ val = pow(val, vec3(2.2));
117
+
118
+ return val;
119
+ }
120
+
121
+ vec3 _agxLook(vec3 val) {
122
+ const vec3 lw = vec3(0.2126, 0.7152, 0.0722);
123
+ float luma = dot(val, lw);
124
+
125
+ // Default
126
+ vec3 offset = vec3(0.0);
127
+ vec3 slope = vec3(1.0);
128
+ vec3 power = vec3(1.0);
129
+ float sat = 1.0;
130
+
131
+ #if AGX_LOOK == 1
132
+ // Golden
133
+ slope = vec3(1.0, 0.9, 0.5);
134
+ power = vec3(0.8);
135
+ sat = 0.8;
136
+ #elif AGX_LOOK == 2
137
+ // Punchy
138
+ slope = vec3(1.0);
139
+ power = vec3(1.35, 1.35, 1.35);
140
+ sat = 1.4;
141
+ #endif
142
+
143
+ // Needle
144
+ slope = vec3(1.05);
145
+ power = vec3(1.10, 1.10, 1.10);
146
+ sat = 1.15;
147
+
148
+ // User
149
+ // slope = userSlope;
150
+ // offset = userOffset;
151
+ // power = userPower;
152
+ // sat = userSaturation;
153
+
154
+ // ASC CDL
155
+ val = pow(val * slope + offset, power);
156
+ return luma + sat * (val - luma);
157
+ }
158
+
159
+
160
+ vec3 AgXToneMapping( vec3 color ) {
161
+ // apply AGX
162
+ color *= toneMappingExposure;
163
+ color = _agx(color);
164
+ color = _agxLook(color); // Optional
165
+ color = _agxEotf(color);
166
+ return color;
167
+ }`;
168
+
169
+ const startString = `vec3 AgXToneMapping( vec3 color ) {`;
170
+ const endString = ` return color;
171
+ }`;
172
+ const startIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(startString);
173
+ const endIndex = ShaderChunk.tonemapping_pars_fragment.indexOf(endString, startIndex);
174
+ if (startIndex >= 0 && endIndex >= 0) {
175
+ const existing = ShaderChunk.tonemapping_pars_fragment.substring(startIndex, endIndex + endString.length);
176
+ ShaderChunk.tonemapping_pars_fragment = ShaderChunk.tonemapping_pars_fragment.replace(existing, agxToneMapping);
50
177
  }
178
+
51
179
  }
52
- patchTonemapping();
53
180
 
181
+
182
+
183
+
184
+
185
+
186
+
187
+
54
188
  // function resetShaders(ctx: Context) {
55
189
  // const scene = ctx.scene;
56
190
  // const gl = ctx.renderer;
src/engine/webcomponents/fonts.ts CHANGED
@@ -34,3 +34,9 @@
34
34
  export function ensureFonts() {
35
35
  loadFont("https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap");
36
36
  }
37
+
38
+ // add to document head AND shadow dom to work
39
+ // using a static font because the variable font is quite large
40
+ // eslint-disable-next-line no-secrets/no-secrets
41
+ export const iconFontUrl = "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0";
42
+ // const fontUrl = "./include/fonts/MaterialSymbolsOutlined.css"; // for offline support
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { getParam, isMobileDevice } from "../../engine_utils.js";
6
6
  import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
7
7
  import { ButtonsFactory } from "../buttons.js";
8
- import { ensureFonts, loadFont } from "../fonts.js";
8
+ import { ensureFonts, iconFontUrl, loadFont } from "../fonts.js";
9
9
  import { getIconElement } from "../icons.js";
10
10
  import { NeedleLogoElement } from "../logo-element.js";
11
11
  import { NeedleSpatialMenu } from "./needle-menu-spatial.js";
@@ -204,13 +204,13 @@
204
204
  }
205
205
  if (!element) {
206
206
  element = NeedleMenuElement.create() as NeedleMenuElement;
207
- element._domElement = domElement;
208
- element._context = context;
209
207
  if (domElement.shadowRoot)
210
208
  domElement.shadowRoot.appendChild(element);
211
209
  else
212
210
  domElement.appendChild(element);
213
211
  }
212
+ element._domElement = domElement;
213
+ element._context = context;
214
214
  return element as NeedleMenuElement;
215
215
  }
216
216
 
@@ -263,7 +263,7 @@
263
263
  padding: 0 .3rem;
264
264
  }
265
265
 
266
- .wrapper > *, .options > * {
266
+ .wrapper > *, .options > button, ::slotted(*) {
267
267
  position: relative;
268
268
  border: none;
269
269
  border-radius: 0;
@@ -288,31 +288,33 @@
288
288
  text-decoration: none;
289
289
  }
290
290
 
291
- .options > * {
291
+ .options > button, ::slotted(button) {
292
+ height: 2.25rem;
292
293
  padding: .4rem .5rem;
293
294
  }
294
295
 
295
- :host .options > * {
296
+ :host .options > button, ::slotted(*) {
296
297
  background: transparent;
297
298
  border: none;
298
299
  white-space: nowrap;
299
300
  transition: all 0.1s linear .02s;
300
301
  border-radius: 0.8rem;
301
302
  }
302
- :host .options > *:hover {
303
+
304
+ :host .options > *:hover, ::slotted(*:hover) {
303
305
  cursor: pointer;
304
306
  color: black;
305
307
  background: rgba(245, 245, 245, .8);
306
308
  outline: rgba(0,0,0,.05) 1px solid;
307
309
  }
308
310
 
309
- :host .options > *:disabled {
311
+ :host .options > *:disabled, ::slotted(*:disabled) {
310
312
  background: rgba(0,0,0,.05);
311
313
  color: rgba(60,60,60,.7);
312
314
  pointer-events: none;
313
315
  }
314
316
 
315
- button {
317
+ button, ::slotted(button) {
316
318
  gap: 0.3rem;
317
319
  }
318
320
 
@@ -423,7 +425,12 @@
423
425
 
424
426
  <div id="root" class="bottom">
425
427
  <div class="wrapper">
426
- <div class="options"></div>
428
+ <div class="options">
429
+ <slot></slot>
430
+ </div>
431
+ <div class="options">
432
+ <slot name="end"></slot>
433
+ </div>
427
434
  <div class="logo">
428
435
  <span class="madewith">powered by</span>
429
436
  </div>
@@ -437,13 +444,8 @@
437
444
  // we need to add the icons to both the shadow dom as well as the HEAD to work
438
445
  // https://github.com/google/material-design-icons/issues/1165
439
446
  ensureFonts();
440
- // add to document head AND shadow dom to work
441
- // using a static font because the variable font is quite large
442
- // eslint-disable-next-line no-secrets/no-secrets
443
- const fontUrl = "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0";
444
- // const fontUrl = "./include/fonts/MaterialSymbolsOutlined.css"; // for offline support
445
- loadFont(fontUrl, { loadedCallback: () => { this.handleSizeChange() } });
446
- loadFont(fontUrl, { element: shadow });
447
+ loadFont(iconFontUrl, { loadedCallback: () => { this.handleSizeChange() } });
448
+ loadFont(iconFontUrl, { element: shadow });
447
449
 
448
450
  const content = template.content.cloneNode(true) as DocumentFragment;
449
451
  shadow?.appendChild(content);
@@ -691,8 +693,18 @@
691
693
  get hasAnyVisibleOptions() {
692
694
  // do we have any visible buttons?
693
695
  for (let i = 0; i < this.options.children.length; i++) {
694
- const child = this.options.children[i] as HTMLElement;
695
- if (child.style.display != "none") return true;
696
+ const child = this.options.children[i] as HTMLElement
697
+ // is slot?
698
+ if (child.tagName === "SLOT") {
699
+ const slotElement = child as HTMLSlotElement;
700
+ const nodes = slotElement.assignedNodes();
701
+ for (const node of nodes) {
702
+ if (node instanceof HTMLElement) {
703
+ if (node.style.display != "none") return true;
704
+ }
705
+ }
706
+ }
707
+ else if (child.style.display != "none") return true;
696
708
  }
697
709
  return false;
698
710
  }
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";
@@ -209,7 +209,7 @@
209
209
  import { WebARCameraBackground } from "../../engine-components/webxr/WebARCameraBackground.js";
210
210
  import { WebARSessionRoot } from "../../engine-components/webxr/WebARSessionRoot.js";
211
211
  import { WebXR } from "../../engine-components/webxr/WebXR.js";
212
- import { WebXRButtonFactory } from "../../engine-components/webxr/WebXRButtons.js";
212
+ import { WebXRButtonFactory } from "../webcomponents/WebXRButtons.js";
213
213
  import { WebXRImageTracking } from "../../engine-components/webxr/WebXRImageTracking.js";
214
214
  import { WebXRImageTrackingModel } from "../../engine-components/webxr/WebXRImageTracking.js";
215
215
  import { WebXRPlaneTracking } from "../../engine-components/webxr/WebXRPlaneTracking.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/export/usdz/USDZExporter.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { SpriteRenderer } from "../../SpriteRenderer.js";
12
12
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
13
13
  import { WebXR } from "../../webxr/WebXR.js";
14
- import { WebXRButtonFactory } from "../../webxr/WebXRButtons.js";
14
+ import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
15
15
  import { XRState, XRStateFlag } from "../../webxr/XRFlag.js";
16
16
  import type { IUSDExporterExtension } from "./Extension.js";
17
17
  import { AnimationExtension } from "./extensions/Animation.js"
src/engine-components/webxr/WebXR.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  import { XRControllerModel } from "./controllers/XRControllerModel.js";
18
18
  import { XRControllerMovement } from "./controllers/XRControllerMovement.js";
19
19
  import { WebARSessionRoot } from "./WebARSessionRoot.js";
20
- import { WebXRButtonFactory } from "./WebXRButtons.js";
20
+ import { WebXRButtonFactory } from "../../engine/webcomponents/WebXRButtons.js";
21
21
  import { XRState, XRStateFlag } from "./XRFlag.js";
22
22
 
23
23
  const debug = getParam("debugwebxr");
src/engine-components/webxr/WebXRButtons.ts DELETED
@@ -1,240 +0,0 @@
1
- import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
2
- import { isMozillaXR } from "../../engine/engine_utils.js";
3
- import { NeedleXRSession } from "../../engine/engine_xr.js";
4
- import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
5
- import { getIconElement } from "../../engine/webcomponents/icons.js";
6
- import { onXRSessionEnd, onXRSessionStart } from "../../engine/xr/events.js";
7
- import { GameObject } from "../Component.js";
8
- import { USDZExporter } from "../export/usdz/USDZExporter.js";
9
-
10
- // TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested)
11
-
12
- /**
13
- * Factory to create WebXR buttons for AR, VR, Quicklook and Send to Quest
14
- * The buttons are created as HTMLButtonElements and can be added to the DOM.
15
- * The buttons will automatically hide when a XR session is started and show again when the session ends.
16
- */
17
- export class WebXRButtonFactory {
18
-
19
- private static _instance: WebXRButtonFactory;
20
- private static create() {
21
- return new WebXRButtonFactory();
22
- }
23
-
24
- static getOrCreate() {
25
- if (!this._instance) {
26
- this._instance = this.create();
27
- }
28
- return this._instance;
29
- }
30
-
31
- private get isSecureConnection() { return window.location.protocol === "https:"; }
32
-
33
-
34
- get quicklookButton() { return this._quicklookButton }
35
- private _quicklookButton?: HTMLButtonElement;
36
-
37
- get arButton() { return this._arButton; }
38
- private _arButton?: HTMLButtonElement;
39
-
40
- get vrButton() { return this._vrButton }
41
- private _vrButton?: HTMLButtonElement;
42
-
43
- get sendToQuestButton() { return this._sendToQuestButton; }
44
- private _sendToQuestButton?: HTMLButtonElement;
45
-
46
- get qrButton() { return ButtonsFactory.getOrCreate().createQRCode(); }
47
-
48
- /** get or create the quicklook button
49
- * Behaviour of the button:
50
- * - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
51
- */
52
- createQuicklookButton(): HTMLButtonElement {
53
- if (this._quicklookButton) return this._quicklookButton;
54
-
55
- const button = document.createElement("button");
56
- this._quicklookButton = button;
57
- button.dataset["needle"] = "quicklook-button";
58
- button.innerText = "Open in Quicklook";
59
- button.prepend(getIconElement("view_in_ar"));
60
- button.addEventListener("click", () => {
61
- const usdzExporter = GameObject.findObjectOfType(USDZExporter);
62
- if (usdzExporter) {
63
- button.classList.add("this-mode-is-requested");
64
- usdzExporter.exportAndOpen().then(() => {
65
- button.classList.remove("this-mode-is-requested");
66
- }).catch(err => {
67
- button.classList.remove("this-mode-is-requested");
68
- console.error(err);
69
- });
70
- }
71
- else {
72
- console.warn("No USDZExporter component found in the scene");
73
- }
74
- });
75
- this.hideElementDuringXRSession(button);
76
- return button;
77
- }
78
- /** get or create the WebXR AR button
79
- * @param init optional session init options
80
- * Behaviour of the button:
81
- * - if the device supports AR, the button will be visible and clickable
82
- * - if the device does not support AR, the button will be hidden
83
- * - if the device changes and now supports AR, the button will be visible
84
- */
85
- createARButton(init?: XRSessionInit): HTMLButtonElement {
86
- if (this._arButton) return this._arButton;
87
-
88
- const mode: XRSessionMode = "immersive-ar";
89
- const button = document.createElement("button");
90
- this._arButton = button;
91
-
92
- button.classList.add("webxr-button");
93
- button.dataset["needle"] = "webxr-ar-button";
94
- button.innerText = "Enter AR";
95
- button.prepend(getIconElement("view_in_ar"))
96
- button.title = "Click to start an AR session";
97
- button.addEventListener("click", () => NeedleXRSession.start(mode, init));
98
- this.updateSessionSupported(button, mode);
99
- this.listenToXRSessionState(button, mode);
100
- this.hideElementDuringXRSession(button);
101
-
102
- if (!this.isSecureConnection) {
103
- button.disabled = true;
104
- button.title = "WebXR requires a secure connection (HTTPS)";
105
- }
106
-
107
- if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
108
- navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
109
-
110
- return button;
111
- }
112
-
113
- /** get or create the WebXR VR button
114
- * @param init optional session init options
115
- * Behaviour of the button:
116
- * - if the device supports VR, the button will be visible and clickable
117
- * - if the device does not support VR, the button will be hidden
118
- * - if the device changes and now supports VR, the button will be visible
119
- */
120
- createVRButton(init?: XRSessionInit): HTMLButtonElement {
121
- if (this._vrButton) return this._vrButton;
122
-
123
- const mode: XRSessionMode = "immersive-vr";
124
- const button = document.createElement("button");
125
- this._vrButton = button;
126
- button.classList.add("webxr-button");
127
- button.dataset["needle"] = "webxr-vr-button";
128
- button.innerText = "Enter VR";
129
- button.prepend(getIconElement("panorama_photosphere"));
130
- button.title = "Click to start a VR session";
131
- button.addEventListener("click", () => NeedleXRSession.start(mode, init));
132
- this.updateSessionSupported(button, mode);
133
- this.listenToXRSessionState(button, mode);
134
- this.hideElementDuringXRSession(button);
135
-
136
- if (!this.isSecureConnection) {
137
- button.disabled = true;
138
- button.title = "WebXR requires a secure connection (HTTPS)";
139
- }
140
-
141
- if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
142
- navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
143
-
144
- return button;
145
- }
146
-
147
- /** get or create the Send To Quest button
148
- * Behaviour of the button:
149
- * - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
150
- */
151
- createSendToQuestButton(): HTMLButtonElement {
152
- if (this._sendToQuestButton) return this._sendToQuestButton;
153
- const baseUrl = `https://oculus.com/open_url/?url=`
154
- const button = document.createElement("button");
155
- this._sendToQuestButton = button;
156
- button.dataset["needle"] = "webxr-sendtoquest-button";
157
- button.innerText = "Open on Quest";
158
- button.prepend(getIconElement("share_windows"));
159
- button.title = "Click to send this page to the Oculus Browser on your Quest";
160
- button.addEventListener("click", () => {
161
- const urlParameter = encodeURIComponent(window.location.href);
162
- const url = baseUrl + urlParameter;
163
- if (window.open(url) == null) {
164
- showBalloonMessage("This page doesn't allow popups. Please paste " + url + " into your browser.");
165
- }
166
- });
167
- this.listenToXRSessionState(button);
168
- this.hideElementDuringXRSession(button);
169
- // make sure to hide the button when we have VR support directly on the device
170
- if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
171
- navigator.xr?.addEventListener("devicechange", () => {
172
- if (navigator.xr?.isSessionSupported("immersive-vr")) {
173
- button.style.display = "none";
174
- }
175
- else {
176
- button.style.display = "";
177
- }
178
- });
179
- }
180
- return button;
181
- }
182
-
183
- /**
184
- * @deprecated please use ButtonsFactory.getOrCreate().createQRCode(). This method will be removed in a future update
185
- */
186
- createQRCode(): HTMLButtonElement {
187
- return ButtonsFactory.getOrCreate().createQRCode();
188
- }
189
-
190
- private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
191
- if (!("xr" in navigator)) {
192
- button.style.display = "none";
193
- return;
194
- }
195
- NeedleXRSession.isSessionSupported(mode).then(supported => {
196
- button.style.display = !supported ? "none" : "";
197
- if (isDevEnvironment() && !supported) console.log("[WebXR] \"" + mode + "\" is not supported on this device – make sure your server runs using HTTPS and you have a device connected that supports " + mode);
198
- });
199
- }
200
-
201
- private hideElementDuringXRSession(element: HTMLElement) {
202
- onXRSessionStart(_ => {
203
- element["previous-display"] = element.style.display;
204
- element.style.display = "none";
205
- });
206
- onXRSessionEnd(_ => {
207
- if (element["previous-display"] != undefined)
208
- element.style.display = element["previous-display"];
209
- });
210
- }
211
-
212
- private listenToXRSessionState(button: HTMLButtonElement, mode?: XRSessionMode) {
213
-
214
- if (mode) {
215
- NeedleXRSession.onSessionRequestStart(args => {
216
- if (args.mode === mode) {
217
- button.classList.add("this-mode-is-requested");
218
- // button["original-text"] = button.innerText;
219
- // let modeText = mode === "immersive-vr" ? "VR" : "AR";
220
- // button.innerText = "Starting " + modeText + "...";
221
- }
222
- else {
223
- button["was-disabled"] = button.disabled;
224
- button.disabled = true;
225
- button.classList.add("other-mode-is-requested");
226
- }
227
- });
228
- NeedleXRSession.onSessionRequestEnd(_ => {
229
- button.classList.remove("this-mode-is-requested");
230
- button.classList.remove("other-mode-is-requested");
231
- button.disabled = button["was-disabled"];
232
- // button.innerText = button["original-text"];
233
- });
234
- }
235
- }
236
- }
237
-
238
-
239
- /** @deprecated please use WebXRButtonFactory. This type will be removed in a future update */
240
- export type NeedleWebXRHtmlElement = WebXRButtonFactory;
src/engine/webcomponents/needle-button.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { WebXRButtonFactory } from "./WebXRButtons.js";
2
+ import { iconFontUrl, loadFont } from "./fonts.js";
3
+
4
+ const htmlTagName = "needle-button";
5
+
6
+ /**
7
+ * A <needle-button> can be used to simply add VR, AR or Quicklook buttons to your website without having to write any code.
8
+ * @example
9
+ * ```html
10
+ * <needle-button ar></needle-button>
11
+ * <needle-button vr></needle-button>
12
+ * <needle-button quicklook></needle-button>
13
+ * ```
14
+ *
15
+ * @example custom label
16
+ * ```html
17
+ * <needle-button ar>Start AR</needle-button>
18
+ * <needle-button vr>Start VR</needle-button>
19
+ * <needle-button quicklook>View in AR</needle-button>
20
+ * ```
21
+ *
22
+ * @example custom styling
23
+ * ```html
24
+ * <!-- You can either style the element directly or use a CSS stylesheet -->
25
+ * <style>
26
+ * needle-button {
27
+ * background-color: red;
28
+ * color: white;
29
+ * }
30
+ * </style>
31
+ * <needle-button ar>Start AR</needle-button>
32
+ * ```
33
+ */
34
+ export class NeedleButtonElement extends HTMLElement {
35
+
36
+ static observedAttributes = ["ar", "vr", "quicklook"];
37
+
38
+ attributeChangedCallback(_name: string, _oldValue: string, _newValue: string) {
39
+ this.#update()
40
+ }
41
+
42
+ #root!: ShadowRoot;
43
+ #slot!: HTMLSlotElement;
44
+ /** These are the default styles that can be overridden by the user from the outside by styling <needle-button> */
45
+ #styles!: HTMLStyleElement;
46
+
47
+ /** This is the button that was generated using one of the factories */
48
+ #button: HTMLButtonElement | undefined;
49
+ /** If AR or VR is requested we create and use the webxr button factory to create a button with default behaviour */
50
+ #webxrfactory: WebXRButtonFactory | undefined;
51
+
52
+ #observer: MutationObserver | undefined;
53
+
54
+ #update() {
55
+ this.#button?.remove();
56
+
57
+ if (this.getAttribute("ar") != null) {
58
+ this.#webxrfactory ??= new WebXRButtonFactory()
59
+ this.#button = this.#webxrfactory.createARButton();
60
+ }
61
+ else if (this.getAttribute("vr") != null) {
62
+ this.#webxrfactory ??= new WebXRButtonFactory()
63
+ this.#button = this.#webxrfactory.createVRButton();
64
+ }
65
+ else if (this.getAttribute("quicklook") != null) {
66
+ this.#webxrfactory ??= new WebXRButtonFactory()
67
+ this.#button = this.#webxrfactory.createQuicklookButton();
68
+ }
69
+ else {
70
+ return;
71
+ }
72
+
73
+ this.#root ??= this.attachShadow({ mode: "open" });
74
+ this.#slot ??= document.createElement("slot");
75
+ this.#styles ??= document.createElement("style");
76
+ this.#styles.innerHTML = `
77
+ button {
78
+ all: initial;
79
+ cursor: inherit;
80
+ color: inherit;
81
+ font-family: inherit;
82
+ gap: inherit;
83
+ white-space: nowrap;
84
+ }
85
+ `;
86
+ const hasUnstyledAttribute = this.getAttribute("unstyled") != undefined;
87
+ if (!hasUnstyledAttribute) {
88
+ this.#styles.innerHTML += `
89
+ :host {
90
+ display: inline-block;
91
+ background: rgba(255, 255, 255, .8);
92
+ backdrop-filter: blur(10px);
93
+ width: fit-content;
94
+ transition: background .2s;
95
+
96
+ cursor: pointer;
97
+ padding: 0.4rem .5rem;
98
+ border-radius: 0.8rem;
99
+ color: black;
100
+ background: rgba(245, 245, 245, .8);
101
+ outline: rgba(0,0,0,.05) 1px solid;
102
+ }
103
+ :host(:hover) {
104
+ background: rgba(255, 255, 255, 1);
105
+ transition: background .2s;
106
+ }
107
+ slot {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ gap: .5rem;
112
+ }
113
+ `
114
+ }
115
+
116
+ /**
117
+ * We now structure the results as follows:
118
+ * <button>
119
+ * <slot>
120
+ * <original_button_content>
121
+ * </slot>
122
+ * </button>
123
+ */
124
+ this.#slot.innerHTML = this.#button.innerHTML;
125
+ this.#slot.style.cssText = `display: flex; align-items: center; justify-content: center;`
126
+ this.#button.innerHTML = this.#slot.outerHTML;
127
+ this.#root.innerHTML = this.#button.outerHTML;
128
+ this.#root.prepend(this.#styles);
129
+ loadFont(iconFontUrl, { element: this.#root });
130
+
131
+ this.#observer?.disconnect();
132
+ this.#observer ??= new MutationObserver(() => this.#updateVisibility());
133
+ this.#observer.observe(this.#button, { attributes: true });
134
+ }
135
+
136
+ #updateVisibility() {
137
+ if (this.#button) {
138
+ // if the user has set any display style don't override it
139
+ if (this.style.display?.length) {
140
+ return;
141
+ }
142
+ console.log(this.#button.style.display);
143
+ if (this.#button.style.display === "none") {
144
+ this.style.display = "none";
145
+ }
146
+ else {
147
+ this.style.display = "";
148
+ }
149
+ }
150
+ }
151
+
152
+ override onclick = (_ev: MouseEvent) => {
153
+
154
+ if (_ev.defaultPrevented) return;
155
+
156
+ if (this.#button) {
157
+ this.#button.click()
158
+ }
159
+
160
+ }
161
+
162
+ }
163
+
164
+
165
+
166
+ if (typeof window !== "undefined" && !window.customElements.get(htmlTagName))
167
+ window.customElements.define(htmlTagName, NeedleButtonElement);
src/engine/webcomponents/WebXRButtons.ts ADDED
@@ -0,0 +1,254 @@
1
+ import { isDevEnvironment, showBalloonMessage } from "../debug/index.js";
2
+ import { isMozillaXR } from "../engine_utils.js";
3
+ import { NeedleXRSession } from "../engine_xr.js";
4
+ import { ButtonsFactory } from "./buttons.js";
5
+ import { getIconElement } from "./icons.js";
6
+ import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
7
+ import { findObjectOfType } from "../engine_components.js";
8
+ import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
9
+ import { Context } from "../engine_setup.js";
10
+
11
+ // TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested)
12
+
13
+ /**
14
+ * Factory to create WebXR buttons for AR, VR, Quicklook and Send to Quest
15
+ * The buttons are created as HTMLButtonElements and can be added to the DOM.
16
+ * The buttons will automatically hide when a XR session is started and show again when the session ends.
17
+ */
18
+ export class WebXRButtonFactory {
19
+
20
+ private static _instance: WebXRButtonFactory;
21
+ private static create() {
22
+ return new WebXRButtonFactory();
23
+ }
24
+
25
+ static getOrCreate() {
26
+ if (!this._instance) {
27
+ this._instance = this.create();
28
+ }
29
+ return this._instance;
30
+ }
31
+
32
+ private get isSecureConnection() { return window.location.protocol === "https:"; }
33
+
34
+
35
+ get quicklookButton() { return this._quicklookButton }
36
+ private _quicklookButton?: HTMLButtonElement;
37
+
38
+ get arButton() { return this._arButton; }
39
+ private _arButton?: HTMLButtonElement;
40
+
41
+ get vrButton() { return this._vrButton }
42
+ private _vrButton?: HTMLButtonElement;
43
+
44
+ get sendToQuestButton() { return this._sendToQuestButton; }
45
+ private _sendToQuestButton?: HTMLButtonElement;
46
+
47
+ get qrButton() { return ButtonsFactory.getOrCreate().createQRCode(); }
48
+
49
+ /** get or create the quicklook button
50
+ * Behaviour of the button:
51
+ * - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
52
+ */
53
+ createQuicklookButton(): HTMLButtonElement {
54
+ if (this._quicklookButton) return this._quicklookButton;
55
+
56
+ const button = document.createElement("button");
57
+ this._quicklookButton = button;
58
+ button.dataset["needle"] = "quicklook-button";
59
+ button.innerText = "Open in Quicklook";
60
+ button.prepend(getIconElement("view_in_ar"));
61
+
62
+ let createdExporter = false;
63
+ let usdzExporter: USDZExporter | null = null;
64
+ button.addEventListener("click", () => {
65
+ usdzExporter = findObjectOfType(USDZExporter);
66
+
67
+ // if the scene doesnt have an USDZExporter component, create one
68
+ if (!usdzExporter) {
69
+ createdExporter = true;
70
+ usdzExporter = new USDZExporter();
71
+ }
72
+ // if we have created a USDZExporter
73
+ if (createdExporter)
74
+ usdzExporter.objectToExport = Context.Current.scene;
75
+
76
+ if (usdzExporter) {
77
+ button.classList.add("this-mode-is-requested");
78
+ usdzExporter.exportAndOpen().then(() => {
79
+ button.classList.remove("this-mode-is-requested");
80
+ }).catch(err => {
81
+ button.classList.remove("this-mode-is-requested");
82
+ console.error(err);
83
+ });
84
+ }
85
+ else {
86
+ console.warn("No USDZExporter component found in the scene");
87
+ }
88
+ });
89
+ this.hideElementDuringXRSession(button);
90
+ return button;
91
+ }
92
+ /** get or create the WebXR AR button
93
+ * @param init optional session init options
94
+ * Behaviour of the button:
95
+ * - if the device supports AR, the button will be visible and clickable
96
+ * - if the device does not support AR, the button will be hidden
97
+ * - if the device changes and now supports AR, the button will be visible
98
+ */
99
+ createARButton(init?: XRSessionInit): HTMLButtonElement {
100
+ if (this._arButton) return this._arButton;
101
+
102
+ const mode: XRSessionMode = "immersive-ar";
103
+ const button = document.createElement("button");
104
+ this._arButton = button;
105
+
106
+ button.classList.add("webxr-button");
107
+ button.dataset["needle"] = "webxr-ar-button";
108
+ button.innerText = "Enter AR";
109
+ button.prepend(getIconElement("view_in_ar"))
110
+ button.title = "Click to start an AR session";
111
+ button.addEventListener("click", () => NeedleXRSession.start(mode, init));
112
+ this.updateSessionSupported(button, mode);
113
+ this.listenToXRSessionState(button, mode);
114
+ this.hideElementDuringXRSession(button);
115
+
116
+ if (!this.isSecureConnection) {
117
+ button.disabled = true;
118
+ button.title = "WebXR requires a secure connection (HTTPS)";
119
+ }
120
+
121
+ if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
122
+ navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
123
+
124
+ return button;
125
+ }
126
+
127
+ /** get or create the WebXR VR button
128
+ * @param init optional session init options
129
+ * Behaviour of the button:
130
+ * - if the device supports VR, the button will be visible and clickable
131
+ * - if the device does not support VR, the button will be hidden
132
+ * - if the device changes and now supports VR, the button will be visible
133
+ */
134
+ createVRButton(init?: XRSessionInit): HTMLButtonElement {
135
+ if (this._vrButton) return this._vrButton;
136
+
137
+ const mode: XRSessionMode = "immersive-vr";
138
+ const button = document.createElement("button");
139
+ this._vrButton = button;
140
+ button.classList.add("webxr-button");
141
+ button.dataset["needle"] = "webxr-vr-button";
142
+ button.innerText = "Enter VR";
143
+ button.prepend(getIconElement("panorama_photosphere"));
144
+ button.title = "Click to start a VR session";
145
+ button.addEventListener("click", () => NeedleXRSession.start(mode, init));
146
+ this.updateSessionSupported(button, mode);
147
+ this.listenToXRSessionState(button, mode);
148
+ this.hideElementDuringXRSession(button);
149
+
150
+ if (!this.isSecureConnection) {
151
+ button.disabled = true;
152
+ button.title = "WebXR requires a secure connection (HTTPS)";
153
+ }
154
+
155
+ if (!isMozillaXR()) // WebXR Viewer can't attach events before session start
156
+ navigator.xr?.addEventListener("devicechange", () => this.updateSessionSupported(button, mode));
157
+
158
+ return button;
159
+ }
160
+
161
+ /** get or create the Send To Quest button
162
+ * Behaviour of the button:
163
+ * - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
164
+ */
165
+ createSendToQuestButton(): HTMLButtonElement {
166
+ if (this._sendToQuestButton) return this._sendToQuestButton;
167
+ const baseUrl = `https://oculus.com/open_url/?url=`
168
+ const button = document.createElement("button");
169
+ this._sendToQuestButton = button;
170
+ button.dataset["needle"] = "webxr-sendtoquest-button";
171
+ button.innerText = "Open on Quest";
172
+ button.prepend(getIconElement("share_windows"));
173
+ button.title = "Click to send this page to the Oculus Browser on your Quest";
174
+ button.addEventListener("click", () => {
175
+ const urlParameter = encodeURIComponent(window.location.href);
176
+ const url = baseUrl + urlParameter;
177
+ if (window.open(url) == null) {
178
+ showBalloonMessage("This page doesn't allow popups. Please paste " + url + " into your browser.");
179
+ }
180
+ });
181
+ this.listenToXRSessionState(button);
182
+ this.hideElementDuringXRSession(button);
183
+ // make sure to hide the button when we have VR support directly on the device
184
+ if (!isMozillaXR()) { // WebXR Viewer can't attach events before session start
185
+ navigator.xr?.addEventListener("devicechange", () => {
186
+ if (navigator.xr?.isSessionSupported("immersive-vr")) {
187
+ button.style.display = "none";
188
+ }
189
+ else {
190
+ button.style.display = "";
191
+ }
192
+ });
193
+ }
194
+ return button;
195
+ }
196
+
197
+ /**
198
+ * @deprecated please use ButtonsFactory.getOrCreate().createQRCode(). This method will be removed in a future update
199
+ */
200
+ createQRCode(): HTMLButtonElement {
201
+ return ButtonsFactory.getOrCreate().createQRCode();
202
+ }
203
+
204
+ private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
205
+ if (!("xr" in navigator)) {
206
+ button.style.display = "none";
207
+ return;
208
+ }
209
+ NeedleXRSession.isSessionSupported(mode).then(supported => {
210
+ button.style.display = !supported ? "none" : "";
211
+ if (isDevEnvironment() && !supported) console.log("[WebXR] \"" + mode + "\" is not supported on this device – make sure your server runs using HTTPS and you have a device connected that supports " + mode);
212
+ });
213
+ }
214
+
215
+ private hideElementDuringXRSession(element: HTMLElement) {
216
+ onXRSessionStart(_ => {
217
+ element["previous-display"] = element.style.display;
218
+ element.style.display = "none";
219
+ });
220
+ onXRSessionEnd(_ => {
221
+ if (element["previous-display"] != undefined)
222
+ element.style.display = element["previous-display"];
223
+ });
224
+ }
225
+
226
+ private listenToXRSessionState(button: HTMLButtonElement, mode?: XRSessionMode) {
227
+
228
+ if (mode) {
229
+ NeedleXRSession.onSessionRequestStart(args => {
230
+ if (args.mode === mode) {
231
+ button.classList.add("this-mode-is-requested");
232
+ // button["original-text"] = button.innerText;
233
+ // let modeText = mode === "immersive-vr" ? "VR" : "AR";
234
+ // button.innerText = "Starting " + modeText + "...";
235
+ }
236
+ else {
237
+ button["was-disabled"] = button.disabled;
238
+ button.disabled = true;
239
+ button.classList.add("other-mode-is-requested");
240
+ }
241
+ });
242
+ NeedleXRSession.onSessionRequestEnd(_ => {
243
+ button.classList.remove("this-mode-is-requested");
244
+ button.classList.remove("other-mode-is-requested");
245
+ button.disabled = button["was-disabled"];
246
+ // button.innerText = button["original-text"];
247
+ });
248
+ }
249
+ }
250
+ }
251
+
252
+
253
+ /** @deprecated please use WebXRButtonFactory. This type will be removed in a future update */
254
+ export type NeedleWebXRHtmlElement = WebXRButtonFactory;