@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.
Files changed (100) hide show
  1. package/README.md +33 -3
  2. package/dist/audio/footsteps.d.ts +37 -0
  3. package/dist/audio/footsteps.d.ts.map +1 -0
  4. package/dist/audio/footsteps.js +178 -0
  5. package/dist/audio/footsteps.js.map +1 -0
  6. package/dist/avatar/descriptor.d.ts +15 -0
  7. package/dist/avatar/descriptor.d.ts.map +1 -1
  8. package/dist/avatar/descriptor.js +19 -0
  9. package/dist/avatar/descriptor.js.map +1 -1
  10. package/dist/avatar/loader.d.ts +12 -1
  11. package/dist/avatar/loader.d.ts.map +1 -1
  12. package/dist/avatar/loader.js +16 -1
  13. package/dist/avatar/loader.js.map +1 -1
  14. package/dist/avatar/share.d.ts +30 -0
  15. package/dist/avatar/share.d.ts.map +1 -0
  16. package/dist/avatar/share.js +203 -0
  17. package/dist/avatar/share.js.map +1 -0
  18. package/dist/camera/cameraRig.d.ts +4 -0
  19. package/dist/camera/cameraRig.d.ts.map +1 -1
  20. package/dist/camera/cameraRig.js +10 -1
  21. package/dist/camera/cameraRig.js.map +1 -1
  22. package/dist/camera/viewHandoff.d.ts +20 -0
  23. package/dist/camera/viewHandoff.d.ts.map +1 -0
  24. package/dist/camera/viewHandoff.js +64 -0
  25. package/dist/camera/viewHandoff.js.map +1 -0
  26. package/dist/controls/input.d.ts +29 -0
  27. package/dist/controls/input.d.ts.map +1 -1
  28. package/dist/controls/input.js +124 -1
  29. package/dist/controls/input.js.map +1 -1
  30. package/dist/controls/touch.d.ts +10 -0
  31. package/dist/controls/touch.d.ts.map +1 -1
  32. package/dist/controls/touch.js +57 -1
  33. package/dist/controls/touch.js.map +1 -1
  34. package/dist/core/worldmesh.d.ts +9 -0
  35. package/dist/core/worldmesh.d.ts.map +1 -1
  36. package/dist/core/worldmesh.js +498 -26
  37. package/dist/core/worldmesh.js.map +1 -1
  38. package/dist/index.d.ts +14 -7
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/net/follow.d.ts +7 -0
  43. package/dist/net/follow.d.ts.map +1 -0
  44. package/dist/net/follow.js +10 -0
  45. package/dist/net/follow.js.map +1 -0
  46. package/dist/net/presence.d.ts +126 -4
  47. package/dist/net/presence.d.ts.map +1 -1
  48. package/dist/net/presence.js +472 -25
  49. package/dist/net/presence.js.map +1 -1
  50. package/dist/net/roomFull.d.ts +44 -0
  51. package/dist/net/roomFull.d.ts.map +1 -0
  52. package/dist/net/roomFull.js +147 -0
  53. package/dist/net/roomFull.js.map +1 -0
  54. package/dist/player/avatar.d.ts +17 -4
  55. package/dist/player/avatar.d.ts.map +1 -1
  56. package/dist/player/avatar.js +85 -18
  57. package/dist/player/avatar.js.map +1 -1
  58. package/dist/player/player.d.ts +6 -2
  59. package/dist/player/player.d.ts.map +1 -1
  60. package/dist/player/player.js +13 -3
  61. package/dist/player/player.js.map +1 -1
  62. package/dist/player/stroke.d.ts +11 -0
  63. package/dist/player/stroke.d.ts.map +1 -0
  64. package/dist/player/stroke.js +96 -0
  65. package/dist/player/stroke.js.map +1 -0
  66. package/dist/portals/portals.d.ts +1 -1
  67. package/dist/portals/portals.d.ts.map +1 -1
  68. package/dist/portals/portals.js +6 -2
  69. package/dist/portals/portals.js.map +1 -1
  70. package/dist/types.d.ts +53 -9
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/ui/overlay.d.ts +18 -0
  73. package/dist/ui/overlay.d.ts.map +1 -1
  74. package/dist/ui/overlay.js +166 -1
  75. package/dist/ui/overlay.js.map +1 -1
  76. package/dist/xr/session.d.ts +13 -0
  77. package/dist/xr/session.d.ts.map +1 -0
  78. package/dist/xr/session.js +33 -0
  79. package/dist/xr/session.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/audio/footsteps.ts +185 -0
  82. package/src/avatar/descriptor.ts +28 -0
  83. package/src/avatar/loader.ts +19 -2
  84. package/src/avatar/share.ts +196 -0
  85. package/src/camera/cameraRig.ts +10 -1
  86. package/src/camera/viewHandoff.ts +67 -0
  87. package/src/controls/input.ts +119 -1
  88. package/src/controls/touch.ts +59 -1
  89. package/src/core/worldmesh.ts +495 -25
  90. package/src/index.ts +14 -7
  91. package/src/net/follow.ts +9 -0
  92. package/src/net/presence.ts +548 -28
  93. package/src/net/roomFull.ts +160 -0
  94. package/src/player/avatar.ts +87 -18
  95. package/src/player/player.ts +13 -4
  96. package/src/player/stroke.ts +93 -0
  97. package/src/portals/portals.ts +5 -2
  98. package/src/types.ts +52 -9
  99. package/src/ui/overlay.ts +171 -1
  100. package/src/xr/session.ts +33 -0
