@@ -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"
|
@@ -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 "
|
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";
|
@@ -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();
|
@@ -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
|
/**
|
@@ -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 = () => {
|
@@ -1,13 +1,19 @@
|
|
1
|
-
import {
|
1
|
+
import { ShaderChunk } from "three";
|
2
2
|
import type { Context } from "./engine_setup";
|
3
3
|
|
4
|
-
|
5
|
-
|
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
|
-
|
9
|
-
//
|
10
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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;
|
@@ -34,3 +34,9 @@
|
|
34
34
|
export function ensureFonts() {
|
35
35
|
loadFont("https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,[email protected],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
|
@@ -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
|
-
|
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"
|
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
|
-
|
441
|
-
|
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
|
-
|
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
|
}
|
@@ -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 "
|
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);
|
@@ -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 "
|
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"
|
@@ -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 "
|
20
|
+
import { WebXRButtonFactory } from "../../engine/webcomponents/WebXRButtons.js";
|
21
21
|
import { XRState, XRStateFlag } from "./XRFlag.js";
|
22
22
|
|
23
23
|
const debug = getParam("debugwebxr");
|
@@ -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;
|
@@ -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);
|
@@ -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;
|