@ericchen1990/pano-view 5.0.0 → 5.1.0

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/dist/index.cjs CHANGED
@@ -2509,6 +2509,19 @@ function createSpatialHost() {
2509
2509
  }
2510
2510
  var SpatialHostContext = (0, import_react.createContext)(null);
2511
2511
 
2512
+ // src/spatial/model-controls.ts
2513
+ function resolveSurfaceModelAdapter(surface, controls, hovered, previous) {
2514
+ let first = null;
2515
+ let retained = null;
2516
+ for (const [node, adapter] of controls) {
2517
+ if (!surface.contains(node)) continue;
2518
+ first ?? (first = adapter);
2519
+ if (adapter === hovered) return adapter;
2520
+ if (adapter === previous) retained = adapter;
2521
+ }
2522
+ return retained ?? first;
2523
+ }
2524
+
2512
2525
  // src/controls/actions.ts
2513
2526
  var CONTROL_PATHS = {
2514
2527
  play: "M8 5 L19 12 L8 19 Z",
@@ -2532,6 +2545,7 @@ var CONTROL_PATHS = {
2532
2545
  function mediaActions(controller, seekSpeed = 10, volumeSpeed = 0.25) {
2533
2546
  if (!controller) return [];
2534
2547
  const snapshot = () => controller.getSnapshot();
2548
+ const safeVolumeSpeed = Number.isFinite(volumeSpeed) && volumeSpeed >= 0 ? volumeSpeed : 0.25;
2535
2549
  return [
2536
2550
  {
2537
2551
  id: snapshot().playing ? "pause" : "play",
@@ -2556,22 +2570,25 @@ function mediaActions(controller, seekSpeed = 10, volumeSpeed = 0.25) {
2556
2570
  )
2557
2571
  )
2558
2572
  })),
2559
- ...(snapshot().volumeAdjustable ? [-1, 1] : []).map((direction) => ({
2573
+ ...(snapshot().hasAudio !== false && snapshot().volumeAdjustable ? [-1, 1] : []).map((direction) => ({
2560
2574
  id: direction < 0 ? "vol-" : "vol+",
2561
2575
  label: direction < 0 ? "Volume down" : "Volume up",
2562
- enabled: () => snapshot().volumeAdjustable && (direction < 0 ? snapshot().volume > 0 : snapshot().volume < 1),
2563
- update: (dt) => controller.setVolume(
2564
- Math.max(
2565
- 0,
2566
- Math.min(1, snapshot().volume + direction * dt * volumeSpeed)
2567
- )
2568
- )
2576
+ enabled: () => snapshot().hasAudio !== false && snapshot().volumeAdjustable && (direction < 0 ? snapshot().volume > 0 : snapshot().volume < 1),
2577
+ update: (dt) => {
2578
+ if (!Number.isFinite(dt) || dt <= 0 || snapshot().hasAudio === false || !snapshot().volumeAdjustable) return;
2579
+ controller.setVolume(
2580
+ Math.max(0, Math.min(1, snapshot().volume + direction * dt * safeVolumeSpeed))
2581
+ );
2582
+ }
2569
2583
  })),
2570
- {
2584
+ ...snapshot().hasAudio === false ? [] : [{
2571
2585
  id: snapshot().muted ? "unmute" : "mute",
2572
2586
  label: snapshot().muted ? "Unmute" : "Mute",
2573
- activate: () => controller.setMuted(!snapshot().muted)
2574
- }
2587
+ enabled: () => snapshot().hasAudio !== false,
2588
+ activate: () => {
2589
+ if (snapshot().hasAudio !== false) controller.setMuted(!snapshot().muted);
2590
+ }
2591
+ }]
2575
2592
  ];
2576
2593
  }
2577
2594
  function modelActions(adapter, rotateSpeed = 30, zoomSpeed = 1.5) {
@@ -2710,6 +2727,35 @@ function scrollViewportBounds(root, target, maxHeight) {
2710
2727
  };
2711
2728
  }
2712
2729
 
2730
+ // src/controls/media-audio.ts
2731
+ function probeMediaHasAudio(media) {
2732
+ if (media.readyState < 1) return void 0;
2733
+ const metadata = media;
2734
+ if ((metadata.audioTracks?.length ?? 0) > 0 || metadata.mozHasAudio === true || (metadata.webkitAudioDecodedByteCount ?? 0) > 0) return true;
2735
+ if (metadata.audioTracks?.length === 0 || metadata.mozHasAudio === false) return false;
2736
+ return void 0;
2737
+ }
2738
+ function subscribeMediaAudio(media, notify2) {
2739
+ let tracks;
2740
+ const trackEvents = ["addtrack", "removetrack", "change"];
2741
+ const sync = () => {
2742
+ const next = media.audioTracks;
2743
+ if (next !== tracks) {
2744
+ trackEvents.forEach((event) => tracks?.removeEventListener(event, notify2));
2745
+ tracks = next;
2746
+ trackEvents.forEach((event) => tracks?.addEventListener(event, notify2));
2747
+ }
2748
+ notify2();
2749
+ };
2750
+ const events = ["loadstart", "emptied", "loadedmetadata", "loadeddata", "playing", "timeupdate"];
2751
+ events.forEach((event) => media.addEventListener(event, sync));
2752
+ sync();
2753
+ return () => {
2754
+ events.forEach((event) => media.removeEventListener(event, sync));
2755
+ trackEvents.forEach((event) => tracks?.removeEventListener(event, notify2));
2756
+ };
2757
+ }
2758
+
2713
2759
  // src/video/format.ts
2714
2760
  var DEFAULT_PANO_VIDEO_PLAYBACK_RATES = [0.5, 1, 1.25, 1.5, 2];
2715
2761
  var MEDIA_PROBE_EPSILON = 1e-3;
@@ -2839,6 +2885,29 @@ function cueText(cue) {
2839
2885
  return cue.text.replace(/<[^>]+>/g, "").replace(/\r\n|\r/g, "\n");
2840
2886
  }
2841
2887
 
2888
+ // src/controls/layout.ts
2889
+ var CONTROL_GROUP_DEFAULTS = { size: 48, gap: 8, padding: 8 };
2890
+ function controlGroupLayout(count, width, size = CONTROL_GROUP_DEFAULTS.size, gap = CONTROL_GROUP_DEFAULTS.gap, padding = CONTROL_GROUP_DEFAULTS.padding) {
2891
+ size = Math.max(1, size);
2892
+ gap = Math.max(0, gap);
2893
+ padding = Math.max(0, padding);
2894
+ const columns = Math.max(1, Math.floor((width - 2 * padding + gap) / (size + gap)));
2895
+ const rows = Math.ceil(count / columns);
2896
+ return {
2897
+ height: count ? rows * size + Math.max(0, rows - 1) * gap + 2 * padding : 0,
2898
+ buttons: Array.from({ length: count }, (_, i) => {
2899
+ const row = Math.floor(i / columns), items = Math.min(columns, count - row * columns);
2900
+ return { x: (width - items * size - (items - 1) * gap) / 2 + i % columns * (size + gap), y: padding + row * (size + gap), w: size, h: size };
2901
+ })
2902
+ };
2903
+ }
2904
+ function allowsAutomaticMediaControls(media) {
2905
+ return media.getAttribute("data-pano-media-controls") !== "none";
2906
+ }
2907
+ function shouldPauseSurfaceMedia(connected, registered) {
2908
+ return !connected || !registered;
2909
+ }
2910
+
2842
2911
  // src/controls/media.ts
2843
2912
  var import_react2 = require("react");
2844
2913
  var mediaControllers = /* @__PURE__ */ new WeakMap();
@@ -2863,6 +2932,7 @@ function usePanoMediaController(ref) {
2863
2932
  playing: media ? !media.paused && !media.ended : false,
2864
2933
  currentTime: media?.currentTime ?? 0,
2865
2934
  duration: media?.duration ?? 0,
2935
+ hasAudio: media ? probeMediaHasAudio(media) : void 0,
2866
2936
  volume: media?.volume ?? 1,
2867
2937
  volumeAdjustable: adjustable.current,
2868
2938
  muted: media?.muted ?? true
@@ -2900,19 +2970,17 @@ function usePanoMediaController(ref) {
2900
2970
  adjustable.current = probeVolumeAdjustable(media);
2901
2971
  const notify2 = () => listeners.current.forEach((fn) => fn());
2902
2972
  const events = [
2903
- "loadedmetadata",
2904
2973
  "durationchange",
2905
- "timeupdate",
2906
2974
  "play",
2907
2975
  "pause",
2908
2976
  "ended",
2909
2977
  "volumechange",
2910
- "error",
2911
- "emptied"
2978
+ "error"
2912
2979
  ];
2913
2980
  events.forEach((event) => media.addEventListener(event, notify2));
2914
- notify2();
2981
+ const unsubscribeAudio = subscribeMediaAudio(media, notify2);
2915
2982
  return () => {
2983
+ unsubscribeAudio();
2916
2984
  if (mediaControllers.get(media) === controller) mediaControllers.delete(media);
2917
2985
  events.forEach((event) => media.removeEventListener(event, notify2));
2918
2986
  };
@@ -3231,6 +3299,7 @@ function PanoControlGroup({
3231
3299
  {
3232
3300
  ...props,
3233
3301
  role: "group",
3302
+ style: { gap: `var(--pano-control-gap, ${CONTROL_GROUP_DEFAULTS.gap}px)`, padding: `var(--pano-control-padding, ${CONTROL_GROUP_DEFAULTS.padding}px)`, ...props.style },
3234
3303
  "data-pano-control-group": "",
3235
3304
  className: `pano-control-group ${props.className ?? ""}`,
3236
3305
  children: children ? children(actions) : actions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -4023,7 +4092,7 @@ function SurfaceMediaControls({ surface }) {
4023
4092
  const sync = () => {
4024
4093
  const videos = [...surface.element.querySelectorAll("video")];
4025
4094
  const automatic = videos.filter((video) => {
4026
- if (explicitTargets.has(mediaControllers.get(video) ?? video)) return false;
4095
+ if (!allowsAutomaticMediaControls(video) || explicitTargets.has(mediaControllers.get(video) ?? video)) return false;
4027
4096
  return ![...host.controls].some(([node]) => node === video || node.contains(video));
4028
4097
  });
4029
4098
  let changed = false;
@@ -4096,7 +4165,7 @@ function makeCanvas() {
4096
4165
  canvas.height = HEIGHT * RASTER_SCALE;
4097
4166
  return canvas;
4098
4167
  }
4099
- async function capture(element, owner) {
4168
+ async function capture(element, owner, footer = false) {
4100
4169
  const canvas = document.createElement("canvas");
4101
4170
  const ctx = canvas.getContext("2d");
4102
4171
  const native = ctx;
@@ -4130,6 +4199,10 @@ async function capture(element, owner) {
4130
4199
  transform: "none",
4131
4200
  position: "relative"
4132
4201
  });
4202
+ if (footer) {
4203
+ clone2.style.borderBottomLeftRadius = "0";
4204
+ clone2.style.borderBottomRightRadius = "0";
4205
+ }
4133
4206
  clone2.setAttribute("drawable", "");
4134
4207
  canvas.append(clone2);
4135
4208
  owner.append(canvas);
@@ -4224,6 +4297,10 @@ async function capture(element, owner) {
4224
4297
  top: "0",
4225
4298
  transform: "none"
4226
4299
  });
4300
+ if (footer) {
4301
+ clone.style.borderBottomLeftRadius = "0";
4302
+ clone.style.borderBottomRightRadius = "0";
4303
+ }
4227
4304
  owner.append(clone);
4228
4305
  originals.forEach((node, i) => {
4229
4306
  copies[i].scrollTop = node.scrollTop;
@@ -4301,6 +4378,7 @@ function SurfacePanel({ surface }) {
4301
4378
  edgeButtons: [],
4302
4379
  target: surface.element,
4303
4380
  adapter: null,
4381
+ modelAdapter: null,
4304
4382
  scrollTarget: null,
4305
4383
  mediaTarget: null
4306
4384
  });
@@ -4311,7 +4389,7 @@ function SurfacePanel({ surface }) {
4311
4389
  controller = {
4312
4390
  subscribe: () => () => {
4313
4391
  },
4314
- getSnapshot: () => ({ ready: media.readyState >= 1, playing: !media.paused && !media.ended, currentTime: media.currentTime, duration: media.duration, volume: media.volume, volumeAdjustable: volumeCapabilities.get(media) ?? false, muted: media.muted }),
4392
+ getSnapshot: () => ({ ready: media.readyState >= 1, playing: !media.paused && !media.ended, currentTime: media.currentTime, duration: media.duration, hasAudio: probeMediaHasAudio(media), volume: media.volume, volumeAdjustable: volumeCapabilities.get(media) ?? false, muted: media.muted }),
4315
4393
  play: async () => {
4316
4394
  try {
4317
4395
  await media.play();
@@ -4328,7 +4406,7 @@ function SurfacePanel({ surface }) {
4328
4406
  if (Number.isFinite(time) && Number.isFinite(media.duration)) media.currentTime = Math.max(0, Math.min(media.duration, time));
4329
4407
  },
4330
4408
  setVolume: (value) => {
4331
- media.volume = value;
4409
+ if (Number.isFinite(value) && volumeCapabilities.get(media)) media.volume = clampVolume(value);
4332
4410
  },
4333
4411
  setMuted: (value) => {
4334
4412
  media.muted = value;
@@ -4345,6 +4423,7 @@ function SurfacePanel({ surface }) {
4345
4423
  s.generation++;
4346
4424
  s.target = surface.element;
4347
4425
  s.adapter = null;
4426
+ s.modelAdapter = null;
4348
4427
  s.dirty = true;
4349
4428
  s.image = null;
4350
4429
  s.mediaBlocked = false;
@@ -4424,7 +4503,7 @@ function SurfacePanel({ surface }) {
4424
4503
  videos.current.clear();
4425
4504
  controllers.current.clear();
4426
4505
  blockedMedia.current = /* @__PURE__ */ new WeakSet();
4427
- surface.element.querySelectorAll("video,audio").forEach((m) => m.pause());
4506
+ if (shouldPauseSurfaceMedia(surface.element.isConnected, host.surfaces.has(surface))) surface.element.querySelectorAll("video,audio").forEach((m) => m.pause());
4428
4507
  };
4429
4508
  }, [surface, camera, gl, host, sceneHost, scratch]);
4430
4509
  (0, import_react9.useEffect)(
@@ -4542,9 +4621,15 @@ function SurfacePanel({ surface }) {
4542
4621
  s.dirty = true;
4543
4622
  }
4544
4623
  const ctx = canvas.getContext("2d");
4624
+ const appearance = getComputedStyle(surface.element);
4625
+ const token = (name, fallback) => {
4626
+ const value = parseFloat(appearance.getPropertyValue(`--pano-control-${name}`));
4627
+ return Number.isFinite(value) ? value : fallback;
4628
+ };
4629
+ const controlSize = token("size", CONTROL_GROUP_DEFAULTS.size), controlGap = token("gap", CONTROL_GROUP_DEFAULTS.gap), controlPadding = token("padding", CONTROL_GROUP_DEFAULTS.padding);
4545
4630
  const nextHeight = Math.max(
4546
4631
  surface.anchor ? 1 : 200,
4547
- Math.min(BODY_HEIGHT, surface.element.offsetHeight) + (s.buttons.length ? 32 + Math.ceil(s.buttons.length / 5) * 88 : 0)
4632
+ Math.min(BODY_HEIGHT, surface.element.offsetHeight) + (s.buttons.length ? controlGroupLayout(s.buttons.length, WIDTH, controlSize, controlGap, controlPadding).height : 0)
4548
4633
  );
4549
4634
  if (canvas.height !== nextHeight * RASTER_SCALE) {
4550
4635
  texture.dispose();
@@ -4581,7 +4666,7 @@ function SurfacePanel({ surface }) {
4581
4666
  });
4582
4667
  owner.setAttribute("aria-hidden", "true");
4583
4668
  surface.element.closest("[data-pano-viewer]")?.append(owner);
4584
- capture(surface.element, owner).then((image) => {
4669
+ capture(surface.element, owner, s.buttons.length > 0).then((image) => {
4585
4670
  if (!s.disposed && generation === s.generation && revision === s.revision) {
4586
4671
  s.image = image;
4587
4672
  s.failed = false;
@@ -4597,7 +4682,6 @@ function SurfacePanel({ surface }) {
4597
4682
  s.busy = false;
4598
4683
  });
4599
4684
  }
4600
- const appearance = getComputedStyle(surface.element);
4601
4685
  const radius = Math.max(0, parseFloat(appearance.borderTopLeftRadius) || 0);
4602
4686
  ctx.clearRect(0, 0, WIDTH, s.height);
4603
4687
  ctx.save();
@@ -4746,31 +4830,55 @@ function SurfacePanel({ surface }) {
4746
4830
  s.edgeButtons = !host.input.hasStick && !surface.anchor && s.scrollTarget && !explicitTargets.has(s.scrollTarget) ? scrollEdges(s.scrollTarget, scrollViewportBounds(surface.element, s.scrollTarget, Math.min(BODY_HEIGHT, surface.element.offsetHeight)), () => {
4747
4831
  s.dirty = true;
4748
4832
  }) : [];
4749
- const adapter = s.adapter;
4833
+ const adapter = resolveSurfaceModelAdapter(surface.element, host.controls, s.adapter, s.modelAdapter);
4834
+ s.modelAdapter = adapter;
4750
4835
  if (adapter && !explicitTargets.has(adapter)) for (const action of modelActions(adapter)) {
4751
4836
  if (host.input.canDrag && adapter.rotateBy && ["left", "right", "up", "down"].includes(action.id) || host.input.hasStick && adapter.zoomBy && ["in", "out"].includes(action.id)) continue;
4752
4837
  add(action.id, action.label, action.update, action.activate, action.enabled);
4753
4838
  }
4754
- if (media instanceof HTMLAudioElement && !adapter && !explicitTargets.has(mediaControllers.get(media) ?? media)) {
4839
+ if (media instanceof HTMLAudioElement && allowsAutomaticMediaControls(media) && !adapter && !explicitTargets.has(mediaControllers.get(media) ?? media)) {
4755
4840
  for (const action of mediaActions(controllerFor(media))) add(action.id === "pause" ? "play" : action.id === "unmute" ? "mute" : action.id, action.label, action.update, action.activate, action.enabled);
4756
4841
  }
4757
- s.buttons = surface.anchor ? [] : buttons;
4758
4842
  if (surface.anchor) buttons.length = 0;
4843
+ const layout = controlGroupLayout(buttons.length, WIDTH, controlSize, controlGap, controlPadding);
4844
+ const bodyHeight = Math.min(BODY_HEIGHT, surface.element.offsetHeight);
4845
+ buttons.forEach((button, i) => Object.assign(button, layout.buttons[i], { y: bodyHeight + layout.buttons[i].y }));
4846
+ if (s.buttons.length !== buttons.length) s.dirty = true;
4847
+ s.buttons = buttons;
4848
+ if (buttons.length) {
4849
+ ctx.fillStyle = "#161b22";
4850
+ ctx.fillRect(0, bodyHeight, WIDTH, layout.height);
4851
+ }
4759
4852
  for (const b of buttons) {
4760
- ctx.fillStyle = b.action.enabled?.() === false ? "#333a43" : appearance.getPropertyValue("--pano-control-background").trim() || "#39424e";
4761
- ctx.fillRect(b.x, b.y, b.w, b.h);
4762
- ctx.fillStyle = "#fff";
4853
+ ctx.save();
4854
+ ctx.globalAlpha = b.action.enabled?.() === false ? 0.4 : 1;
4855
+ ctx.fillStyle = appearance.getPropertyValue("--pano-control-background").trim() || "#39424e";
4856
+ ctx.beginPath();
4857
+ ctx.roundRect(b.x, b.y, b.w, b.h, token("radius", b.w / 2));
4858
+ ctx.fill();
4859
+ const border = /^(\d+(?:\.\d+)?)px\s+solid\s+(.+)$/.exec(appearance.getPropertyValue("--pano-control-border").trim() || "1px solid #68778a");
4860
+ if (border) {
4861
+ ctx.strokeStyle = border[2];
4862
+ ctx.lineWidth = Number(border[1]);
4863
+ ctx.stroke();
4864
+ }
4865
+ if (host.action?.id === b.action.id) {
4866
+ ctx.strokeStyle = appearance.getPropertyValue("--pano-control-focus").trim() || "#ffb300";
4867
+ ctx.lineWidth = 3;
4868
+ ctx.stroke();
4869
+ }
4763
4870
  const id = String(b.action.id).replace("scroll-", "");
4764
4871
  const icon = id === "play" && media && !media.paused ? "pause" : id === "mute" && media?.muted ? "unmute" : id;
4765
4872
  ctx.save();
4766
- ctx.translate(b.x + b.w / 2 - 16, b.y + b.h / 2 - 16);
4767
- ctx.scale(32 / 24, 32 / 24);
4768
- ctx.strokeStyle = "#fff";
4873
+ ctx.translate(b.x + b.w / 2 - 12, b.y + b.h / 2 - 12);
4874
+ ctx.scale(1, 1);
4875
+ ctx.strokeStyle = appearance.getPropertyValue("--pano-control-color").trim() || "#baf5ff";
4769
4876
  ctx.lineWidth = 2;
4770
4877
  ctx.lineCap = "round";
4771
4878
  ctx.lineJoin = "round";
4772
4879
  ctx.stroke(new Path2D(CONTROL_PATHS[icon] ?? CONTROL_PATHS.reset));
4773
4880
  ctx.restore();
4881
+ ctx.restore();
4774
4882
  }
4775
4883
  for (const button of s.edgeButtons) {
4776
4884
  const focused = host.action?.id === button.action.id || [...contacts.current.values()].some((contact) => contact.action?.id === button.action.id);
@@ -9696,6 +9804,7 @@ function VolumeControl({
9696
9804
  const mutedOrSilent = muted || volume === 0;
9697
9805
  (0, import_react30.useEffect)(() => {
9698
9806
  onOpenChange(open);
9807
+ return () => onOpenChange(false);
9699
9808
  }, [onOpenChange, open]);
9700
9809
  (0, import_react30.useEffect)(() => {
9701
9810
  if (!open || !volumeAdjustable || hasFinePointerHover()) {
@@ -10143,7 +10252,7 @@ function PanoVideoControlsHud({
10143
10252
  }
10144
10253
  ),
10145
10254
  hideTime ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "pano-video-time", children: formatMediaTime(duration) }),
10146
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
10255
+ snapshot.hasAudio === false ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
10147
10256
  VolumeControl,
10148
10257
  {
10149
10258
  appearance: resolved,
@@ -15086,6 +15195,32 @@ function suppressFollowingClick(target) {
15086
15195
  setTimeout(cleanup, CLICK_SUPPRESS_MS);
15087
15196
  }
15088
15197
 
15198
+ // src/hotspot/tooltip-layout.ts
15199
+ function tooltipPadding(value) {
15200
+ const values = String(value).trim().split(/\s+/).map((value2) => Math.max(0, parseFloat(value2) || 0));
15201
+ const [top = 0, right2 = top, bottom = top, left = right2] = values;
15202
+ return [top, right2, bottom, left];
15203
+ }
15204
+ function tooltipBaseline(top, lineHeight, ascent, descent) {
15205
+ return top + (lineHeight - ascent - descent) / 2 + ascent;
15206
+ }
15207
+ function tooltipLines(text, maxWidth, measure) {
15208
+ const lines = [];
15209
+ if (!text) return lines;
15210
+ for (const paragraph of text.split("\n")) {
15211
+ let line = "";
15212
+ for (const char of paragraph) {
15213
+ if (line && measure(line + char) > maxWidth) {
15214
+ lines.push(line);
15215
+ line = "";
15216
+ }
15217
+ line += char;
15218
+ }
15219
+ lines.push(line);
15220
+ }
15221
+ return lines;
15222
+ }
15223
+
15089
15224
  // src/hotspot/tooltip-transform.ts
15090
15225
  var import_three22 = require("three");
15091
15226
  var point = new import_three22.Vector3();
@@ -15179,25 +15314,17 @@ function SpatialHotspotTooltip({
15179
15314
  }, [content.image]);
15180
15315
  const raster = (0, import_react47.useMemo)(() => {
15181
15316
  const fontSize = Math.max(1, number(appearance.fontSize, 12));
15182
- const padding = Math.max(0, number(appearance.padding, 8));
15317
+ const [pt, pr, pb, pl] = tooltipPadding(appearance.padding);
15318
+ const padding = Math.max(pl, pr);
15319
+ const letterSpacing = appearance.letterSpacing === "normal" ? 0 : number(appearance.letterSpacing ?? 0, 0);
15183
15320
  const leading = appearance.lineHeight;
15184
15321
  const lineHeight = typeof leading === "string" && leading.endsWith("px") ? number(leading, fontSize * 1.4) : fontSize * number(leading, 1.4) / (typeof leading === "string" && leading.endsWith("%") ? 100 : 1);
15185
15322
  const canvas = document.createElement("canvas");
15186
15323
  const ctx = canvas.getContext("2d");
15187
15324
  const font = `${fontSize}px ${appearance.fontFamily}`;
15188
15325
  ctx.font = font;
15189
- const lines = [];
15190
- for (const paragraph of (content.text ?? "").split("\n")) {
15191
- let line = "";
15192
- for (const char of paragraph) {
15193
- if (line && ctx.measureText(line + char).width > 220 - padding * 2) {
15194
- lines.push(line);
15195
- line = "";
15196
- }
15197
- line += char;
15198
- }
15199
- if (line || content.text) lines.push(line);
15200
- }
15326
+ const measure = (text) => (letterSpacing ? [...text].reduce((width2, char) => width2 + ctx.measureText(char).width, 0) : ctx.measureText(text).width) + Math.max(0, [...text].length - 1) * letterSpacing;
15327
+ const lines = tooltipLines(content.text ?? "", 220 - pl - pr, measure);
15201
15328
  const imageWidth = image ? Math.min(
15202
15329
  220 - padding * 2,
15203
15330
  120 * image.naturalWidth / image.naturalHeight
@@ -15206,12 +15333,12 @@ function SpatialHotspotTooltip({
15206
15333
  const width = Math.ceil(
15207
15334
  Math.max(
15208
15335
  imageWidth,
15209
- ...lines.map((line) => ctx.measureText(line).width),
15336
+ ...lines.map(measure),
15210
15337
  1
15211
- ) + padding * 2
15338
+ ) + pl + pr
15212
15339
  );
15213
15340
  const height = Math.ceil(
15214
- padding * 2 + imageHeight + (image && lines.length ? padding : 0) + lines.length * lineHeight
15341
+ pt + pb + imageHeight + (image && lines.length ? padding : 0) + lines.length * lineHeight
15215
15342
  );
15216
15343
  canvas.width = width * 2;
15217
15344
  canvas.height = height * 2;
@@ -15234,17 +15361,26 @@ function SpatialHotspotTooltip({
15234
15361
  }
15235
15362
  ctx.save();
15236
15363
  ctx.clip();
15237
- let top = padding;
15364
+ let top = pt;
15238
15365
  if (image) {
15239
- ctx.drawImage(image, padding, top, imageWidth, imageHeight);
15366
+ ctx.drawImage(image, pl, top, imageWidth, imageHeight);
15240
15367
  top += imageHeight + padding;
15241
15368
  }
15242
15369
  ctx.font = font;
15243
15370
  ctx.fillStyle = appearance.color;
15244
- ctx.textBaseline = "top";
15245
- lines.forEach(
15246
- (line, i) => ctx.fillText(line, padding, top + i * lineHeight)
15247
- );
15371
+ ctx.textBaseline = "alphabetic";
15372
+ lines.forEach((line, i) => {
15373
+ const metrics = ctx.measureText(line || "Mg");
15374
+ const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
15375
+ const descent = metrics.actualBoundingBoxDescent ?? fontSize * 0.2;
15376
+ let x = pl;
15377
+ const y = tooltipBaseline(top + i * lineHeight, lineHeight, ascent, descent);
15378
+ if (!letterSpacing) ctx.fillText(line, x, y);
15379
+ else for (const char of line) {
15380
+ ctx.fillText(char, x, y);
15381
+ x += ctx.measureText(char).width + letterSpacing;
15382
+ }
15383
+ });
15248
15384
  ctx.restore();
15249
15385
  const texture = new import_three23.CanvasTexture(canvas);
15250
15386
  texture.colorSpace = import_three23.SRGBColorSpace;
@@ -19291,7 +19427,7 @@ function BackgroundAudio({
19291
19427
  setInternalPlaying((prev) => !prev);
19292
19428
  },
19293
19429
  setVolume: (vol) => {
19294
- if (isControlledRef.current) {
19430
+ if (isControlledRef.current || !Number.isFinite(vol)) {
19295
19431
  return;
19296
19432
  }
19297
19433
  const next = clampAudioVolume(vol);
@@ -20629,21 +20765,34 @@ function subscribeWebVRPresentationExit(element, onExit) {
20629
20765
  });
20630
20766
  }
20631
20767
 
20768
+ // src/webvr/reticle-geometry.ts
20769
+ var import_three41 = require("three");
20770
+ function createReticleRing(inner, outer) {
20771
+ const geometry = new import_three41.RingGeometry(inner, outer, 96, 1, Math.PI / 2, -Math.PI * 2);
20772
+ const source = geometry.index;
20773
+ const indices = [];
20774
+ for (let segment = 0; segment < 96; segment++) {
20775
+ if (segment % 4 >= 2) continue;
20776
+ for (let vertex = 0; vertex < 6; vertex++) indices.push(source.getX(segment * 6 + vertex));
20777
+ }
20778
+ geometry.setIndex(indices);
20779
+ return geometry;
20780
+ }
20781
+
20632
20782
  // src/webvr/reticle.tsx
20633
20783
  var import_fiber31 = require("@react-three/fiber");
20634
20784
  var import_react65 = require("react");
20635
- var import_three41 = require("three");
20785
+ var import_three42 = require("three");
20636
20786
  var import_jsx_runtime44 = require("react/jsx-runtime");
20637
- var CENTER_NDC = new import_three41.Vector2(0, 0);
20787
+ var CENTER_NDC = new import_three42.Vector2(0, 0);
20638
20788
  var HUD_DISTANCE = 2;
20639
20789
  var RETICLE_SCALE = 1;
20640
20790
  var MOBILE_VR_RETICLE_SCALE = 3;
20641
20791
  var RETICLE_RENDER_ORDER = 1e5;
20642
20792
  var CROSSHAIR_LENGTH = 0.044;
20643
- var CROSSHAIR_THICKNESS = 8e-3;
20793
+ var CROSSHAIR_THICKNESS = 6e-3;
20644
20794
  var RING_INNER_RADIUS = 0.05;
20645
20795
  var RING_OUTER_RADIUS = 0.058;
20646
- var RING_SEGMENTS = 96;
20647
20796
  var RING_INDICES_PER_SEGMENT = 6;
20648
20797
  var GAZE_POINTER_ID = 1000001;
20649
20798
  var DEFAULT_WEBVR_CURSOR_DWELL_MS = 1500;
@@ -20725,8 +20874,17 @@ function WebVRReticle({
20725
20874
  }, []);
20726
20875
  const groupRef = (0, import_react65.useRef)(null);
20727
20876
  const ringMeshRef = (0, import_react65.useRef)(null);
20728
- const directionRef = (0, import_react65.useRef)(new import_three41.Vector3());
20729
- const raycasterRef = (0, import_react65.useRef)(new import_three41.Raycaster());
20877
+ const ringBackdropRef = (0, import_react65.useRef)(null);
20878
+ const rings = (0, import_react65.useMemo)(() => ({
20879
+ outline: createReticleRing(RING_INNER_RADIUS - 3e-3, RING_OUTER_RADIUS + 3e-3),
20880
+ track: createReticleRing(RING_INNER_RADIUS, RING_OUTER_RADIUS),
20881
+ progress: createReticleRing(RING_INNER_RADIUS, RING_OUTER_RADIUS)
20882
+ }), []);
20883
+ (0, import_react65.useEffect)(() => () => {
20884
+ Object.values(rings).forEach((geometry) => geometry.dispose());
20885
+ }, [rings]);
20886
+ const directionRef = (0, import_react65.useRef)(new import_three42.Vector3());
20887
+ const raycasterRef = (0, import_react65.useRef)(new import_three42.Raycaster());
20730
20888
  const hoveredRef = (0, import_react65.useRef)(null);
20731
20889
  const dwellRef = (0, import_react65.useRef)(0);
20732
20890
  const clickedRef = (0, import_react65.useRef)(false);
@@ -20751,6 +20909,7 @@ function WebVRReticle({
20751
20909
  }
20752
20910
  dwellRef.current = 0;
20753
20911
  clickedRef.current = false;
20912
+ if (ringBackdropRef.current) ringBackdropRef.current.visible = false;
20754
20913
  if (ringMesh) {
20755
20914
  ringMesh.visible = false;
20756
20915
  ringMesh.geometry.setDrawRange(0, 0);
@@ -20813,8 +20972,9 @@ function WebVRReticle({
20813
20972
  dwellRef.current = 0;
20814
20973
  }
20815
20974
  const progress = spatialOpen ? spatialProgress : target && !clickedRef.current ? resolvedDwellMs > 0 ? Math.min(dwellRef.current / resolvedDwellMs, 1) : 1 : 0;
20975
+ if (ringBackdropRef.current) ringBackdropRef.current.visible = progress > 1e-3;
20816
20976
  if (ringMesh) {
20817
- const segments = Math.max(0, Math.ceil(progress * RING_SEGMENTS));
20977
+ const segments = Math.max(0, Math.ceil(progress * rings.progress.index.count / RING_INDICES_PER_SEGMENT));
20818
20978
  ringMesh.visible = progress > 1e-3;
20819
20979
  ringMesh.geometry.setDrawRange(0, segments * RING_INDICES_PER_SEGMENT);
20820
20980
  }
@@ -20830,6 +20990,14 @@ function WebVRReticle({
20830
20990
  renderOrder: RETICLE_RENDER_ORDER,
20831
20991
  visible,
20832
20992
  children: [
20993
+ [false, true].map((vertical) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("mesh", { frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER, children: [
20994
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("boxGeometry", { args: vertical ? [CROSSHAIR_THICKNESS + 6e-3, CROSSHAIR_LENGTH + 6e-3, 1e-3] : [CROSSHAIR_LENGTH + 6e-3, CROSSHAIR_THICKNESS + 6e-3, 1e-3] }),
20995
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("meshBasicMaterial", { color: "#101419", transparent: true, depthTest: false, depthWrite: false, toneMapped: false })
20996
+ ] }, String(vertical))),
20997
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("group", { ref: ringBackdropRef, renderOrder: RETICLE_RENDER_ORDER, visible: false, children: [
20998
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("mesh", { geometry: rings.outline, frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("meshBasicMaterial", { color: "#101419", transparent: true, side: import_three42.DoubleSide, depthTest: false, depthWrite: false, toneMapped: false }) }),
20999
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("mesh", { geometry: rings.track, frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER + 1, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("meshBasicMaterial", { color: "#ffffff", transparent: true, opacity: 0.45, side: import_three42.DoubleSide, depthTest: false, depthWrite: false, toneMapped: false }) })
21000
+ ] }),
20833
21001
  /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
20834
21002
  "mesh",
20835
21003
  {
@@ -20872,40 +21040,26 @@ function WebVRReticle({
20872
21040
  ]
20873
21041
  }
20874
21042
  ),
20875
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
21043
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
20876
21044
  "mesh",
20877
21045
  {
20878
21046
  frustumCulled: false,
20879
21047
  raycast: noopRaycast,
20880
21048
  ref: ringMeshRef,
21049
+ geometry: rings.progress,
20881
21050
  renderOrder: RETICLE_RENDER_ORDER + 2,
20882
21051
  visible: false,
20883
- children: [
20884
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
20885
- "ringGeometry",
20886
- {
20887
- args: [
20888
- RING_INNER_RADIUS,
20889
- RING_OUTER_RADIUS,
20890
- RING_SEGMENTS,
20891
- 1,
20892
- Math.PI / 2,
20893
- Math.PI * -2
20894
- ]
20895
- }
20896
- ),
20897
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
20898
- "meshBasicMaterial",
20899
- {
20900
- color: "#ffffff",
20901
- depthTest: false,
20902
- depthWrite: false,
20903
- side: import_three41.DoubleSide,
20904
- toneMapped: false,
20905
- transparent: true
20906
- }
20907
- )
20908
- ]
21052
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21053
+ "meshBasicMaterial",
21054
+ {
21055
+ color: "#ffffff",
21056
+ depthTest: false,
21057
+ depthWrite: false,
21058
+ side: import_three42.DoubleSide,
21059
+ toneMapped: false,
21060
+ transparent: true
21061
+ }
21062
+ )
20909
21063
  }
20910
21064
  )
20911
21065
  ]
@@ -20937,7 +21091,7 @@ void main() {
20937
21091
  // src/webvr/stereo.tsx
20938
21092
  var import_fiber32 = require("@react-three/fiber");
20939
21093
  var import_react66 = require("react");
20940
- var import_three42 = require("three");
21094
+ var import_three43 = require("three");
20941
21095
 
20942
21096
  // src/webvr/lens-center.ts
20943
21097
  function physicalScreenFromDiagonal(screensizeInches, displayAspect) {
@@ -21028,7 +21182,7 @@ function computeStereoLayout(logicalWidth, logicalHeight, pixelRatio, oversampli
21028
21182
  }
21029
21183
 
21030
21184
  // src/webvr/stereo.tsx
21031
- var TOE_IN = new import_three42.Matrix4();
21185
+ var TOE_IN = new import_three43.Matrix4();
21032
21186
  var vertexShader = `
21033
21187
  varying vec2 vUv;
21034
21188
  void main() {
@@ -21052,38 +21206,38 @@ void main() {
21052
21206
  }
21053
21207
  `;
21054
21208
  function createDistortionResources() {
21055
- const target = new import_three42.WebGLRenderTarget(1, 1, {
21209
+ const target = new import_three43.WebGLRenderTarget(1, 1, {
21056
21210
  depthBuffer: true,
21057
21211
  stencilBuffer: false
21058
21212
  });
21059
- const geometry = new import_three42.PlaneGeometry(2, 2);
21060
- const material = new import_three42.ShaderMaterial({
21213
+ const geometry = new import_three43.PlaneGeometry(2, 2);
21214
+ const material = new import_three43.ShaderMaterial({
21061
21215
  depthTest: false,
21062
21216
  depthWrite: false,
21063
21217
  fragmentShader,
21064
21218
  uniforms: {
21065
- distortion: { value: new import_three42.Vector2() },
21066
- lensCenter: { value: new import_three42.Vector2() },
21219
+ distortion: { value: new import_three43.Vector2() },
21220
+ lensCenter: { value: new import_three43.Vector2() },
21067
21221
  sourceTexture: { value: target.texture }
21068
21222
  },
21069
21223
  vertexShader
21070
21224
  });
21071
- const mesh = new import_three42.Mesh(geometry, material);
21072
- const scene = new import_three42.Scene();
21225
+ const mesh = new import_three43.Mesh(geometry, material);
21226
+ const scene = new import_three43.Scene();
21073
21227
  scene.add(mesh);
21074
- const maskMaterial = new import_three42.ShaderMaterial({
21228
+ const maskMaterial = new import_three43.ShaderMaterial({
21075
21229
  vertexShader,
21076
21230
  fragmentShader: stereoLensMaskFragmentShader,
21077
21231
  uniforms: material.uniforms,
21078
21232
  depthTest: false,
21079
21233
  depthWrite: false
21080
21234
  });
21081
- const maskScene = new import_three42.Scene();
21082
- maskScene.add(new import_three42.Mesh(geometry, maskMaterial));
21235
+ const maskScene = new import_three43.Scene();
21236
+ maskScene.add(new import_three43.Mesh(geometry, maskMaterial));
21083
21237
  return {
21084
21238
  maskScene,
21085
21239
  maskMaterial,
21086
- camera: new import_three42.OrthographicCamera(-1, 1, 1, -1, 0, 1),
21240
+ camera: new import_three43.OrthographicCamera(-1, 1, 1, -1, 0, 1),
21087
21241
  geometry,
21088
21242
  material,
21089
21243
  mesh,
@@ -21144,15 +21298,15 @@ function WebVRStereoRenderer({
21144
21298
  const runtime = (0, import_react66.useContext)(WebVRRuntimeContext);
21145
21299
  const fallbackStereoRef = (0, import_react66.useRef)(null);
21146
21300
  if (!fallbackStereoRef.current) {
21147
- fallbackStereoRef.current = new import_three42.StereoCamera();
21301
+ fallbackStereoRef.current = new import_three43.StereoCamera();
21148
21302
  }
21149
21303
  const stereo = runtime?.stereoView.stereo ?? fallbackStereoRef.current;
21150
21304
  const stereoRef = (0, import_react66.useRef)(stereo);
21151
21305
  stereoRef.current = stereo;
21152
21306
  const resources = (0, import_react66.useMemo)(createDistortionResources, []);
21153
- const sizeRef = (0, import_react66.useRef)(new import_three42.Vector2());
21154
- const viewportRef = (0, import_react66.useRef)(new import_three42.Vector4());
21155
- const scissorRef = (0, import_react66.useRef)(new import_three42.Vector4());
21307
+ const sizeRef = (0, import_react66.useRef)(new import_three43.Vector2());
21308
+ const viewportRef = (0, import_react66.useRef)(new import_three43.Vector4());
21309
+ const scissorRef = (0, import_react66.useRef)(new import_three43.Vector4());
21156
21310
  const originalFovRef = (0, import_react66.useRef)(null);
21157
21311
  const stereoActiveRef = (0, import_react66.useRef)(false);
21158
21312
  const profileRef = (0, import_react66.useRef)(profile);
@@ -21173,7 +21327,7 @@ function WebVRStereoRenderer({
21173
21327
  (0, import_react66.useEffect)(() => {
21174
21328
  const originalRender = gl.render.bind(gl);
21175
21329
  gl.render = (sceneToRender, cameraToRender) => {
21176
- if (!stereoActiveRef.current || !(cameraToRender instanceof import_three42.PerspectiveCamera)) {
21330
+ if (!stereoActiveRef.current || !(cameraToRender instanceof import_three43.PerspectiveCamera)) {
21177
21331
  originalRender(sceneToRender, cameraToRender);
21178
21332
  return;
21179
21333
  }
@@ -21255,7 +21409,7 @@ function WebVRStereoRenderer({
21255
21409
  if (runtime) {
21256
21410
  runtime.stereoView.active = fallback;
21257
21411
  }
21258
- if (fallback && camera instanceof import_three42.PerspectiveCamera) {
21412
+ if (fallback && camera instanceof import_three43.PerspectiveCamera) {
21259
21413
  syncStereoCameras(
21260
21414
  stereoRef.current,
21261
21415
  camera,
@@ -21267,7 +21421,7 @@ function WebVRStereoRenderer({
21267
21421
  );
21268
21422
  return;
21269
21423
  }
21270
- if (originalFovRef.current !== null && camera instanceof import_three42.PerspectiveCamera) {
21424
+ if (originalFovRef.current !== null && camera instanceof import_three43.PerspectiveCamera) {
21271
21425
  camera.fov = originalFovRef.current;
21272
21426
  camera.updateProjectionMatrix();
21273
21427
  originalFovRef.current = null;
@@ -23042,7 +23196,7 @@ function usePanoViewer() {
23042
23196
  // src/video/pano-video.tsx
23043
23197
  var import_fiber34 = require("@react-three/fiber");
23044
23198
  var import_react72 = require("react");
23045
- var import_three43 = require("three");
23199
+ var import_three44 = require("three");
23046
23200
  var import_jsx_runtime47 = require("react/jsx-runtime");
23047
23201
  var EMPTY_TRACKS = [];
23048
23202
  var EMPTY_RATES = DEFAULT_PANO_VIDEO_PLAYBACK_RATES;
@@ -23119,8 +23273,8 @@ function findTextTrack(video, tracks, trackId) {
23119
23273
  return list.length > 0 ? list[0] : null;
23120
23274
  }
23121
23275
  function applyEquirectangularLayout(texture, anisotropy) {
23122
- texture.colorSpace = import_three43.SRGBColorSpace;
23123
- texture.wrapS = import_three43.RepeatWrapping;
23276
+ texture.colorSpace = import_three44.SRGBColorSpace;
23277
+ texture.wrapS = import_three44.RepeatWrapping;
23124
23278
  texture.repeat.x = -1;
23125
23279
  texture.offset.x = 1;
23126
23280
  texture.anisotropy = anisotropy;
@@ -23139,7 +23293,7 @@ function usePosterTexture2(poster, onError) {
23139
23293
  active = false;
23140
23294
  };
23141
23295
  }
23142
- const loader = new import_three43.TextureLoader();
23296
+ const loader = new import_three44.TextureLoader();
23143
23297
  loader.load(
23144
23298
  poster,
23145
23299
  (loadedTexture) => {
@@ -23151,9 +23305,9 @@ function usePosterTexture2(poster, onError) {
23151
23305
  loadedTexture,
23152
23306
  gl.capabilities.getMaxAnisotropy()
23153
23307
  );
23154
- loadedTexture.wrapT = import_three43.RepeatWrapping;
23155
- loadedTexture.magFilter = import_three43.LinearFilter;
23156
- loadedTexture.minFilter = import_three43.LinearMipmapLinearFilter;
23308
+ loadedTexture.wrapT = import_three44.RepeatWrapping;
23309
+ loadedTexture.magFilter = import_three44.LinearFilter;
23310
+ loadedTexture.minFilter = import_three44.LinearMipmapLinearFilter;
23157
23311
  loadedTexture.generateMipmaps = true;
23158
23312
  setTexture(loadedTexture);
23159
23313
  },
@@ -23307,10 +23461,10 @@ function PanoVideo({
23307
23461
  video.muted = true;
23308
23462
  applyInlineVideoPlayback(video, true);
23309
23463
  video.preload = "metadata";
23310
- const texture = new import_three43.VideoTexture(video);
23464
+ const texture = new import_three44.VideoTexture(video);
23311
23465
  applyEquirectangularLayout(texture, 1);
23312
- texture.magFilter = import_three43.LinearFilter;
23313
- texture.minFilter = import_three43.LinearFilter;
23466
+ texture.magFilter = import_three44.LinearFilter;
23467
+ texture.minFilter = import_three44.LinearFilter;
23314
23468
  texture.generateMipmaps = false;
23315
23469
  return { video, texture };
23316
23470
  }, []);
@@ -23371,6 +23525,7 @@ function PanoVideo({
23371
23525
  ended: playbackState === "ended",
23372
23526
  currentTime: video?.currentTime ?? 0,
23373
23527
  duration: video && Number.isFinite(video.duration) ? video.duration : 0,
23528
+ hasAudio: video ? probeMediaHasAudio(video) : void 0,
23374
23529
  volume: video ? clampVolume(video.volume) : clampVolume(volume),
23375
23530
  volumeAdjustable: mediaCapabilitiesRef.current.volumeAdjustable,
23376
23531
  muted: video?.muted ?? muted,
@@ -23572,7 +23727,7 @@ function PanoVideo({
23572
23727
  emitSnapshot();
23573
23728
  },
23574
23729
  setVolume: (nextVolume) => {
23575
- if (!mediaCapabilitiesRef.current.volumeAdjustable) {
23730
+ if (!Number.isFinite(nextVolume) || !mediaCapabilitiesRef.current.volumeAdjustable) {
23576
23731
  return;
23577
23732
  }
23578
23733
  video.volume = clampVolume(nextVolume);
@@ -23676,7 +23831,7 @@ function PanoVideo({
23676
23831
  const handleVolume = () => {
23677
23832
  emitSnapshot();
23678
23833
  };
23679
- video.addEventListener("timeupdate", handleTimeUpdate);
23834
+ const unsubscribeAudio = subscribeMediaAudio(video, handleTimeUpdate);
23680
23835
  video.addEventListener("durationchange", handleTimeUpdate);
23681
23836
  video.addEventListener("progress", handleTimeUpdate);
23682
23837
  video.addEventListener("play", handlePlay);
@@ -23686,7 +23841,7 @@ function PanoVideo({
23686
23841
  video.addEventListener("ratechange", handleVolume);
23687
23842
  return () => {
23688
23843
  active = false;
23689
- video.removeEventListener("timeupdate", handleTimeUpdate);
23844
+ unsubscribeAudio();
23690
23845
  video.removeEventListener("durationchange", handleTimeUpdate);
23691
23846
  video.removeEventListener("progress", handleTimeUpdate);
23692
23847
  video.removeEventListener("play", handlePlay);
@@ -23746,7 +23901,7 @@ function PanoVideo({
23746
23901
  activeTexture ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
23747
23902
  "mesh",
23748
23903
  {
23749
- rotation: [0, import_three43.MathUtils.degToRad(90 + yawOffset2), 0],
23904
+ rotation: [0, import_three44.MathUtils.degToRad(90 + yawOffset2), 0],
23750
23905
  visible,
23751
23906
  children: [
23752
23907
  /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
@@ -23754,7 +23909,7 @@ function PanoVideo({
23754
23909
  PanoBasicMaterial,
23755
23910
  {
23756
23911
  map: activeTexture,
23757
- side: import_three43.BackSide,
23912
+ side: import_three44.BackSide,
23758
23913
  toneMapped: false
23759
23914
  }
23760
23915
  )
@@ -24965,7 +25120,7 @@ function PanoMapOverlay({
24965
25120
  var import_react76 = require("react");
24966
25121
  var import_client2 = require("react-dom/client");
24967
25122
  var import_fiber35 = require("@react-three/fiber");
24968
- var import_three44 = require("three");
25123
+ var import_three45 = require("three");
24969
25124
 
24970
25125
  // src/webvr/scene-menu-layout.ts
24971
25126
  var bounded = (value, fallback, min, max) => Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
@@ -25175,7 +25330,7 @@ function MenuSurface({
25175
25330
  timer: 0
25176
25331
  });
25177
25332
  const group = (0, import_react76.useRef)(null);
25178
- const direction = (0, import_react76.useMemo)(() => new import_three44.Vector3(), []);
25333
+ const direction = (0, import_react76.useMemo)(() => new import_three45.Vector3(), []);
25179
25334
  const [reduced, setReduced] = (0, import_react76.useState)(false);
25180
25335
  (0, import_react76.useEffect)(() => {
25181
25336
  const media = matchMedia("(prefers-reduced-motion: reduce)");
@@ -25231,13 +25386,13 @@ function MenuSurface({
25231
25386
  const canvas = document.createElement("canvas");
25232
25387
  canvas.width = w * MENU_TEXTURE_SCALE;
25233
25388
  canvas.height = atlasHeight * MENU_TEXTURE_SCALE;
25234
- const texture = new import_three44.CanvasTexture(canvas);
25235
- texture.colorSpace = import_three44.SRGBColorSpace;
25236
- texture.minFilter = import_three44.LinearMipmapLinearFilter;
25237
- texture.magFilter = import_three44.LinearFilter;
25389
+ const texture = new import_three45.CanvasTexture(canvas);
25390
+ texture.colorSpace = import_three45.SRGBColorSpace;
25391
+ texture.minFilter = import_three45.LinearMipmapLinearFilter;
25392
+ texture.magFilter = import_three45.LinearFilter;
25238
25393
  texture.generateMipmaps = true;
25239
25394
  texture.anisotropy = Math.min(4, gl.capabilities.getMaxAnisotropy());
25240
- const material = new import_three44.MeshBasicMaterial({
25395
+ const material = new import_three45.MeshBasicMaterial({
25241
25396
  map: texture,
25242
25397
  transparent: true,
25243
25398
  depthTest: false,
@@ -25308,12 +25463,12 @@ function MenuSurface({
25308
25463
  }
25309
25464
  }
25310
25465
  }
25311
- const geometry = new import_three44.BufferGeometry();
25466
+ const geometry = new import_three45.BufferGeometry();
25312
25467
  geometry.setAttribute(
25313
25468
  "position",
25314
- new import_three44.Float32BufferAttribute(positions, 3)
25469
+ new import_three45.Float32BufferAttribute(positions, 3)
25315
25470
  );
25316
- geometry.setAttribute("uv", new import_three44.Float32BufferAttribute(uvs, 2));
25471
+ geometry.setAttribute("uv", new import_three45.Float32BufferAttribute(uvs, 2));
25317
25472
  geometry.computeVertexNormals();
25318
25473
  result.push({
25319
25474
  canvas,
@@ -25340,7 +25495,7 @@ function MenuSurface({
25340
25495
  );
25341
25496
  const blocker = (0, import_react76.useMemo)(() => {
25342
25497
  if (!open || layout !== "arc") return null;
25343
- const geometry = new import_three44.PlaneGeometry(1, 1, Math.ceil(degrees / 3), 1);
25498
+ const geometry = new import_three45.PlaneGeometry(1, 1, Math.ceil(degrees / 3), 1);
25344
25499
  const positions = geometry.attributes.position;
25345
25500
  const top = sceneMenuPoint(
25346
25501
  0,
@@ -25893,7 +26048,7 @@ function MenuSurface({
25893
26048
  },
25894
26049
  raycast: function(raycaster, intersections) {
25895
26050
  if (!state.current.closing && state.current.alpha > 0)
25896
- import_three44.Mesh.prototype.raycast.call(this, raycaster, intersections);
26051
+ import_three45.Mesh.prototype.raycast.call(this, raycaster, intersections);
25897
26052
  }
25898
26053
  },
25899
26054
  index