package/src/ui/overlay.ts CHANGED
@@ -101,6 +101,49 @@ const CSS = `
101
101
  border-color: rgba(255,255,255,0.6);
102
102
  transform: scale(1.02);
103
103
  }
104
+ .wm-vr-launch {
105
+ position: absolute;
106
+ top: max(16px, env(safe-area-inset-top));
107
+ left: 50%;
108
+ transform: translateX(-50%);
109
+ pointer-events: auto;
110
+ font-size: 13px;
111
+ letter-spacing: .06em;
112
+ text-transform: uppercase;
113
+ padding: 10px 22px;
114
+ border: 1px solid rgba(255,255,255,0.4);
115
+ border-radius: 999px;
116
+ background: rgba(8,11,16,0.72);
117
+ color: #fff;
118
+ cursor: pointer;
119
+ font-family: inherit;
120
+ font-weight: 600;
121
+ touch-action: manipulation;
122
+ -webkit-tap-highlight-color: transparent;
123
+ }
124
+ .wm-vr-launch:hover, .wm-vr-launch:active {
125
+ background: rgba(8,11,16,0.9);
126
+ border-color: rgba(255,255,255,0.7);
127
+ }
128
+ .wm-overlay:not([data-locked="true"]) .wm-vr-launch,
129
+ .wm-overlay[data-xr="true"] .wm-vr-launch,
130
+ .wm-overlay[data-xr="true"] .wm-vr-menu {
131
+ display: none;
132
+ }
133
+ .wm-vr-menu {
134
+ font-size: 13px;
135
+ letter-spacing: .06em;
136
+ text-transform: uppercase;
137
+ padding: 10px 22px;
138
+ border: 1px solid rgba(255,255,255,0.35);
139
+ border-radius: 999px;
140
+ background: rgba(255,255,255,0.08);
141
+ color: #fff;
142
+ cursor: pointer;
143
+ font-family: inherit;
144
+ font-weight: 500;
145
+ touch-action: manipulation;
146
+ }
104
147
  .wm-keys {
105
148
  display: grid;
106
149
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -201,6 +244,55 @@ const CSS = `
201
244
  transition: opacity .2s ease;
202
245
  }
203
246
  .wm-overlay[data-locked="true"] .wm-hint { opacity: 1; }
