@worldmesh/runtime 0.2.0 → 0.2.2
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.
- package/README.md +33 -3
- package/dist/audio/footsteps.d.ts +37 -0
- package/dist/audio/footsteps.d.ts.map +1 -0
- package/dist/audio/footsteps.js +178 -0
- package/dist/audio/footsteps.js.map +1 -0
- package/dist/avatar/descriptor.d.ts +15 -0
- package/dist/avatar/descriptor.d.ts.map +1 -1
- package/dist/avatar/descriptor.js +19 -0
- package/dist/avatar/descriptor.js.map +1 -1
- package/dist/avatar/loader.d.ts +12 -1
- package/dist/avatar/loader.d.ts.map +1 -1
- package/dist/avatar/loader.js +16 -1
- package/dist/avatar/loader.js.map +1 -1
- package/dist/avatar/share.d.ts +30 -0
- package/dist/avatar/share.d.ts.map +1 -0
- package/dist/avatar/share.js +203 -0
- package/dist/avatar/share.js.map +1 -0
- package/dist/camera/cameraRig.d.ts +4 -0
- package/dist/camera/cameraRig.d.ts.map +1 -1
- package/dist/camera/cameraRig.js +10 -1
- package/dist/camera/cameraRig.js.map +1 -1
- package/dist/camera/viewHandoff.d.ts +20 -0
- package/dist/camera/viewHandoff.d.ts.map +1 -0
- package/dist/camera/viewHandoff.js +64 -0
- package/dist/camera/viewHandoff.js.map +1 -0
- package/dist/controls/input.d.ts +29 -0
- package/dist/controls/input.d.ts.map +1 -1
- package/dist/controls/input.js +124 -1
- package/dist/controls/input.js.map +1 -1
- package/dist/controls/touch.d.ts +10 -0
- package/dist/controls/touch.d.ts.map +1 -1
- package/dist/controls/touch.js +57 -1
- package/dist/controls/touch.js.map +1 -1
- package/dist/core/worldmesh.d.ts +9 -0
- package/dist/core/worldmesh.d.ts.map +1 -1
- package/dist/core/worldmesh.js +498 -26
- package/dist/core/worldmesh.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/net/follow.d.ts +7 -0
- package/dist/net/follow.d.ts.map +1 -0
- package/dist/net/follow.js +10 -0
- package/dist/net/follow.js.map +1 -0
- package/dist/net/presence.d.ts +126 -4
- package/dist/net/presence.d.ts.map +1 -1
- package/dist/net/presence.js +472 -25
- package/dist/net/presence.js.map +1 -1
- package/dist/net/roomFull.d.ts +44 -0
- package/dist/net/roomFull.d.ts.map +1 -0
- package/dist/net/roomFull.js +147 -0
- package/dist/net/roomFull.js.map +1 -0
- package/dist/player/avatar.d.ts +17 -4
- package/dist/player/avatar.d.ts.map +1 -1
- package/dist/player/avatar.js +85 -18
- package/dist/player/avatar.js.map +1 -1
- package/dist/player/player.d.ts +6 -2
- package/dist/player/player.d.ts.map +1 -1
- package/dist/player/player.js +13 -3
- package/dist/player/player.js.map +1 -1
- package/dist/player/stroke.d.ts +11 -0
- package/dist/player/stroke.d.ts.map +1 -0
- package/dist/player/stroke.js +96 -0
- package/dist/player/stroke.js.map +1 -0
- package/dist/portals/portals.d.ts +1 -1
- package/dist/portals/portals.d.ts.map +1 -1
- package/dist/portals/portals.js +6 -2
- package/dist/portals/portals.js.map +1 -1
- package/dist/types.d.ts +53 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlay.d.ts +18 -0
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +166 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/xr/session.d.ts +13 -0
- package/dist/xr/session.d.ts.map +1 -0
- package/dist/xr/session.js +33 -0
- package/dist/xr/session.js.map +1 -0
- package/package.json +1 -1
- package/src/audio/footsteps.ts +185 -0
- package/src/avatar/descriptor.ts +28 -0
- package/src/avatar/loader.ts +19 -2
- package/src/avatar/share.ts +196 -0
- package/src/camera/cameraRig.ts +10 -1
- package/src/camera/viewHandoff.ts +67 -0
- package/src/controls/input.ts +119 -1
- package/src/controls/touch.ts +59 -1
- package/src/core/worldmesh.ts +495 -25
- package/src/index.ts +14 -7
- package/src/net/follow.ts +9 -0
- package/src/net/presence.ts +548 -28
- package/src/net/roomFull.ts +160 -0
- package/src/player/avatar.ts +87 -18
- package/src/player/player.ts +13 -4
- package/src/player/stroke.ts +93 -0
- package/src/portals/portals.ts +5 -2
- package/src/types.ts +52 -9
- package/src/ui/overlay.ts +171 -1
- package/src/xr/session.ts +33 -0
package/dist/core/worldmesh.js
CHANGED
|
@@ -1,24 +1,49 @@
|
|
|
1
|
-
import { Vector3 } from 'three';
|
|
1
|
+
import { Euler, Group, PerspectiveCamera, Plane, Quaternion, Vector2, Vector3 } from 'three';
|
|
2
2
|
import { resolveAbilities } from '../abilities/abilities.js';
|
|
3
3
|
import { parseAvatarDescriptor } from '../avatar/descriptor.js';
|
|
4
|
-
import { resolveWorldMeshAvatar, takeAvatarTicket } from '../avatar/handoff.js';
|
|
4
|
+
import { getAvatarTicket, resolveWorldMeshAvatar, setAvatarTicket, takeAvatarTicket } from '../avatar/handoff.js';
|
|
5
|
+
import { SharedAvatarLink, createShareVerifier } from '../avatar/share.js';
|
|
6
|
+
import { rememberView, takeViewHandoff } from '../camera/viewHandoff.js';
|
|
5
7
|
import { loadAvatarModel } from '../avatar/loader.js';
|
|
8
|
+
import { Footsteps } from '../audio/footsteps.js';
|
|
6
9
|
import { CameraRig } from '../camera/cameraRig.js';
|
|
7
10
|
import { Input } from '../controls/input.js';
|
|
8
11
|
import { CollisionWorld } from '../movement/collision.js';
|
|
9
12
|
import { MovementController } from '../movement/controller.js';
|
|
10
|
-
import { expressionForDigit, isAvatarExpression } from '../player/avatar.js';
|
|
13
|
+
import { expressionForDigit, isAvatarExpression, setAvatarColor } from '../player/avatar.js';
|
|
11
14
|
import { Player } from '../player/player.js';
|
|
12
15
|
import { PortalManager, buildTravelUrl } from '../portals/portals.js';
|
|
13
16
|
import { Overlay } from '../ui/overlay.js';
|
|
14
17
|
import { isTouchDevice } from '../controls/touch.js';
|
|
18
|
+
import { immersiveVrSupported, requestImmersiveVr } from '../xr/session.js';
|
|
15
19
|
import { Emitter } from './events.js';
|
|
16
20
|
import { DEFAULT_PRESENCE_SERVER, Presence } from '../net/presence.js';
|
|
21
|
+
import { roomFullLabel } from '../net/roomFull.js';
|
|
17
22
|
/** Never simulate more than this per substep, so a stall cannot tunnel the player. */
|
|
18
23
|
const MAX_STEP = 1 / 60;
|
|
19
24
|
/** Never simulate more than this per frame, so a backgrounded tab does not catch up violently. */
|
|
20
25
|
const MAX_FRAME = 0.1;
|
|
21
26
|
const DEFAULT_HUB = 'https://worldmesh.net/';
|
|
27
|
+
/** Seconds to ease the field of view back to the world's own after taking over the hub's camera. */
|
|
28
|
+
const FOV_RETURN = 1.5;
|
|
29
|
+
/**
|
|
30
|
+
* Set by a host that draws the world live in one of its doorways (Doorway,
|
|
31
|
+
* docs/specs/doorway.md §3.1). The world stays offline, shows no UI, takes no
|
|
32
|
+
* input, and draws only when the host sends a camera (`doorway:view`), from
|
|
33
|
+
* that camera carried through the doorway onto its spawn point. When the
|
|
34
|
+
* visitor walks through (`doorway:enter`), it takes over from their pose
|
|
35
|
+
* and goes online, without reloading (§4).
|
|
36
|
+
*/
|
|
37
|
+
export const DOORWAY_VIEW_PARAM = 'doorway-view';
|
|
38
|
+
/** Metres from the doorway back to the spawn point in a live view, and where a walk-in lands. */
|
|
39
|
+
const VIEW_SPAWN_BEHIND = 0.6;
|
|
40
|
+
/** Seconds a live view waits for the player to land at spawn before saying it is ready. */
|
|
41
|
+
const VIEW_SETTLE = 2;
|
|
42
|
+
/** The pre-Doorway names, still understood: `worldmesh:*` messages. */
|
|
43
|
+
const isDoorwayMessage = (data, name) => {
|
|
44
|
+
const type = data?.type;
|
|
45
|
+
return type === `doorway:${name}` || type === `worldmesh:${name}`;
|
|
46
|
+
};
|
|
22
47
|
/**
|
|
23
48
|
* Turn an ordinary Three.js scene into a WorldMesh world.
|
|
24
49
|
*
|
|
@@ -31,15 +56,26 @@ export function createWorldMesh(options) {
|
|
|
31
56
|
// Always take an avatar handoff ticket out of the address bar, even if this
|
|
32
57
|
// world shows no avatars: portals still carry it on to the next world.
|
|
33
58
|
const avatarTicket = takeAvatarTicket();
|
|
59
|
+
// A view the visitor picked here or in the world they came from wins over this world's default.
|
|
60
|
+
const rememberedView = takeViewHandoff();
|
|
34
61
|
const spawn = new Vector3(...(options.spawn ?? [0, 2, 0]));
|
|
35
62
|
const abilities = resolveAbilities(options.abilities);
|
|
63
|
+
// Before anything sizes the canvas: a live doorway view draws at 1×.
|
|
64
|
+
/** Drawn live in a host's doorway, not walked into yet. */
|
|
65
|
+
let viewing = isDoorwayViewRequest();
|
|
66
|
+
/** Inside a host's page (in its doorway, or walked into from it): other pages open through the host. */
|
|
67
|
+
const inHost = viewing;
|
|
36
68
|
const height = options.player?.height ?? 1.8;
|
|
37
69
|
const radius = options.player?.radius ?? 0.35;
|
|
38
70
|
const collision = new CollisionWorld(options.colliders ?? [], options.groundLevel ?? 0);
|
|
39
71
|
const events = new Emitter();
|
|
72
|
+
const vrEnabled = options.vr !== false;
|
|
40
73
|
const overlay = new Overlay({
|
|
41
74
|
...options.ui,
|
|
42
75
|
onEnter: (touch) => input.requestPointerLock(touch),
|
|
76
|
+
onEnterVr: () => {
|
|
77
|
+
void enterVR();
|
|
78
|
+
},
|
|
43
79
|
onInteract: () => {
|
|
44
80
|
if (activePortal) {
|
|
45
81
|
activatePortal(activePortal);
|
|
@@ -66,7 +102,7 @@ export function createWorldMesh(options) {
|
|
|
66
102
|
return false;
|
|
67
103
|
},
|
|
68
104
|
});
|
|
69
|
-
const cameraRig = new CameraRig({ ...options.view, camera, collision });
|
|
105
|
+
const cameraRig = new CameraRig({ ...options.view, mode: rememberedView ?? options.view?.mode, camera, collision });
|
|
70
106
|
const player = new Player({ ...options.player, height, radius });
|
|
71
107
|
if (player.root)
|
|
72
108
|
scene.add(player.root);
|
|
@@ -81,13 +117,30 @@ export function createWorldMesh(options) {
|
|
|
81
117
|
});
|
|
82
118
|
const portals = new PortalManager(scene, options.portals);
|
|
83
119
|
let running = false;
|
|
84
|
-
let frameHandle = 0;
|
|
85
120
|
let lastTime = 0;
|
|
86
121
|
let elapsed = 0;
|
|
87
122
|
let activePortal = null;
|
|
88
123
|
let disposed = false;
|
|
124
|
+
const footsteps = options.footsteps ? new Footsteps() : null;
|
|
89
125
|
let avatarDescriptor = null;
|
|
90
126
|
let avatarLoad = null;
|
|
127
|
+
/** The avatar other players see this visitor in; its link is renewed through the hub when it expires. */
|
|
128
|
+
const sharedAvatar = new SharedAvatarLink(options.avatar?.source === 'worldmesh'
|
|
129
|
+
? () => {
|
|
130
|
+
const ticket = getAvatarTicket();
|
|
131
|
+
const hubUrl = (options.avatar?.source === 'worldmesh' && options.avatar.hubUrl) || DEFAULT_HUB;
|
|
132
|
+
return ticket ? resolveWorldMeshAvatar(hubUrl, ticket) : Promise.resolve(null);
|
|
133
|
+
}
|
|
134
|
+
: null);
|
|
135
|
+
let xrSession = null;
|
|
136
|
+
const xrOrigin = new Group();
|
|
137
|
+
xrOrigin.name = 'worldmesh-xr-origin';
|
|
138
|
+
const xrHeading = new Euler(0, 0, 0, 'YXZ');
|
|
139
|
+
if (vrEnabled) {
|
|
140
|
+
renderer.xr.enabled = true;
|
|
141
|
+
renderer.xr.setReferenceSpaceType('local-floor');
|
|
142
|
+
scene.add(xrOrigin);
|
|
143
|
+
}
|
|
91
144
|
const handle = {
|
|
92
145
|
scene,
|
|
93
146
|
camera,
|
|
@@ -107,15 +160,25 @@ export function createWorldMesh(options) {
|
|
|
107
160
|
respawn: () => doRespawn('manual'),
|
|
108
161
|
setViewMode,
|
|
109
162
|
getViewMode: () => cameraRig.mode,
|
|
163
|
+
getCameraDistance: () => cameraRig.distance,
|
|
164
|
+
setCameraDistance: (distance) => {
|
|
165
|
+
if (Number.isFinite(distance))
|
|
166
|
+
cameraRig.distance = Math.max(0.4, distance);
|
|
167
|
+
},
|
|
110
168
|
addPortal: (portal) => {
|
|
111
169
|
portals.add(portal);
|
|
112
170
|
},
|
|
113
171
|
travelTo,
|
|
172
|
+
releasePointer: () => input.release(),
|
|
114
173
|
loadAvatar,
|
|
115
174
|
clearAvatar,
|
|
175
|
+
getSharedAvatar: () => sharedAvatar.get(),
|
|
116
176
|
on: (event, fn) => events.on(event, fn),
|
|
117
177
|
off: (event, fn) => events.off(event, fn),
|
|
118
178
|
refreshColliders: () => collision.refresh(),
|
|
179
|
+
enterVR,
|
|
180
|
+
exitVR,
|
|
181
|
+
isVR: () => renderer.xr.isPresenting,
|
|
119
182
|
};
|
|
120
183
|
applyViewVisibility();
|
|
121
184
|
if (options.autoResize !== false)
|
|
@@ -123,12 +186,277 @@ export function createWorldMesh(options) {
|
|
|
123
186
|
handleResize();
|
|
124
187
|
if (options.autoStart !== false)
|
|
125
188
|
start();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
189
|
+
let network;
|
|
190
|
+
/** Easing the field of view back to the world's own, after a walk-in took over the hub's. */
|
|
191
|
+
let fovReturn = null;
|
|
192
|
+
/** Live doorway view: said ready to the host, and drawn a view for it. */
|
|
193
|
+
let viewReady = false;
|
|
194
|
+
let viewFramed = false;
|
|
195
|
+
const viewStart = performance.now();
|
|
196
|
+
/** The host's latest view, not drawn yet. */
|
|
197
|
+
let pendingView = null;
|
|
198
|
+
if (viewing)
|
|
199
|
+
serveDoorwayView();
|
|
200
|
+
else
|
|
201
|
+
goOnline();
|
|
202
|
+
if (!viewing)
|
|
203
|
+
startAvatar();
|
|
204
|
+
if (vrEnabled && !viewing)
|
|
205
|
+
void offerVr();
|
|
131
206
|
return handle;
|
|
207
|
+
function goOnline() {
|
|
208
|
+
network = options.network ?? createMultiplayer();
|
|
209
|
+
if (network instanceof Presence) {
|
|
210
|
+
const presence = network;
|
|
211
|
+
presence.setOnFull((full) => {
|
|
212
|
+
overlay.setRoomFull(full ? roomFullLabel(presence.url) : null, {
|
|
213
|
+
onRetry: full ? () => presence.rejoin() : undefined,
|
|
214
|
+
});
|
|
215
|
+
if (full)
|
|
216
|
+
input.exitPointerLock();
|
|
217
|
+
events.emit('room:full', { full });
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
network?.attach(handle);
|
|
221
|
+
const bodies = network?.bodies?.bind(network);
|
|
222
|
+
if (bodies)
|
|
223
|
+
controller.bodies = () => bodies();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Drawn live in a host's doorway (DOORWAY_VIEW_PARAM): offline, no overlay,
|
|
227
|
+
* no input, no body of its own (the visitor's is in the host). The world
|
|
228
|
+
* keeps running (water, animations) and draws, once a frame, the latest
|
|
229
|
+
* view the host sent; none sent, nothing drawn. Ready once the player has
|
|
230
|
+
* landed at spawn, so the first view doesn't sink.
|
|
231
|
+
*/
|
|
232
|
+
function serveDoorwayView() {
|
|
233
|
+
// Kept, out of sight: walked into, the world shows them again.
|
|
234
|
+
injectDoorwayViewStyle();
|
|
235
|
+
document.documentElement.classList.add(DOORWAY_VIEW_CLASS);
|
|
236
|
+
applyViewVisibility();
|
|
237
|
+
const onMessage = (event) => {
|
|
238
|
+
if (event.source !== window.parent || disposed || !viewReady)
|
|
239
|
+
return;
|
|
240
|
+
const view = event.data;
|
|
241
|
+
// Only the latest counts: a world slower than the host never falls behind.
|
|
242
|
+
if (view?.type === 'doorway:view' && validDoorwayView(view))
|
|
243
|
+
pendingView = view;
|
|
244
|
+
else if (isDoorwayMessage(event.data, 'enter')) {
|
|
245
|
+
window.removeEventListener('message', onMessage);
|
|
246
|
+
enterFromView(event.data);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
window.addEventListener('message', onMessage);
|
|
250
|
+
awaitCharacter();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Walked into from the doorway: the pose comes in the door's frame, like
|
|
254
|
+
* the views, and lands the same way, on the spawn point beyond it. Then the
|
|
255
|
+
* world is itself again: drawing on its own, its UI and the visitor's body
|
|
256
|
+
* back, online. The host hears `doorway:entered` once a frame from there
|
|
257
|
+
* is drawn.
|
|
258
|
+
*/
|
|
259
|
+
function enterFromView(message) {
|
|
260
|
+
const url = typeof message.url === 'string' ? message.url : null;
|
|
261
|
+
try {
|
|
262
|
+
if (url && new URL(url).origin === window.location.origin)
|
|
263
|
+
window.history.replaceState(null, '', url);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Keep the view address.
|
|
267
|
+
}
|
|
268
|
+
if (disposed)
|
|
269
|
+
return;
|
|
270
|
+
takePose(poseFromDoor(message.pose));
|
|
271
|
+
viewing = false;
|
|
272
|
+
pendingView = null;
|
|
273
|
+
document.documentElement.classList.remove(DOORWAY_VIEW_CLASS);
|
|
274
|
+
handleResize();
|
|
275
|
+
applyViewVisibility();
|
|
276
|
+
input.requestPointerLock(isTouchDevice());
|
|
277
|
+
window.addEventListener('message', onCarry);
|
|
278
|
+
goOnline();
|
|
279
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.parent.postMessage({ type: 'doorway:entered' }, '*')));
|
|
280
|
+
}
|
|
281
|
+
/** A pose in the door's frame (doorway:enter from a live view), in this world's coordinates; null if malformed. */
|
|
282
|
+
function poseFromDoor(pose) {
|
|
283
|
+
const p = pose;
|
|
284
|
+
const vec = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n));
|
|
285
|
+
if (!p || !vec(p.position))
|
|
286
|
+
return null;
|
|
287
|
+
const spawnYaw = cameraRig.yaw;
|
|
288
|
+
viewTurn.setFromAxisAngle(UP, spawnYaw);
|
|
289
|
+
viewThreshold.set(0, 0, VIEW_SPAWN_BEHIND).applyQuaternion(viewTurn).add(controller.position);
|
|
290
|
+
const place = (v) => new Vector3(...v).applyQuaternion(viewTurn).add(viewThreshold).toArray();
|
|
291
|
+
const turn = (n) => (typeof n === 'number' && Number.isFinite(n) ? n + spawnYaw : n);
|
|
292
|
+
return {
|
|
293
|
+
...p,
|
|
294
|
+
position: place(p.position),
|
|
295
|
+
velocity: vec(p.velocity) ? new Vector3(...p.velocity).applyQuaternion(viewTurn).toArray() : p.velocity,
|
|
296
|
+
camera: vec(p.camera) ? place(p.camera) : p.camera,
|
|
297
|
+
yaw: turn(p.yaw),
|
|
298
|
+
facing: turn(p.facing),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** In the loop, instead of drawing: say ready once settled at spawn, then draw the latest view. */
|
|
302
|
+
function stepDoorwayView() {
|
|
303
|
+
if (!viewReady && (controller.onGround || performance.now() - viewStart > VIEW_SETTLE * 1000)) {
|
|
304
|
+
viewReady = true;
|
|
305
|
+
window.parent.postMessage({ type: 'doorway:view-ready' }, '*');
|
|
306
|
+
}
|
|
307
|
+
const view = pendingView;
|
|
308
|
+
if (!view)
|
|
309
|
+
return;
|
|
310
|
+
pendingView = null;
|
|
311
|
+
drawDoorwayView(view);
|
|
312
|
+
if (!viewFramed) {
|
|
313
|
+
viewFramed = true;
|
|
314
|
+
window.parent.postMessage({ type: 'doorway:view-frame' }, '*');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* One view: the host's camera, in the door's frame (threshold at the
|
|
319
|
+
* origin, +Y up, −Z through the doorway), carried onto the spawn point
|
|
320
|
+
* VIEW_SPAWN_BEHIND metres beyond it, facing the way the player does.
|
|
321
|
+
* Only the doorway's part of the screen, and nothing on the host's side.
|
|
322
|
+
*/
|
|
323
|
+
function drawDoorwayView(view) {
|
|
324
|
+
const { width, height: viewHeight } = view.viewport;
|
|
325
|
+
renderer.getSize(viewSize);
|
|
326
|
+
if (viewSize.x !== width || viewSize.y !== viewHeight)
|
|
327
|
+
renderer.setSize(width, viewHeight);
|
|
328
|
+
if (camera instanceof PerspectiveCamera) {
|
|
329
|
+
camera.fov = view.camera.fov;
|
|
330
|
+
camera.aspect = width / viewHeight;
|
|
331
|
+
camera.updateProjectionMatrix();
|
|
332
|
+
}
|
|
333
|
+
viewTurn.setFromAxisAngle(UP, cameraRig.yaw);
|
|
334
|
+
viewThreshold.set(0, 0, VIEW_SPAWN_BEHIND).applyQuaternion(viewTurn).add(controller.position);
|
|
335
|
+
camera.position.fromArray(view.camera.position).applyQuaternion(viewTurn).add(viewThreshold);
|
|
336
|
+
camera.quaternion.copy(viewTurn).multiply(viewQuaternion.fromArray(view.camera.quaternion));
|
|
337
|
+
camera.updateMatrixWorld();
|
|
338
|
+
viewClip.setFromNormalAndCoplanarPoint(viewForward.set(0, 0, -1).applyQuaternion(viewTurn), viewThreshold);
|
|
339
|
+
const worldPlanes = renderer.clippingPlanes;
|
|
340
|
+
renderer.clippingPlanes = [...worldPlanes, viewClip];
|
|
341
|
+
const [left, top, right, bottom] = view.rect;
|
|
342
|
+
renderer.setScissorTest(true);
|
|
343
|
+
renderer.setScissor(left, viewHeight - bottom, right - left, bottom - top);
|
|
344
|
+
renderer.render(scene, camera);
|
|
345
|
+
renderer.setScissorTest(false);
|
|
346
|
+
renderer.clippingPlanes = worldPlanes;
|
|
347
|
+
}
|
|
348
|
+
/** The hub's joystick, still held as the visitor walked in, keeps them walking until it is let go. */
|
|
349
|
+
function onCarry(event) {
|
|
350
|
+
if (event.source !== window.parent || !isDoorwayMessage(event.data, 'carry'))
|
|
351
|
+
return;
|
|
352
|
+
input.carry(Number(event.data.x) || 0, Number(event.data.z) || 0);
|
|
353
|
+
if (!event.data.x && !event.data.z)
|
|
354
|
+
window.removeEventListener('message', onCarry);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* In a doorway: the host hands over the visitor's character while the world
|
|
358
|
+
* still waits, so it is on screen from the first frame they walk in
|
|
359
|
+
* (Doorway §6): `{ type: 'doorway:character', format, name, data }`, the
|
|
360
|
+
* model file itself (nothing is fetched twice), or `data: null` for the
|
|
361
|
+
* default body. Taken from any host, only if the world shows visitors'
|
|
362
|
+
* characters at all. From this world's own hub (`avatar.hubUrl`), the
|
|
363
|
+
* WorldMesh extras under `worldmesh` are used too: the Avatar Wallet
|
|
364
|
+
* descriptor (for credits) and the handoff ticket for portals onwards. The
|
|
365
|
+
* pre-Doorway `worldmesh:avatar` from the hub is still understood. Answers
|
|
366
|
+
* `doorway:character-ready` (or `worldmesh:avatar-ready`).
|
|
367
|
+
*/
|
|
368
|
+
function awaitCharacter() {
|
|
369
|
+
const config = options.avatar;
|
|
370
|
+
if (config?.source !== 'worldmesh')
|
|
371
|
+
return;
|
|
372
|
+
let hubOrigin;
|
|
373
|
+
try {
|
|
374
|
+
hubOrigin = new URL(config.hubUrl ?? DEFAULT_HUB).origin;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
let latest = 0;
|
|
380
|
+
window.addEventListener('message', async (event) => {
|
|
381
|
+
if (event.source !== window.parent)
|
|
382
|
+
return;
|
|
383
|
+
const message = event.data;
|
|
384
|
+
const fromHub = event.origin === hubOrigin;
|
|
385
|
+
const neutral = message?.type === 'doorway:character';
|
|
386
|
+
if (!neutral && !(fromHub && message?.type === 'worldmesh:avatar'))
|
|
387
|
+
return;
|
|
388
|
+
const turn = ++latest;
|
|
389
|
+
const extras = (neutral ? message.worldmesh : message);
|
|
390
|
+
if (fromHub && typeof extras?.ticket === 'string')
|
|
391
|
+
setAvatarTicket(extras.ticket);
|
|
392
|
+
const data = message.data instanceof ArrayBuffer ? message.data : undefined;
|
|
393
|
+
let descriptor = fromHub ? parseAvatarDescriptor(extras?.descriptor) : null;
|
|
394
|
+
if (!descriptor && neutral && data && (message.format === 'vrm' || message.format === 'glb')) {
|
|
395
|
+
descriptor = {
|
|
396
|
+
provider: 'doorway',
|
|
397
|
+
avatarId: '',
|
|
398
|
+
name: typeof message.name === 'string' ? message.name.slice(0, 64) : null,
|
|
399
|
+
thumbnail: null,
|
|
400
|
+
format: message.format,
|
|
401
|
+
// Never fetched: the file came with the message.
|
|
402
|
+
modelUrl: '',
|
|
403
|
+
expiresAt: null,
|
|
404
|
+
sourceUrl: null,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
let loaded = false;
|
|
408
|
+
if (descriptor) {
|
|
409
|
+
loaded = await loadAvatar(descriptor, data);
|
|
410
|
+
}
|
|
411
|
+
else if (neutral ? message.data === null : extras?.descriptor === null) {
|
|
412
|
+
// Back to the default body (private mode, or no character any more).
|
|
413
|
+
clearAvatar();
|
|
414
|
+
}
|
|
415
|
+
if (turn !== latest || disposed)
|
|
416
|
+
return;
|
|
417
|
+
const answer = neutral ? 'doorway:character-ready' : 'worldmesh:avatar-ready';
|
|
418
|
+
window.parent.postMessage({ type: answer, loaded }, event.origin);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/** Where a hub had the visitor as they walked in, in this world's coordinates. Ignores anything malformed. */
|
|
422
|
+
function takePose(pose) {
|
|
423
|
+
const p = pose;
|
|
424
|
+
const vec = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => Number.isFinite(n));
|
|
425
|
+
const num = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
426
|
+
if (!p || !vec(p.position))
|
|
427
|
+
return;
|
|
428
|
+
controller.reset(new Vector3(...p.position));
|
|
429
|
+
if (vec(p.velocity))
|
|
430
|
+
controller.velocity.set(...p.velocity);
|
|
431
|
+
if (num(p.yaw))
|
|
432
|
+
cameraRig.yaw = p.yaw;
|
|
433
|
+
if (num(p.pitch))
|
|
434
|
+
cameraRig.pitch = p.pitch;
|
|
435
|
+
if (num(p.facing))
|
|
436
|
+
player.facing = p.facing;
|
|
437
|
+
if (p.mode === 'first' || p.mode === 'third')
|
|
438
|
+
setViewMode(p.mode);
|
|
439
|
+
if (cameraRig.mode === 'third' && vec(p.camera)) {
|
|
440
|
+
// Same boom as the hub's camera had, measured from this world's eyes; it eases back out from there.
|
|
441
|
+
const eyes = new Vector3(...p.position).add(new Vector3(0, player.eyeHeight * (controller.height / height), 0));
|
|
442
|
+
cameraRig.snapBoom(eyes.distanceTo(new Vector3(...p.camera)));
|
|
443
|
+
}
|
|
444
|
+
// The figure keeps the colour it had in the hub (the default body only).
|
|
445
|
+
if (typeof p.color === 'string' && /^#[0-9a-f]{6}$/i.test(p.color) && player.root && !avatarDescriptor)
|
|
446
|
+
setAvatarColor(player.root, p.color);
|
|
447
|
+
if (num(p.fov) && camera instanceof PerspectiveCamera && Math.abs(camera.fov - p.fov) > 0.01) {
|
|
448
|
+
fovReturn = { from: p.fov, to: camera.fov, t: 0 };
|
|
449
|
+
camera.fov = p.fov;
|
|
450
|
+
camera.updateProjectionMatrix();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/** Leave for another page; a world inside a host's page asks the host to go there instead. */
|
|
454
|
+
function leaveFor(url) {
|
|
455
|
+
if (inHost)
|
|
456
|
+
window.parent.postMessage({ type: 'doorway:navigate', url }, '*');
|
|
457
|
+
else
|
|
458
|
+
window.location.href = url;
|
|
459
|
+
}
|
|
132
460
|
/** Brings in the visitor's own avatar, if the world asked for one. The default body stays until it is ready. */
|
|
133
461
|
function startAvatar() {
|
|
134
462
|
const config = options.avatar;
|
|
@@ -149,7 +477,7 @@ export function createWorldMesh(options) {
|
|
|
149
477
|
void loadAvatar(descriptor);
|
|
150
478
|
}, (error) => events.emit('avatar:error', { descriptor: null, error }));
|
|
151
479
|
}
|
|
152
|
-
async function loadAvatar(descriptor) {
|
|
480
|
+
async function loadAvatar(descriptor, data) {
|
|
153
481
|
if (disposed || !player.root)
|
|
154
482
|
return false;
|
|
155
483
|
avatarLoad?.abort();
|
|
@@ -159,6 +487,7 @@ export function createWorldMesh(options) {
|
|
|
159
487
|
height,
|
|
160
488
|
maxBytes: options.avatar?.maxBytes,
|
|
161
489
|
signal: load.signal,
|
|
490
|
+
data,
|
|
162
491
|
});
|
|
163
492
|
if (disposed || load.signal.aborted) {
|
|
164
493
|
loaded.dispose();
|
|
@@ -167,6 +496,7 @@ export function createWorldMesh(options) {
|
|
|
167
496
|
if (!player.setExternalBody(loaded))
|
|
168
497
|
return false;
|
|
169
498
|
avatarDescriptor = descriptor;
|
|
499
|
+
sharedAvatar.set(descriptor);
|
|
170
500
|
applyViewVisibility();
|
|
171
501
|
events.emit('avatar:load', { descriptor });
|
|
172
502
|
return true;
|
|
@@ -187,38 +517,111 @@ export function createWorldMesh(options) {
|
|
|
187
517
|
avatarLoad?.abort();
|
|
188
518
|
player.clearExternalBody();
|
|
189
519
|
avatarDescriptor = null;
|
|
520
|
+
sharedAvatar.set(null);
|
|
190
521
|
}
|
|
191
522
|
function start() {
|
|
192
523
|
if (running || disposed)
|
|
193
524
|
return;
|
|
194
525
|
running = true;
|
|
195
526
|
lastTime = performance.now();
|
|
196
|
-
|
|
527
|
+
// setAnimationLoop is the WebXR frame pump; it also drives the flat canvas.
|
|
528
|
+
renderer.setAnimationLoop(frame);
|
|
197
529
|
}
|
|
198
530
|
function stop() {
|
|
199
531
|
running = false;
|
|
200
|
-
|
|
201
|
-
cancelAnimationFrame(frameHandle);
|
|
202
|
-
frameHandle = 0;
|
|
532
|
+
renderer.setAnimationLoop(null);
|
|
203
533
|
}
|
|
204
534
|
function frame(now) {
|
|
205
535
|
if (!running)
|
|
206
536
|
return;
|
|
207
|
-
frameHandle = requestAnimationFrame(frame);
|
|
208
537
|
const dt = Math.min((now - lastTime) / 1000, MAX_FRAME);
|
|
209
538
|
lastTime = now;
|
|
210
539
|
update(dt);
|
|
211
540
|
options.onUpdate?.(dt, handle);
|
|
212
|
-
|
|
541
|
+
if (fovReturn && camera instanceof PerspectiveCamera) {
|
|
542
|
+
fovReturn.t = Math.min(1, fovReturn.t + dt / FOV_RETURN);
|
|
543
|
+
const k = fovReturn.t * fovReturn.t * (3 - 2 * fovReturn.t);
|
|
544
|
+
camera.fov = fovReturn.from + (fovReturn.to - fovReturn.from) * k;
|
|
545
|
+
camera.updateProjectionMatrix();
|
|
546
|
+
if (fovReturn.t >= 1)
|
|
547
|
+
fovReturn = null;
|
|
548
|
+
}
|
|
549
|
+
// A live doorway view draws the host's latest camera instead (stepDoorwayView).
|
|
550
|
+
// Drawing is what places things otherwise, and colliders need them placed.
|
|
551
|
+
if (viewing) {
|
|
552
|
+
scene.updateMatrixWorld();
|
|
553
|
+
stepDoorwayView();
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
renderer.render(scene, camera);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function offerVr() {
|
|
560
|
+
if (disposed || !(await immersiveVrSupported()))
|
|
561
|
+
return;
|
|
562
|
+
overlay.setVrAvailable(true);
|
|
563
|
+
}
|
|
564
|
+
async function enterVR() {
|
|
565
|
+
if (!vrEnabled || disposed)
|
|
566
|
+
return false;
|
|
567
|
+
if (renderer.xr.isPresenting)
|
|
568
|
+
return true;
|
|
569
|
+
try {
|
|
570
|
+
input.exitPointerLock();
|
|
571
|
+
const session = await requestImmersiveVr(renderer);
|
|
572
|
+
bindXrSession(session);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
console.warn('[worldmesh] Could not start VR.', error);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function exitVR() {
|
|
581
|
+
if (!xrSession)
|
|
582
|
+
return;
|
|
583
|
+
try {
|
|
584
|
+
await xrSession.end();
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
unbindXrSession();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function bindXrSession(session) {
|
|
591
|
+
xrSession = session;
|
|
592
|
+
input.setXrSession(session);
|
|
593
|
+
if (camera.parent !== xrOrigin)
|
|
594
|
+
xrOrigin.add(camera);
|
|
595
|
+
overlay.setVrPresenting(true);
|
|
596
|
+
applyViewVisibility();
|
|
597
|
+
session.addEventListener('end', unbindXrSession);
|
|
598
|
+
events.emit('vr:enter', {});
|
|
599
|
+
}
|
|
600
|
+
function unbindXrSession() {
|
|
601
|
+
if (!xrSession)
|
|
602
|
+
return;
|
|
603
|
+
xrSession.removeEventListener('end', unbindXrSession);
|
|
604
|
+
xrSession = null;
|
|
605
|
+
input.setXrSession(null);
|
|
606
|
+
if (camera.parent === xrOrigin)
|
|
607
|
+
xrOrigin.remove(camera);
|
|
608
|
+
xrOrigin.position.set(0, 0, 0);
|
|
609
|
+
xrOrigin.rotation.set(0, 0, 0);
|
|
610
|
+
overlay.setVrPresenting(false);
|
|
611
|
+
applyViewVisibility();
|
|
612
|
+
events.emit('vr:exit', {});
|
|
213
613
|
}
|
|
214
614
|
/** Advance the simulation. Split from rendering so a fixed tick can drive it. */
|
|
215
615
|
function update(dt) {
|
|
216
616
|
if (dt <= 0)
|
|
217
617
|
return;
|
|
218
618
|
elapsed += dt;
|
|
619
|
+
const presenting = renderer.xr.isPresenting;
|
|
620
|
+
if (presenting)
|
|
621
|
+
input.pollXr(dt);
|
|
219
622
|
const look = input.readLook();
|
|
220
623
|
const playing = isPlaying();
|
|
221
|
-
if (playing) {
|
|
624
|
+
if (playing && !presenting) {
|
|
222
625
|
if (input.locked || isTouchDevice() || options.ui?.moveBeforeLock === true) {
|
|
223
626
|
cameraRig.look(look.dx, look.dy);
|
|
224
627
|
if (input.locked)
|
|
@@ -231,6 +634,11 @@ export function createWorldMesh(options) {
|
|
|
231
634
|
if (expression)
|
|
232
635
|
player.setExpression(expression);
|
|
233
636
|
}
|
|
637
|
+
if (presenting) {
|
|
638
|
+
xrOrigin.rotation.y += input.readXrTurn();
|
|
639
|
+
xrHeading.setFromQuaternion(renderer.xr.getCamera().quaternion, 'YXZ');
|
|
640
|
+
cameraRig.yaw = xrHeading.y;
|
|
641
|
+
}
|
|
234
642
|
// Fixed substeps keep movement stable regardless of frame rate.
|
|
235
643
|
let remaining = dt;
|
|
236
644
|
while (remaining > 0) {
|
|
@@ -240,13 +648,19 @@ export function createWorldMesh(options) {
|
|
|
240
648
|
}
|
|
241
649
|
if (controller.position.y < controller.tuning.fallLimit)
|
|
242
650
|
doRespawn('fell');
|
|
243
|
-
|
|
244
|
-
|
|
651
|
+
if (presenting) {
|
|
652
|
+
xrOrigin.position.copy(controller.position);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
cameraRig.update(dt, controller.position, player.eyeHeight * (controller.height / height));
|
|
656
|
+
}
|
|
657
|
+
const footfall = player.sync(controller.position, cameraRig.yaw, controller.height, {
|
|
245
658
|
dt,
|
|
246
659
|
speed: Math.hypot(controller.velocity.x, controller.velocity.z),
|
|
247
660
|
grounded: controller.onGround || controller.flying,
|
|
248
661
|
heading: Math.atan2(-controller.velocity.x, -controller.velocity.z),
|
|
249
662
|
}, cameraRig.mode === 'third');
|
|
663
|
+
footsteps?.update(dt, playing ? Math.hypot(controller.velocity.x, controller.velocity.z) : 0, controller.onGround, footfall, controller.velocity.y, controller.flying);
|
|
250
664
|
portals.animate(elapsed);
|
|
251
665
|
updatePortals();
|
|
252
666
|
input.endFrame();
|
|
@@ -254,6 +668,8 @@ export function createWorldMesh(options) {
|
|
|
254
668
|
}
|
|
255
669
|
/** Walking, until Esc opens the pause screen. */
|
|
256
670
|
function isPlaying() {
|
|
671
|
+
if (renderer.xr.isPresenting)
|
|
672
|
+
return true;
|
|
257
673
|
return !overlay.isPaused() && (input.locked || options.ui?.moveBeforeLock === true);
|
|
258
674
|
}
|
|
259
675
|
function updatePortals() {
|
|
@@ -294,19 +710,21 @@ export function createWorldMesh(options) {
|
|
|
294
710
|
});
|
|
295
711
|
if (cancelled)
|
|
296
712
|
return;
|
|
297
|
-
input.
|
|
298
|
-
|
|
713
|
+
input.release();
|
|
714
|
+
leaveFor(url);
|
|
299
715
|
}
|
|
300
716
|
function setViewMode(mode) {
|
|
301
717
|
if (cameraRig.mode === mode)
|
|
302
718
|
return;
|
|
303
719
|
cameraRig.setMode(mode);
|
|
720
|
+
rememberView(mode);
|
|
304
721
|
applyViewVisibility();
|
|
305
722
|
events.emit('view:change', { mode });
|
|
306
723
|
}
|
|
307
724
|
/** In first person the avatar would fill the screen. */
|
|
308
725
|
function applyViewVisibility() {
|
|
309
|
-
|
|
726
|
+
// In a doorway the visitor's own body is the host's: none here.
|
|
727
|
+
player.setVisible(!viewing && (cameraRig.mode === 'third' || renderer.xr.isPresenting));
|
|
310
728
|
}
|
|
311
729
|
function getState() {
|
|
312
730
|
return {
|
|
@@ -349,15 +767,25 @@ export function createWorldMesh(options) {
|
|
|
349
767
|
events.emit('respawn', { reason });
|
|
350
768
|
}
|
|
351
769
|
function travelTo(url) {
|
|
352
|
-
input.
|
|
353
|
-
|
|
770
|
+
input.release();
|
|
771
|
+
leaveFor(buildTravelUrl(url));
|
|
772
|
+
}
|
|
773
|
+
function isDoorwayViewRequest() {
|
|
774
|
+
try {
|
|
775
|
+
return window.parent !== window && new URLSearchParams(window.location.search).has(DOORWAY_VIEW_PARAM);
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
354
780
|
}
|
|
355
781
|
function handleResize() {
|
|
782
|
+
if (renderer.xr.isPresenting)
|
|
783
|
+
return;
|
|
356
784
|
const width = window.innerWidth;
|
|
357
785
|
const viewHeight = window.innerHeight;
|
|
358
786
|
// Pixel ratio first: setSize derives the drawing buffer from it, and it
|
|
359
787
|
// must also update the canvas CSS size or the canvas overflows the window.
|
|
360
|
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
788
|
+
renderer.setPixelRatio(viewing ? 1 : Math.min(window.devicePixelRatio, 2));
|
|
361
789
|
renderer.setSize(width, viewHeight);
|
|
362
790
|
cameraRig.resize(width, viewHeight);
|
|
363
791
|
}
|
|
@@ -369,6 +797,10 @@ export function createWorldMesh(options) {
|
|
|
369
797
|
url: `${server.replace(/\/$/, '')}/world`,
|
|
370
798
|
scene,
|
|
371
799
|
onCount: (count) => events.emit('players', { count }),
|
|
800
|
+
getAvatar: () => sharedAvatar.get(),
|
|
801
|
+
// Other players' avatars are shown only from links this world's hub signed.
|
|
802
|
+
verifyAvatar: createShareVerifier((options.avatar?.source === 'worldmesh' && options.avatar.hubUrl) || DEFAULT_HUB),
|
|
803
|
+
maxAvatarBytes: options.avatar?.maxBytes,
|
|
372
804
|
});
|
|
373
805
|
}
|
|
374
806
|
function dispose() {
|
|
@@ -376,14 +808,54 @@ export function createWorldMesh(options) {
|
|
|
376
808
|
return;
|
|
377
809
|
disposed = true;
|
|
378
810
|
stop();
|
|
811
|
+
const session = xrSession;
|
|
812
|
+
if (session) {
|
|
813
|
+
session.removeEventListener('end', unbindXrSession);
|
|
814
|
+
unbindXrSession();
|
|
815
|
+
void session.end().catch(() => undefined);
|
|
816
|
+
}
|
|
379
817
|
avatarLoad?.abort();
|
|
818
|
+
sharedAvatar.dispose();
|
|
380
819
|
window.removeEventListener('resize', handleResize);
|
|
381
820
|
network?.detach?.();
|
|
382
821
|
input.dispose();
|
|
383
822
|
overlay.dispose();
|
|
384
823
|
portals.dispose();
|
|
385
824
|
player.dispose();
|
|
825
|
+
footsteps?.dispose();
|
|
826
|
+
if (vrEnabled)
|
|
827
|
+
scene.remove(xrOrigin);
|
|
386
828
|
events.clear();
|
|
387
829
|
}
|
|
388
830
|
}
|
|
831
|
+
function validDoorwayView(view) {
|
|
832
|
+
const numbers = (value, length) => Array.isArray(value) && value.length === length && value.every((n) => typeof n === 'number' && Number.isFinite(n));
|
|
833
|
+
const { camera, viewport } = view;
|
|
834
|
+
return (!!camera &&
|
|
835
|
+
numbers(camera.position, 3) &&
|
|
836
|
+
numbers(camera.quaternion, 4) &&
|
|
837
|
+
typeof camera.fov === 'number' &&
|
|
838
|
+
camera.fov > 0 &&
|
|
839
|
+
!!viewport &&
|
|
840
|
+
viewport.width > 0 &&
|
|
841
|
+
viewport.height > 0 &&
|
|
842
|
+
numbers(view.rect, 4));
|
|
843
|
+
}
|
|
844
|
+
const UP = new Vector3(0, 1, 0);
|
|
845
|
+
const viewSize = new Vector2();
|
|
846
|
+
const viewTurn = new Quaternion();
|
|
847
|
+
const viewQuaternion = new Quaternion();
|
|
848
|
+
const viewThreshold = new Vector3();
|
|
849
|
+
const viewForward = new Vector3();
|
|
850
|
+
const viewClip = new Plane();
|
|
851
|
+
/** On the page while it is drawn in a host's doorway: its overlay and touch controls are kept, unseen. */
|
|
852
|
+
const DOORWAY_VIEW_CLASS = 'wm-doorway-view';
|
|
853
|
+
function injectDoorwayViewStyle() {
|
|
854
|
+
if (document.getElementById(DOORWAY_VIEW_CLASS))
|
|
855
|
+
return;
|
|
856
|
+
const style = document.createElement('style');
|
|
857
|
+
style.id = DOORWAY_VIEW_CLASS;
|
|
858
|
+
style.textContent = `html.${DOORWAY_VIEW_CLASS} .wm-overlay, html.${DOORWAY_VIEW_CLASS} .wm-touch-root { display: none !important; }`;
|
|
859
|
+
document.head.appendChild(style);
|
|
860
|
+
}
|
|
389
861
|
//# sourceMappingURL=worldmesh.js.map
|