247
+ .wm-room-full {
248
+ position: absolute;
249
+ inset: 0;
250
+ display: none;
251
+ flex-direction: column;
252
+ align-items: center;
253
+ justify-content: center;
254
+ gap: 18px;
255
+ background: rgba(8, 11, 16, 0.22);
256
+ backdrop-filter: blur(8px);
257
+ -webkit-backdrop-filter: blur(8px);
258
+ pointer-events: auto;
259
+ z-index: 6;
260
+ padding: max(20px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
261
+ }
262
+ .wm-overlay[data-room-full="true"] .wm-room-full { display: flex; }
263
+ .wm-overlay[data-room-full="true"] .wm-lock { display: none; }
264
+ .wm-room-full-label {
265
+ margin: 0;
266
+ padding: 14px 36px;
267
+ border-radius: 4px;
268
+ background: rgba(160, 12, 18, 0.92);
269
+ color: #fff;
270
+ font-family: Urbanist, ui-sans-serif, system-ui, sans-serif;
271
+ font-size: clamp(22px, 4.4vw, 36px);
272
+ font-weight: 700;
273
+ letter-spacing: .02em;
274
+ text-align: center;
275
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
276
+ }
277
+ .wm-room-full-retry {
278
+ font-size: 13.5px;
279
+ letter-spacing: .14em;
280
+ text-transform: uppercase;
281
+ padding: 12px 28px;
282
+ border: 1px solid rgba(255,255,255,0.45);
283
+ border-radius: 999px;
284
+ background: rgba(255,255,255,0.12);
285
+ color: #fff;
286
+ cursor: pointer;
287
+ font-family: inherit;
288
+ font-weight: 500;
289
+ touch-action: manipulation;
290
+ -webkit-tap-highlight-color: transparent;
291
+ }
292
+ .wm-room-full-retry:hover, .wm-room-full-retry:active {
293
+ background: rgba(255,255,255,0.22);
294
+ border-color: rgba(255,255,255,0.7);
295
+ }
204
296
 
205
297
  @media (max-width: 400px) {
206
298
  .wm-lock {
@@ -278,13 +370,20 @@ export class Overlay {
278
370
  readonly root: HTMLDivElement;
279
371
  private prompt: HTMLDivElement;
280
372
  private lock: HTMLDivElement;
373
+ private vrLaunch: HTMLButtonElement;
374
+ private vrMenu: HTMLButtonElement;
375
+ private roomFull: HTMLDivElement;
376
+ private roomFullLabel: HTMLParagraphElement;
377
+ private roomFullRetry: HTMLButtonElement;
281
378
  private onEnter: (touch?: boolean) => void;
379
+ private onRoomFullRetry: (() => void) | null = null;
282
380
  private deferLockPanel: boolean;
283
381
 
284
382
  constructor(
285
383
  options: UiOptions & {
286
384
  onEnter: (touch?: boolean) => void;
287
385
  onInteract?: () => void;
386
+ onEnterVr?: () => void;
288
387
  },
289
388
  ) {
290
389
  injectStyles();
@@ -299,7 +398,7 @@ export class Overlay {
299
398
  this.root.dataset.locked = options.moveBeforeLock ? 'true' : 'false';
300
399
  if (this.deferLockPanel) this.root.dataset.deferLock = 'true';
301
400
 
302
- if (options.crosshair !== false) {
401
+ if (options.crosshair === true) {
303
402
  const crosshair = document.createElement('div');
304
403
  crosshair.className = 'wm-crosshair';
305
404
  this.root.appendChild(crosshair);
@@ -345,6 +444,18 @@ export class Overlay {
345
444
  handleEnter(event);
346
445
  });
347
446
 
447
+ const vrMenu = document.createElement('button');
448
+ vrMenu.type = 'button';
449
+ vrMenu.className = 'wm-vr-menu';
450
+ vrMenu.textContent = 'Enter VR';
451
+ vrMenu.hidden = true;
452
+ vrMenu.addEventListener('click', (event) => {
453
+ event.stopPropagation();
454
+ options.onEnterVr?.();
455
+ });
456
+ this.vrMenu = vrMenu;
457
+ content.appendChild(vrMenu);
458
+
348
459
  if (options.controlsHint !== false) {
349
460
  const keys = document.createElement('div');
350
461
  keys.className = 'wm-keys';
@@ -382,6 +493,8 @@ export class Overlay {
382
493
  const badge = document.createElement('a');
383
494
  badge.className = 'wm-badge';
384
495
  badge.href = options.hubUrl ?? 'https://worldmesh.net';
496
+ // Shown inside a hub's door: back to the hub replaces the whole page, not the frame.
497
+ if (window.parent !== window) badge.target = '_top';
385
498
  const dot = document.createElement('span');
386
499
  dot.className = 'wm-badge-dot';
387
500
  const label = document.createElement('span');
@@ -395,9 +508,53 @@ export class Overlay {
395
508
  hint.textContent = isTouch ? 'Tap ⏸ to open the menu' : 'Esc to open the menu';
396
509
  this.root.appendChild(hint);
397
510
 
511
+ const vrLaunch = document.createElement('button');
512
+ vrLaunch.type = 'button';
513
+ vrLaunch.className = 'wm-vr-launch';
514
+ vrLaunch.textContent = 'Enter VR';
515
+ vrLaunch.hidden = true;
516
+ vrLaunch.addEventListener('click', (event) => {
517
+ event.stopPropagation();
518
+ options.onEnterVr?.();
519
+ });
520
+ this.vrLaunch = vrLaunch;
521
+ this.root.appendChild(vrLaunch);
522
+
523
+ const roomFull = document.createElement('div');
524
+ roomFull.className = 'wm-room-full';
525
+ roomFull.hidden = true;
526
+ const roomFullLabel = document.createElement('p');
527
+ roomFullLabel.className = 'wm-room-full-label';
528
+ const roomFullRetry = document.createElement('button');
529
+ roomFullRetry.type = 'button';
530
+ roomFullRetry.className = 'wm-room-full-retry';
531
+ roomFullRetry.textContent = 'Try again';
532
+ roomFullRetry.hidden = true;
533
+ roomFullRetry.addEventListener('click', (event) => {
534
+ event.stopPropagation();
535
+ this.onRoomFullRetry?.();
536
+ });
537
+ roomFull.append(roomFullLabel, roomFullRetry);
538
+ this.roomFull = roomFull;
539
+ this.roomFullLabel = roomFullLabel;
540
+ this.roomFullRetry = roomFullRetry;
541
+ this.root.appendChild(roomFull);
542
+
398
543
  document.body.appendChild(this.root);
399
544
  }
400
545
 
546
+ /** Show Enter VR once the browser reports immersive-vr support. */
547
+ setVrAvailable(available: boolean): void {
548
+ this.vrLaunch.hidden = !available;
549
+ this.vrMenu.hidden = !available;
550
+ }
551
+
552
+ /** Hide the 2D Enter VR controls while the headset is presenting. */
553
+ setVrPresenting(presenting: boolean): void {
554
+ this.root.dataset.xr = String(presenting);
555
+ if (presenting) this.setLocked(true);
556
+ }
557
+
401
558
  /** The pause screen, with the control list, is on screen. */
402
559
  isPaused(): boolean {
403
560
  return this.root.dataset.lockReady === 'true' && this.root.dataset.locked !== 'true';
@@ -428,6 +585,19 @@ export class Overlay {
428
585
  if (content instanceof HTMLElement) content.style.visibility = '';
429
586
  }
430
587
 
588
+ /**
589
+ * Presence refused the join because the room is full. Soft-blur the scene
590
+ * and show the same red banner language as a full world door.
591
+ */
592
+ setRoomFull(label: string | null, options?: { onRetry?: () => void }): void {
593
+ const full = Boolean(label);
594
+ this.root.dataset.roomFull = String(full);
595
+ this.roomFull.hidden = !full;
596
+ this.roomFullLabel.textContent = label ?? '';
597
+ this.onRoomFullRetry = options?.onRetry ?? null;
598
+ this.roomFullRetry.hidden = !this.onRoomFullRetry;
599
+ }
600
+
431
601
  setPrompt(text: string | null): void {
432
602
  if (!text) {
433
603
  this.prompt.dataset.visible = 'false';
@@ -0,0 +1,33 @@
1
+ import type { WebGLRenderer } from 'three';
2
+
3
+ export const IMMERSIVE_VR = 'immersive-vr';
4
+
5
+ /**
6
+ * Whether this browser can start a WebXR immersive-vr session.
7
+ * Headset browsers resolve true; ordinary desktop Chrome resolves false.
8
+ */
9
+ export async function immersiveVrSupported(): Promise<boolean> {
10
+ const xr = navigator.xr;
11
+ if (!xr?.isSessionSupported) return false;
12
+ try {
13
+ return await xr.isSessionSupported(IMMERSIVE_VR);
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Ask the browser for an immersive VR session and hand it to Three.js.
21
+ * Must run from a user gesture (the Enter VR button).
22
+ */
23
+ export async function requestImmersiveVr(renderer: WebGLRenderer): Promise<XRSession> {
24
+ const xr = navigator.xr;
25
+ if (!xr?.requestSession) throw new Error('WebXR is not available');
26
+ renderer.xr.enabled = true;
27
+ renderer.xr.setReferenceSpaceType('local-floor');
28
+ const session = await xr.requestSession(IMMERSIVE_VR, {
29
+ optionalFeatures: ['local-floor', 'bounded-floor'],
30
+ });
31
+ await renderer.xr.setSession(session);
32
+ return session;
33
+ }