@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.mjs CHANGED
@@ -2409,6 +2409,19 @@ function createSpatialHost() {
2409
2409
  }
2410
2410
  var SpatialHostContext = createContext(null);
2411
2411
 
2412
+ // src/spatial/model-controls.ts
2413
+ function resolveSurfaceModelAdapter(surface, controls, hovered, previous) {
2414
+ let first = null;
2415
+ let retained = null;
2416
+ for (const [node, adapter] of controls) {
2417
+ if (!surface.contains(node)) continue;
2418
+ first ?? (first = adapter);
2419
+ if (adapter === hovered) return adapter;
2420
+ if (adapter === previous) retained = adapter;
2421
+ }
2422
+ return retained ?? first;
2423
+ }
2424
+
2412
2425
  // src/controls/actions.ts
2413
2426
  var CONTROL_PATHS = {
2414
2427
  play: "M8 5 L19 12 L8 19 Z",
@@ -2432,6 +2445,7 @@ var CONTROL_PATHS = {
2432
2445
  function mediaActions(controller, seekSpeed = 10, volumeSpeed = 0.25) {
2433
2446
  if (!controller) return [];
2434
2447
  const snapshot = () => controller.getSnapshot();
2448
+ const safeVolumeSpeed = Number.isFinite(volumeSpeed) && volumeSpeed >= 0 ? volumeSpeed : 0.25;
2435
2449
  return [
2436
2450
  {
2437
2451
  id: snapshot().playing ? "pause" : "play",
@@ -2456,22 +2470,25 @@ function mediaActions(controller, seekSpeed = 10, volumeSpeed = 0.25) {
2456
2470
  )
2457
2471
  )
2458
2472
  })),
2459
- ...(snapshot().volumeAdjustable ? [-1, 1] : []).map((direction) => ({
2473
+ ...(snapshot().hasAudio !== false && snapshot().volumeAdjustable ? [-1, 1] : []).map((direction) => ({
2460
2474
  id: direction < 0 ? "vol-" : "vol+",
2461
2475
  label: direction < 0 ? "Volume down" : "Volume up",
2462
- enabled: () => snapshot().volumeAdjustable && (direction < 0 ? snapshot().volume > 0 : snapshot().volume < 1),
2463
- update: (dt) => controller.setVolume(
2464
- Math.max(
2465
- 0,
2466
- Math.min(1, snapshot().volume + direction * dt * volumeSpeed)
2467
- )
2468
- )
2476
+ enabled: () => snapshot().hasAudio !== false && snapshot().volumeAdjustable && (direction < 0 ? snapshot().volume > 0 : snapshot().volume < 1),
2477
+ update: (dt) => {
2478
+ if (!Number.isFinite(dt) || dt <= 0 || snapshot().hasAudio === false || !snapshot().volumeAdjustable) return;
2479
+ controller.setVolume(
2480
+ Math.max(0, Math.min(1, snapshot().volume + direction * dt * safeVolumeSpeed))
2481
+ );
2482
+ }
2469
2483
  })),
2470
- {
2484
+ ...snapshot().hasAudio === false ? [] : [{
2471
2485
  id: snapshot().muted ? "unmute" : "mute",
2472
2486
  label: snapshot().muted ? "Unmute" : "Mute",
2473
- activate: () => controller.setMuted(!snapshot().muted)
2474
- }
2487
+ enabled: () => snapshot().hasAudio !== false,
2488
+ activate: () => {
2489
+ if (snapshot().hasAudio !== false) controller.setMuted(!snapshot().muted);
2490
+ }
2491
+ }]
2475
2492
  ];
2476
2493
  }
2477
2494
  function modelActions(adapter, rotateSpeed = 30, zoomSpeed = 1.5) {
@@ -2610,6 +2627,35 @@ function scrollViewportBounds(root, target, maxHeight) {
2610
2627
  };
2611
2628
  }
2612
2629
 
2630
+ // src/controls/media-audio.ts
2631
+ function probeMediaHasAudio(media) {
2632
+ if (media.readyState < 1) return void 0;
2633
+ const metadata = media;
2634
+ if ((metadata.audioTracks?.length ?? 0) > 0 || metadata.mozHasAudio === true || (metadata.webkitAudioDecodedByteCount ?? 0) > 0) return true;
2635
+ if (metadata.audioTracks?.length === 0 || metadata.mozHasAudio === false) return false;
2636
+ return void 0;
2637
+ }
2638
+ function subscribeMediaAudio(media, notify2) {
2639
+ let tracks;
2640
+ const trackEvents = ["addtrack", "removetrack", "change"];
2641
+ const sync = () => {
2642
+ const next = media.audioTracks;
2643
+ if (next !== tracks) {
2644
+ trackEvents.forEach((event) => tracks?.removeEventListener(event, notify2));
2645
+ tracks = next;
2646
+ trackEvents.forEach((event) => tracks?.addEventListener(event, notify2));
2647
+ }
2648
+ notify2();
2649
+ };
2650
+ const events = ["loadstart", "emptied", "loadedmetadata", "loadeddata", "playing", "timeupdate"];
2651
+ events.forEach((event) => media.addEventListener(event, sync));
2652
+ sync();
2653
+ return () => {
2654
+ events.forEach((event) => media.removeEventListener(event, sync));
2655
+ trackEvents.forEach((event) => tracks?.removeEventListener(event, notify2));
2656
+ };
2657
+ }
2658
+
2613
2659
  // src/video/format.ts
2614
2660
  var DEFAULT_PANO_VIDEO_PLAYBACK_RATES = [0.5, 1, 1.25, 1.5, 2];
2615
2661
  var MEDIA_PROBE_EPSILON = 1e-3;
@@ -2739,6 +2785,29 @@ function cueText(cue) {
2739
2785
  return cue.text.replace(/<[^>]+>/g, "").replace(/\r\n|\r/g, "\n");
2740
2786
  }
2741
2787
 
2788
+ // src/controls/layout.ts
2789
+ var CONTROL_GROUP_DEFAULTS = { size: 48, gap: 8, padding: 8 };
2790
+ function controlGroupLayout(count, width, size = CONTROL_GROUP_DEFAULTS.size, gap = CONTROL_GROUP_DEFAULTS.gap, padding = CONTROL_GROUP_DEFAULTS.padding) {
2791
+ size = Math.max(1, size);
2792
+ gap = Math.max(0, gap);
2793
+ padding = Math.max(0, padding);
2794
+ const columns = Math.max(1, Math.floor((width - 2 * padding + gap) / (size + gap)));
2795
+ const rows = Math.ceil(count / columns);
2796
+ return {
2797
+ height: count ? rows * size + Math.max(0, rows - 1) * gap + 2 * padding : 0,
2798
+ buttons: Array.from({ length: count }, (_, i) => {
2799
+ const row = Math.floor(i / columns), items = Math.min(columns, count - row * columns);
2800
+ return { x: (width - items * size - (items - 1) * gap) / 2 + i % columns * (size + gap), y: padding + row * (size + gap), w: size, h: size };
2801
+ })
2802
+ };
2803
+ }
2804
+ function allowsAutomaticMediaControls(media) {
2805
+ return media.getAttribute("data-pano-media-controls") !== "none";
2806
+ }
2807
+ function shouldPauseSurfaceMedia(connected, registered) {
2808
+ return !connected || !registered;
2809
+ }
2810
+
2742
2811
  // src/controls/media.ts
2743
2812
  import { useEffect, useMemo, useRef } from "react";
2744
2813
  var mediaControllers = /* @__PURE__ */ new WeakMap();
@@ -2763,6 +2832,7 @@ function usePanoMediaController(ref) {
2763
2832
  playing: media ? !media.paused && !media.ended : false,
2764
2833
  currentTime: media?.currentTime ?? 0,
2765
2834
  duration: media?.duration ?? 0,
2835
+ hasAudio: media ? probeMediaHasAudio(media) : void 0,
2766
2836
  volume: media?.volume ?? 1,
2767
2837
  volumeAdjustable: adjustable.current,
2768
2838
  muted: media?.muted ?? true
@@ -2800,19 +2870,17 @@ function usePanoMediaController(ref) {
2800
2870
  adjustable.current = probeVolumeAdjustable(media);
2801
2871
  const notify2 = () => listeners.current.forEach((fn) => fn());
2802
2872
  const events = [
2803
- "loadedmetadata",
2804
2873
  "durationchange",
2805
- "timeupdate",
2806
2874
  "play",
2807
2875
  "pause",
2808
2876
  "ended",
2809
2877
  "volumechange",
2810
- "error",
2811
- "emptied"
2878
+ "error"
2812
2879
  ];
2813
2880
  events.forEach((event) => media.addEventListener(event, notify2));
2814
- notify2();
2881
+ const unsubscribeAudio = subscribeMediaAudio(media, notify2);
2815
2882
  return () => {
2883
+ unsubscribeAudio();
2816
2884
  if (mediaControllers.get(media) === controller) mediaControllers.delete(media);
2817
2885
  events.forEach((event) => media.removeEventListener(event, notify2));
2818
2886
  };
@@ -3152,6 +3220,7 @@ function PanoControlGroup({
3152
3220
  {
3153
3221
  ...props,
3154
3222
  role: "group",
3223
+ style: { gap: `var(--pano-control-gap, ${CONTROL_GROUP_DEFAULTS.gap}px)`, padding: `var(--pano-control-padding, ${CONTROL_GROUP_DEFAULTS.padding}px)`, ...props.style },
3155
3224
  "data-pano-control-group": "",
3156
3225
  className: `pano-control-group ${props.className ?? ""}`,
3157
3226
  children: children ? children(actions) : actions.map((action) => /* @__PURE__ */ jsx2(
@@ -3958,7 +4027,7 @@ function SurfaceMediaControls({ surface }) {
3958
4027
  const sync = () => {
3959
4028
  const videos = [...surface.element.querySelectorAll("video")];
3960
4029
  const automatic = videos.filter((video) => {
3961
- if (explicitTargets.has(mediaControllers.get(video) ?? video)) return false;
4030
+ if (!allowsAutomaticMediaControls(video) || explicitTargets.has(mediaControllers.get(video) ?? video)) return false;
3962
4031
  return ![...host.controls].some(([node]) => node === video || node.contains(video));
3963
4032
  });
3964
4033
  let changed = false;
@@ -4031,7 +4100,7 @@ function makeCanvas() {
4031
4100
  canvas.height = HEIGHT * RASTER_SCALE;
4032
4101
  return canvas;
4033
4102
  }
4034
- async function capture(element, owner) {
4103
+ async function capture(element, owner, footer = false) {
4035
4104
  const canvas = document.createElement("canvas");
4036
4105
  const ctx = canvas.getContext("2d");
4037
4106
  const native = ctx;
@@ -4065,6 +4134,10 @@ async function capture(element, owner) {
4065
4134
  transform: "none",
4066
4135
  position: "relative"
4067
4136
  });
4137
+ if (footer) {
4138
+ clone2.style.borderBottomLeftRadius = "0";
4139
+ clone2.style.borderBottomRightRadius = "0";
4140
+ }
4068
4141
  clone2.setAttribute("drawable", "");
4069
4142
  canvas.append(clone2);
4070
4143
  owner.append(canvas);
@@ -4159,6 +4232,10 @@ async function capture(element, owner) {
4159
4232
  top: "0",
4160
4233
  transform: "none"
4161
4234
  });
4235
+ if (footer) {
4236
+ clone.style.borderBottomLeftRadius = "0";
4237
+ clone.style.borderBottomRightRadius = "0";
4238
+ }
4162
4239
  owner.append(clone);
4163
4240
  originals.forEach((node, i) => {
4164
4241
  copies[i].scrollTop = node.scrollTop;
@@ -4236,6 +4313,7 @@ function SurfacePanel({ surface }) {
4236
4313
  edgeButtons: [],
4237
4314
  target: surface.element,
4238
4315
  adapter: null,
4316
+ modelAdapter: null,
4239
4317
  scrollTarget: null,
4240
4318
  mediaTarget: null
4241
4319
  });
@@ -4246,7 +4324,7 @@ function SurfacePanel({ surface }) {
4246
4324
  controller = {
4247
4325
  subscribe: () => () => {
4248
4326
  },
4249
- 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 }),
4327
+ 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 }),
4250
4328
  play: async () => {
4251
4329
  try {
4252
4330
  await media.play();
@@ -4263,7 +4341,7 @@ function SurfacePanel({ surface }) {
4263
4341
  if (Number.isFinite(time) && Number.isFinite(media.duration)) media.currentTime = Math.max(0, Math.min(media.duration, time));
4264
4342
  },
4265
4343
  setVolume: (value) => {
4266
- media.volume = value;
4344
+ if (Number.isFinite(value) && volumeCapabilities.get(media)) media.volume = clampVolume(value);
4267
4345
  },
4268
4346
  setMuted: (value) => {
4269
4347
  media.muted = value;
@@ -4280,6 +4358,7 @@ function SurfacePanel({ surface }) {
4280
4358
  s.generation++;
4281
4359
  s.target = surface.element;
4282
4360
  s.adapter = null;
4361
+ s.modelAdapter = null;
4283
4362
  s.dirty = true;
4284
4363
  s.image = null;
4285
4364
  s.mediaBlocked = false;
@@ -4359,7 +4438,7 @@ function SurfacePanel({ surface }) {
4359
4438
  videos.current.clear();
4360
4439
  controllers.current.clear();
4361
4440
  blockedMedia.current = /* @__PURE__ */ new WeakSet();
4362
- surface.element.querySelectorAll("video,audio").forEach((m) => m.pause());
4441
+ if (shouldPauseSurfaceMedia(surface.element.isConnected, host.surfaces.has(surface))) surface.element.querySelectorAll("video,audio").forEach((m) => m.pause());
4363
4442
  };
4364
4443
  }, [surface, camera, gl, host, sceneHost, scratch]);
4365
4444
  useEffect3(
@@ -4477,9 +4556,15 @@ function SurfacePanel({ surface }) {
4477
4556
  s.dirty = true;
4478
4557
  }
4479
4558
  const ctx = canvas.getContext("2d");
4559
+ const appearance = getComputedStyle(surface.element);
4560
+ const token = (name, fallback) => {
4561
+ const value = parseFloat(appearance.getPropertyValue(`--pano-control-${name}`));
4562
+ return Number.isFinite(value) ? value : fallback;
4563
+ };
4564
+ const controlSize = token("size", CONTROL_GROUP_DEFAULTS.size), controlGap = token("gap", CONTROL_GROUP_DEFAULTS.gap), controlPadding = token("padding", CONTROL_GROUP_DEFAULTS.padding);
4480
4565
  const nextHeight = Math.max(
4481
4566
  surface.anchor ? 1 : 200,
4482
- Math.min(BODY_HEIGHT, surface.element.offsetHeight) + (s.buttons.length ? 32 + Math.ceil(s.buttons.length / 5) * 88 : 0)
4567
+ Math.min(BODY_HEIGHT, surface.element.offsetHeight) + (s.buttons.length ? controlGroupLayout(s.buttons.length, WIDTH, controlSize, controlGap, controlPadding).height : 0)
4483
4568
  );
4484
4569
  if (canvas.height !== nextHeight * RASTER_SCALE) {
4485
4570
  texture.dispose();
@@ -4516,7 +4601,7 @@ function SurfacePanel({ surface }) {
4516
4601
  });
4517
4602
  owner.setAttribute("aria-hidden", "true");
4518
4603
  surface.element.closest("[data-pano-viewer]")?.append(owner);
4519
- capture(surface.element, owner).then((image) => {
4604
+ capture(surface.element, owner, s.buttons.length > 0).then((image) => {
4520
4605
  if (!s.disposed && generation === s.generation && revision === s.revision) {
4521
4606
  s.image = image;
4522
4607
  s.failed = false;
@@ -4532,7 +4617,6 @@ function SurfacePanel({ surface }) {
4532
4617
  s.busy = false;
4533
4618
  });
4534
4619
  }
4535
- const appearance = getComputedStyle(surface.element);
4536
4620
  const radius = Math.max(0, parseFloat(appearance.borderTopLeftRadius) || 0);
4537
4621
  ctx.clearRect(0, 0, WIDTH, s.height);
4538
4622
  ctx.save();
@@ -4681,31 +4765,55 @@ function SurfacePanel({ surface }) {
4681
4765
  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)), () => {
4682
4766
  s.dirty = true;
4683
4767
  }) : [];
4684
- const adapter = s.adapter;
4768
+ const adapter = resolveSurfaceModelAdapter(surface.element, host.controls, s.adapter, s.modelAdapter);
4769
+ s.modelAdapter = adapter;
4685
4770
  if (adapter && !explicitTargets.has(adapter)) for (const action of modelActions(adapter)) {
4686
4771
  if (host.input.canDrag && adapter.rotateBy && ["left", "right", "up", "down"].includes(action.id) || host.input.hasStick && adapter.zoomBy && ["in", "out"].includes(action.id)) continue;
4687
4772
  add(action.id, action.label, action.update, action.activate, action.enabled);
4688
4773
  }
4689
- if (media instanceof HTMLAudioElement && !adapter && !explicitTargets.has(mediaControllers.get(media) ?? media)) {
4774
+ if (media instanceof HTMLAudioElement && allowsAutomaticMediaControls(media) && !adapter && !explicitTargets.has(mediaControllers.get(media) ?? media)) {
4690
4775
  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);
4691
4776
  }
4692
- s.buttons = surface.anchor ? [] : buttons;
4693
4777
  if (surface.anchor) buttons.length = 0;
4778
+ const layout = controlGroupLayout(buttons.length, WIDTH, controlSize, controlGap, controlPadding);
4779
+ const bodyHeight = Math.min(BODY_HEIGHT, surface.element.offsetHeight);
4780
+ buttons.forEach((button, i) => Object.assign(button, layout.buttons[i], { y: bodyHeight + layout.buttons[i].y }));
4781
+ if (s.buttons.length !== buttons.length) s.dirty = true;
4782
+ s.buttons = buttons;
4783
+ if (buttons.length) {
4784
+ ctx.fillStyle = "#161b22";
4785
+ ctx.fillRect(0, bodyHeight, WIDTH, layout.height);
4786
+ }
4694
4787
  for (const b of buttons) {
4695
- ctx.fillStyle = b.action.enabled?.() === false ? "#333a43" : appearance.getPropertyValue("--pano-control-background").trim() || "#39424e";
4696
- ctx.fillRect(b.x, b.y, b.w, b.h);
4697
- ctx.fillStyle = "#fff";
4788
+ ctx.save();
4789
+ ctx.globalAlpha = b.action.enabled?.() === false ? 0.4 : 1;
4790
+ ctx.fillStyle = appearance.getPropertyValue("--pano-control-background").trim() || "#39424e";
4791
+ ctx.beginPath();
4792
+ ctx.roundRect(b.x, b.y, b.w, b.h, token("radius", b.w / 2));
4793
+ ctx.fill();
4794
+ const border = /^(\d+(?:\.\d+)?)px\s+solid\s+(.+)$/.exec(appearance.getPropertyValue("--pano-control-border").trim() || "1px solid #68778a");
4795
+ if (border) {
4796
+ ctx.strokeStyle = border[2];
4797
+ ctx.lineWidth = Number(border[1]);
4798
+ ctx.stroke();
4799
+ }
4800
+ if (host.action?.id === b.action.id) {
4801
+ ctx.strokeStyle = appearance.getPropertyValue("--pano-control-focus").trim() || "#ffb300";
4802
+ ctx.lineWidth = 3;
4803
+ ctx.stroke();
4804
+ }
4698
4805
  const id = String(b.action.id).replace("scroll-", "");
4699
4806
  const icon = id === "play" && media && !media.paused ? "pause" : id === "mute" && media?.muted ? "unmute" : id;
4700
4807
  ctx.save();
4701
- ctx.translate(b.x + b.w / 2 - 16, b.y + b.h / 2 - 16);
4702
- ctx.scale(32 / 24, 32 / 24);
4703
- ctx.strokeStyle = "#fff";
4808
+ ctx.translate(b.x + b.w / 2 - 12, b.y + b.h / 2 - 12);
4809
+ ctx.scale(1, 1);
4810
+ ctx.strokeStyle = appearance.getPropertyValue("--pano-control-color").trim() || "#baf5ff";
4704
4811
  ctx.lineWidth = 2;
4705
4812
  ctx.lineCap = "round";
4706
4813
  ctx.lineJoin = "round";
4707
4814
  ctx.stroke(new Path2D(CONTROL_PATHS[icon] ?? CONTROL_PATHS.reset));
4708
4815
  ctx.restore();
4816
+ ctx.restore();
4709
4817
  }
4710
4818
  for (const button of s.edgeButtons) {
4711
4819
  const focused = host.action?.id === button.action.id || [...contacts.current.values()].some((contact) => contact.action?.id === button.action.id);
@@ -9683,6 +9791,7 @@ function VolumeControl({
9683
9791
  const mutedOrSilent = muted || volume === 0;
9684
9792
  useEffect15(() => {
9685
9793
  onOpenChange(open);
9794
+ return () => onOpenChange(false);
9686
9795
  }, [onOpenChange, open]);
9687
9796
  useEffect15(() => {
9688
9797
  if (!open || !volumeAdjustable || hasFinePointerHover()) {
@@ -10130,7 +10239,7 @@ function PanoVideoControlsHud({
10130
10239
  }
10131
10240
  ),
10132
10241
  hideTime ? null : /* @__PURE__ */ jsx12("span", { className: "pano-video-time", children: formatMediaTime(duration) }),
10133
- /* @__PURE__ */ jsx12(
10242
+ snapshot.hasAudio === false ? null : /* @__PURE__ */ jsx12(
10134
10243
  VolumeControl,
10135
10244
  {
10136
10245
  appearance: resolved,
@@ -15168,6 +15277,32 @@ function suppressFollowingClick(target) {
15168
15277
  setTimeout(cleanup, CLICK_SUPPRESS_MS);
15169
15278
  }
15170
15279
 
15280
+ // src/hotspot/tooltip-layout.ts
15281
+ function tooltipPadding(value) {
15282
+ const values = String(value).trim().split(/\s+/).map((value2) => Math.max(0, parseFloat(value2) || 0));
15283
+ const [top = 0, right2 = top, bottom = top, left = right2] = values;
15284
+ return [top, right2, bottom, left];
15285
+ }
15286
+ function tooltipBaseline(top, lineHeight, ascent, descent) {
15287
+ return top + (lineHeight - ascent - descent) / 2 + ascent;
15288
+ }
15289
+ function tooltipLines(text, maxWidth, measure) {
15290
+ const lines = [];
15291
+ if (!text) return lines;
15292
+ for (const paragraph of text.split("\n")) {
15293
+ let line = "";
15294
+ for (const char of paragraph) {
15295
+ if (line && measure(line + char) > maxWidth) {
15296
+ lines.push(line);
15297
+ line = "";
15298
+ }
15299
+ line += char;
15300
+ }
15301
+ lines.push(line);
15302
+ }
15303
+ return lines;
15304
+ }
15305
+
15171
15306
  // src/hotspot/tooltip-transform.ts
15172
15307
  import {
15173
15308
  MathUtils as MathUtils8,
@@ -15273,25 +15408,17 @@ function SpatialHotspotTooltip({
15273
15408
  }, [content.image]);
15274
15409
  const raster = useMemo21(() => {
15275
15410
  const fontSize = Math.max(1, number(appearance.fontSize, 12));
15276
- const padding = Math.max(0, number(appearance.padding, 8));
15411
+ const [pt, pr, pb, pl] = tooltipPadding(appearance.padding);
15412
+ const padding = Math.max(pl, pr);
15413
+ const letterSpacing = appearance.letterSpacing === "normal" ? 0 : number(appearance.letterSpacing ?? 0, 0);
15277
15414
  const leading = appearance.lineHeight;
15278
15415
  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);
15279
15416
  const canvas = document.createElement("canvas");
15280
15417
  const ctx = canvas.getContext("2d");
15281
15418
  const font = `${fontSize}px ${appearance.fontFamily}`;
15282
15419
  ctx.font = font;
15283
- const lines = [];
15284
- for (const paragraph of (content.text ?? "").split("\n")) {
15285
- let line = "";
15286
- for (const char of paragraph) {
15287
- if (line && ctx.measureText(line + char).width > 220 - padding * 2) {
15288
- lines.push(line);
15289
- line = "";
15290
- }
15291
- line += char;
15292
- }
15293
- if (line || content.text) lines.push(line);
15294
- }
15420
+ 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;
15421
+ const lines = tooltipLines(content.text ?? "", 220 - pl - pr, measure);
15295
15422
  const imageWidth = image ? Math.min(
15296
15423
  220 - padding * 2,
15297
15424
  120 * image.naturalWidth / image.naturalHeight
@@ -15300,12 +15427,12 @@ function SpatialHotspotTooltip({
15300
15427
  const width = Math.ceil(
15301
15428
  Math.max(
15302
15429
  imageWidth,
15303
- ...lines.map((line) => ctx.measureText(line).width),
15430
+ ...lines.map(measure),
15304
15431
  1
15305
- ) + padding * 2
15432
+ ) + pl + pr
15306
15433
  );
15307
15434
  const height = Math.ceil(
15308
- padding * 2 + imageHeight + (image && lines.length ? padding : 0) + lines.length * lineHeight
15435
+ pt + pb + imageHeight + (image && lines.length ? padding : 0) + lines.length * lineHeight
15309
15436
  );
15310
15437
  canvas.width = width * 2;
15311
15438
  canvas.height = height * 2;
@@ -15328,17 +15455,26 @@ function SpatialHotspotTooltip({
15328
15455
  }
15329
15456
  ctx.save();
15330
15457
  ctx.clip();
15331
- let top = padding;
15458
+ let top = pt;
15332
15459
  if (image) {
15333
- ctx.drawImage(image, padding, top, imageWidth, imageHeight);
15460
+ ctx.drawImage(image, pl, top, imageWidth, imageHeight);
15334
15461
  top += imageHeight + padding;
15335
15462
  }
15336
15463
  ctx.font = font;
15337
15464
  ctx.fillStyle = appearance.color;
15338
- ctx.textBaseline = "top";
15339
- lines.forEach(
15340
- (line, i) => ctx.fillText(line, padding, top + i * lineHeight)
15341
- );
15465
+ ctx.textBaseline = "alphabetic";
15466
+ lines.forEach((line, i) => {
15467
+ const metrics = ctx.measureText(line || "Mg");
15468
+ const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
15469
+ const descent = metrics.actualBoundingBoxDescent ?? fontSize * 0.2;
15470
+ let x = pl;
15471
+ const y = tooltipBaseline(top + i * lineHeight, lineHeight, ascent, descent);
15472
+ if (!letterSpacing) ctx.fillText(line, x, y);
15473
+ else for (const char of line) {
15474
+ ctx.fillText(char, x, y);
15475
+ x += ctx.measureText(char).width + letterSpacing;
15476
+ }
15477
+ });
15342
15478
  ctx.restore();
15343
15479
  const texture = new CanvasTexture2(canvas);
15344
15480
  texture.colorSpace = SRGBColorSpace4;
@@ -19450,7 +19586,7 @@ function BackgroundAudio({
19450
19586
  setInternalPlaying((prev) => !prev);
19451
19587
  },
19452
19588
  setVolume: (vol) => {
19453
- if (isControlledRef.current) {
19589
+ if (isControlledRef.current || !Number.isFinite(vol)) {
19454
19590
  return;
19455
19591
  }
19456
19592
  const next = clampAudioVolume(vol);
@@ -20409,7 +20545,7 @@ import {
20409
20545
  useContext as useContext35,
20410
20546
  useEffect as useEffect46,
20411
20547
  useImperativeHandle as useImperativeHandle5,
20412
- useMemo as useMemo32,
20548
+ useMemo as useMemo33,
20413
20549
  useRef as useRef47,
20414
20550
  useState as useState28,
20415
20551
  useSyncExternalStore as useSyncExternalStore16
@@ -20806,9 +20942,23 @@ function subscribeWebVRPresentationExit(element, onExit) {
20806
20942
  });
20807
20943
  }
20808
20944
 
20945
+ // src/webvr/reticle-geometry.ts
20946
+ import { RingGeometry } from "three";
20947
+ function createReticleRing(inner, outer) {
20948
+ const geometry = new RingGeometry(inner, outer, 96, 1, Math.PI / 2, -Math.PI * 2);
20949
+ const source = geometry.index;
20950
+ const indices = [];
20951
+ for (let segment = 0; segment < 96; segment++) {
20952
+ if (segment % 4 >= 2) continue;
20953
+ for (let vertex = 0; vertex < 6; vertex++) indices.push(source.getX(segment * 6 + vertex));
20954
+ }
20955
+ geometry.setIndex(indices);
20956
+ return geometry;
20957
+ }
20958
+
20809
20959
  // src/webvr/reticle.tsx
20810
20960
  import { useFrame as useFrame24, useThree as useThree29 } from "@react-three/fiber";
20811
- import { useContext as useContext33, useEffect as useEffect42, useRef as useRef44 } from "react";
20961
+ import { useContext as useContext33, useEffect as useEffect42, useMemo as useMemo31, useRef as useRef44 } from "react";
20812
20962
  import {
20813
20963
  DoubleSide as DoubleSide6,
20814
20964
  Raycaster as Raycaster5,
@@ -20822,10 +20972,9 @@ var RETICLE_SCALE = 1;
20822
20972
  var MOBILE_VR_RETICLE_SCALE = 3;
20823
20973
  var RETICLE_RENDER_ORDER = 1e5;
20824
20974
  var CROSSHAIR_LENGTH = 0.044;
20825
- var CROSSHAIR_THICKNESS = 8e-3;
20975
+ var CROSSHAIR_THICKNESS = 6e-3;
20826
20976
  var RING_INNER_RADIUS = 0.05;
20827
20977
  var RING_OUTER_RADIUS = 0.058;
20828
- var RING_SEGMENTS = 96;
20829
20978
  var RING_INDICES_PER_SEGMENT = 6;
20830
20979
  var GAZE_POINTER_ID = 1000001;
20831
20980
  var DEFAULT_WEBVR_CURSOR_DWELL_MS = 1500;
@@ -20907,6 +21056,15 @@ function WebVRReticle({
20907
21056
  }, []);
20908
21057
  const groupRef = useRef44(null);
20909
21058
  const ringMeshRef = useRef44(null);
21059
+ const ringBackdropRef = useRef44(null);
21060
+ const rings = useMemo31(() => ({
21061
+ outline: createReticleRing(RING_INNER_RADIUS - 3e-3, RING_OUTER_RADIUS + 3e-3),
21062
+ track: createReticleRing(RING_INNER_RADIUS, RING_OUTER_RADIUS),
21063
+ progress: createReticleRing(RING_INNER_RADIUS, RING_OUTER_RADIUS)
21064
+ }), []);
21065
+ useEffect42(() => () => {
21066
+ Object.values(rings).forEach((geometry) => geometry.dispose());
21067
+ }, [rings]);
20910
21068
  const directionRef = useRef44(new Vector324());
20911
21069
  const raycasterRef = useRef44(new Raycaster5());
20912
21070
  const hoveredRef = useRef44(null);
@@ -20933,6 +21091,7 @@ function WebVRReticle({
20933
21091
  }
20934
21092
  dwellRef.current = 0;
20935
21093
  clickedRef.current = false;
21094
+ if (ringBackdropRef.current) ringBackdropRef.current.visible = false;
20936
21095
  if (ringMesh) {
20937
21096
  ringMesh.visible = false;
20938
21097
  ringMesh.geometry.setDrawRange(0, 0);
@@ -20995,8 +21154,9 @@ function WebVRReticle({
20995
21154
  dwellRef.current = 0;
20996
21155
  }
20997
21156
  const progress = spatialOpen ? spatialProgress : target && !clickedRef.current ? resolvedDwellMs > 0 ? Math.min(dwellRef.current / resolvedDwellMs, 1) : 1 : 0;
21157
+ if (ringBackdropRef.current) ringBackdropRef.current.visible = progress > 1e-3;
20998
21158
  if (ringMesh) {
20999
- const segments = Math.max(0, Math.ceil(progress * RING_SEGMENTS));
21159
+ const segments = Math.max(0, Math.ceil(progress * rings.progress.index.count / RING_INDICES_PER_SEGMENT));
21000
21160
  ringMesh.visible = progress > 1e-3;
21001
21161
  ringMesh.geometry.setDrawRange(0, segments * RING_INDICES_PER_SEGMENT);
21002
21162
  }
@@ -21012,6 +21172,14 @@ function WebVRReticle({
21012
21172
  renderOrder: RETICLE_RENDER_ORDER,
21013
21173
  visible,
21014
21174
  children: [
21175
+ [false, true].map((vertical) => /* @__PURE__ */ jsxs23("mesh", { frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER, children: [
21176
+ /* @__PURE__ */ jsx44("boxGeometry", { args: vertical ? [CROSSHAIR_THICKNESS + 6e-3, CROSSHAIR_LENGTH + 6e-3, 1e-3] : [CROSSHAIR_LENGTH + 6e-3, CROSSHAIR_THICKNESS + 6e-3, 1e-3] }),
21177
+ /* @__PURE__ */ jsx44("meshBasicMaterial", { color: "#101419", transparent: true, depthTest: false, depthWrite: false, toneMapped: false })
21178
+ ] }, String(vertical))),
21179
+ /* @__PURE__ */ jsxs23("group", { ref: ringBackdropRef, renderOrder: RETICLE_RENDER_ORDER, visible: false, children: [
21180
+ /* @__PURE__ */ jsx44("mesh", { geometry: rings.outline, frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER, children: /* @__PURE__ */ jsx44("meshBasicMaterial", { color: "#101419", transparent: true, side: DoubleSide6, depthTest: false, depthWrite: false, toneMapped: false }) }),
21181
+ /* @__PURE__ */ jsx44("mesh", { geometry: rings.track, frustumCulled: false, raycast: noopRaycast, renderOrder: RETICLE_RENDER_ORDER + 1, children: /* @__PURE__ */ jsx44("meshBasicMaterial", { color: "#ffffff", transparent: true, opacity: 0.45, side: DoubleSide6, depthTest: false, depthWrite: false, toneMapped: false }) })
21182
+ ] }),
21015
21183
  /* @__PURE__ */ jsxs23(
21016
21184
  "mesh",
21017
21185
  {
@@ -21054,40 +21222,26 @@ function WebVRReticle({
21054
21222
  ]
21055
21223
  }
21056
21224
  ),
21057
- /* @__PURE__ */ jsxs23(
21225
+ /* @__PURE__ */ jsx44(
21058
21226
  "mesh",
21059
21227
  {
21060
21228
  frustumCulled: false,
21061
21229
  raycast: noopRaycast,
21062
21230
  ref: ringMeshRef,
21231
+ geometry: rings.progress,
21063
21232
  renderOrder: RETICLE_RENDER_ORDER + 2,
21064
21233
  visible: false,
21065
- children: [
21066
- /* @__PURE__ */ jsx44(
21067
- "ringGeometry",
21068
- {
21069
- args: [
21070
- RING_INNER_RADIUS,
21071
- RING_OUTER_RADIUS,
21072
- RING_SEGMENTS,
21073
- 1,
21074
- Math.PI / 2,
21075
- Math.PI * -2
21076
- ]
21077
- }
21078
- ),
21079
- /* @__PURE__ */ jsx44(
21080
- "meshBasicMaterial",
21081
- {
21082
- color: "#ffffff",
21083
- depthTest: false,
21084
- depthWrite: false,
21085
- side: DoubleSide6,
21086
- toneMapped: false,
21087
- transparent: true
21088
- }
21089
- )
21090
- ]
21234
+ children: /* @__PURE__ */ jsx44(
21235
+ "meshBasicMaterial",
21236
+ {
21237
+ color: "#ffffff",
21238
+ depthTest: false,
21239
+ depthWrite: false,
21240
+ side: DoubleSide6,
21241
+ toneMapped: false,
21242
+ transparent: true
21243
+ }
21244
+ )
21091
21245
  }
21092
21246
  )
21093
21247
  ]
@@ -21118,7 +21272,7 @@ void main() {
21118
21272
 
21119
21273
  // src/webvr/stereo.tsx
21120
21274
  import { useFrame as useFrame25, useThree as useThree30 } from "@react-three/fiber";
21121
- import { useContext as useContext34, useEffect as useEffect43, useMemo as useMemo31, useRef as useRef45 } from "react";
21275
+ import { useContext as useContext34, useEffect as useEffect43, useMemo as useMemo32, useRef as useRef45 } from "react";
21122
21276
  import {
21123
21277
  Matrix4 as Matrix410,
21124
21278
  Mesh as Mesh9,
@@ -21343,7 +21497,7 @@ function WebVRStereoRenderer({
21343
21497
  const stereo = runtime?.stereoView.stereo ?? fallbackStereoRef.current;
21344
21498
  const stereoRef = useRef45(stereo);
21345
21499
  stereoRef.current = stereo;
21346
- const resources = useMemo31(createDistortionResources, []);
21500
+ const resources = useMemo32(createDistortionResources, []);
21347
21501
  const sizeRef = useRef45(new Vector28());
21348
21502
  const viewportRef = useRef45(new Vector42());
21349
21503
  const scissorRef = useRef45(new Vector42());
@@ -21866,7 +22020,7 @@ var WebVR = forwardRef4(function WebVR2({
21866
22020
  () => host.snapshot.setupOpen,
21867
22021
  () => false
21868
22022
  );
21869
- const resolvedProfile = useMemo32(
22023
+ const resolvedProfile = useMemo33(
21870
22024
  () => resolveWebVRProfile(profile, settings.profileId),
21871
22025
  [profile, settings.profileId]
21872
22026
  );
@@ -22111,7 +22265,7 @@ var WebVR = forwardRef4(function WebVR2({
22111
22265
  clearDetectedScreensize();
22112
22266
  }
22113
22267
  }, [clearDetectedScreensize, host, resolveAutoScreensize]);
22114
- const controller = useMemo32(
22268
+ const controller = useMemo33(
22115
22269
  () => ({
22116
22270
  closeSetup: () => updateWebVRHost(host, { setupOpen: false }),
22117
22271
  enterVR,
@@ -22316,7 +22470,7 @@ import {
22316
22470
  useEffect as useEffect47,
22317
22471
  useId as useId3,
22318
22472
  useLayoutEffect as useLayoutEffect11,
22319
- useMemo as useMemo33,
22473
+ useMemo as useMemo34,
22320
22474
  useRef as useRef48,
22321
22475
  useState as useState29
22322
22476
  } from "react";
@@ -22595,7 +22749,7 @@ function PanoSceneMenu({
22595
22749
  const animRef = useRef48(null);
22596
22750
  const maxOffset = Math.max(0, contentSize - viewportSize);
22597
22751
  offsetRef.current = offset;
22598
- const activeIndex = useMemo33(() => {
22752
+ const activeIndex = useMemo34(() => {
22599
22753
  if (activeId == null) {
22600
22754
  return -1;
22601
22755
  }
@@ -22737,7 +22891,7 @@ function PanoSceneMenu({
22737
22891
  expanded,
22738
22892
  horizontal
22739
22893
  ]);
22740
- const rootStyle = useMemo33(() => {
22894
+ const rootStyle = useMemo34(() => {
22741
22895
  const vars = {
22742
22896
  "--pano-scene-menu-background": resolved.background,
22743
22897
  "--pano-scene-menu-padding": cssLength(resolved.padding),
@@ -23124,7 +23278,7 @@ function PanoSceneMenu({
23124
23278
  }
23125
23279
 
23126
23280
  // src/use-pano-viewer.ts
23127
- import { useMemo as useMemo34, useRef as useRef49 } from "react";
23281
+ import { useMemo as useMemo35, useRef as useRef49 } from "react";
23128
23282
  function createPanoViewerHandle(ref) {
23129
23283
  return {
23130
23284
  getView: () => ref.current?.getView() ?? { yaw: 0, pitch: 0, fov: 75 },
@@ -23237,8 +23391,8 @@ function createPanoViewerHandle(ref) {
23237
23391
  }
23238
23392
  function usePanoViewer() {
23239
23393
  const ref = useRef49(null);
23240
- const handle = useMemo34(() => createPanoViewerHandle(ref), []);
23241
- return useMemo34(() => ({ ref, ...handle }), [handle]);
23394
+ const handle = useMemo35(() => createPanoViewerHandle(ref), []);
23395
+ return useMemo35(() => ({ ref, ...handle }), [handle]);
23242
23396
  }
23243
23397
 
23244
23398
  // src/video/pano-video.tsx
@@ -23248,7 +23402,7 @@ import {
23248
23402
  useContext as useContext36,
23249
23403
  useEffect as useEffect48,
23250
23404
  useLayoutEffect as useLayoutEffect12,
23251
- useMemo as useMemo35,
23405
+ useMemo as useMemo36,
23252
23406
  useRef as useRef50,
23253
23407
  useState as useState30
23254
23408
  } from "react";
@@ -23447,13 +23601,13 @@ function PanoVideo({
23447
23601
  usePanoMediaActivationIntent(autoPlay);
23448
23602
  const gl = useThree32((state) => state.gl);
23449
23603
  const host = useContext36(PanoVideoHostContext);
23450
- const variants = useMemo35(
23604
+ const variants = useMemo36(
23451
23605
  () => resolvePanoVideoVariants({ poster, src, variants: variantsProp }),
23452
23606
  [poster, src, variantsProp]
23453
23607
  );
23454
23608
  const tracks = tracksProp ?? EMPTY_TRACKS;
23455
23609
  const captionsEnabled = captions !== false;
23456
- const captionAppearance = useMemo35(
23610
+ const captionAppearance = useMemo36(
23457
23611
  () => resolvePanoVideoCaptionAppearance(
23458
23612
  typeof captions === "object" ? captions : void 0
23459
23613
  ),
@@ -23518,7 +23672,7 @@ function PanoVideo({
23518
23672
  onEndedRef.current = onEnded;
23519
23673
  onVariantChangeRef.current = onVariantChange;
23520
23674
  onTrackChangeRef.current = onTrackChange;
23521
- const resource = useMemo35(() => {
23675
+ const resource = useMemo36(() => {
23522
23676
  if (typeof document === "undefined") {
23523
23677
  return null;
23524
23678
  }
@@ -23590,6 +23744,7 @@ function PanoVideo({
23590
23744
  ended: playbackState === "ended",
23591
23745
  currentTime: video?.currentTime ?? 0,
23592
23746
  duration: video && Number.isFinite(video.duration) ? video.duration : 0,
23747
+ hasAudio: video ? probeMediaHasAudio(video) : void 0,
23593
23748
  volume: video ? clampVolume(video.volume) : clampVolume(volume),
23594
23749
  volumeAdjustable: mediaCapabilitiesRef.current.volumeAdjustable,
23595
23750
  muted: video?.muted ?? muted,
@@ -23622,7 +23777,7 @@ function PanoVideo({
23622
23777
  captionAppearance
23623
23778
  ]
23624
23779
  );
23625
- const controller = useMemo35(
23780
+ const controller = useMemo36(
23626
23781
  () => ({
23627
23782
  subscribe: (onStoreChange) => {
23628
23783
  const store = storeRef.current;
@@ -23791,7 +23946,7 @@ function PanoVideo({
23791
23946
  emitSnapshot();
23792
23947
  },
23793
23948
  setVolume: (nextVolume) => {
23794
- if (!mediaCapabilitiesRef.current.volumeAdjustable) {
23949
+ if (!Number.isFinite(nextVolume) || !mediaCapabilitiesRef.current.volumeAdjustable) {
23795
23950
  return;
23796
23951
  }
23797
23952
  video.volume = clampVolume(nextVolume);
@@ -23895,7 +24050,7 @@ function PanoVideo({
23895
24050
  const handleVolume = () => {
23896
24051
  emitSnapshot();
23897
24052
  };
23898
- video.addEventListener("timeupdate", handleTimeUpdate);
24053
+ const unsubscribeAudio = subscribeMediaAudio(video, handleTimeUpdate);
23899
24054
  video.addEventListener("durationchange", handleTimeUpdate);
23900
24055
  video.addEventListener("progress", handleTimeUpdate);
23901
24056
  video.addEventListener("play", handlePlay);
@@ -23905,7 +24060,7 @@ function PanoVideo({
23905
24060
  video.addEventListener("ratechange", handleVolume);
23906
24061
  return () => {
23907
24062
  active = false;
23908
- video.removeEventListener("timeupdate", handleTimeUpdate);
24063
+ unsubscribeAudio();
23909
24064
  video.removeEventListener("durationchange", handleTimeUpdate);
23910
24065
  video.removeEventListener("progress", handleTimeUpdate);
23911
24066
  video.removeEventListener("play", handlePlay);
@@ -23990,7 +24145,7 @@ import {
23990
24145
  useEffect as useEffect50,
23991
24146
  useId as useId4,
23992
24147
  useLayoutEffect as useLayoutEffect13,
23993
- useMemo as useMemo36,
24148
+ useMemo as useMemo37,
23994
24149
  useRef as useRef52,
23995
24150
  useState as useState32
23996
24151
  } from "react";
@@ -24243,11 +24398,11 @@ function PanoMap({
24243
24398
  const collapseControl = showCollapseControl ?? map.controls?.collapse === true;
24244
24399
  const collapsed = controlledCollapsed ?? uncontrolledCollapsed;
24245
24400
  const editing = mode === "edit";
24246
- const fit = useMemo36(
24401
+ const fit = useMemo37(
24247
24402
  () => containRect(size.width, size.height, imageSize2.width, imageSize2.height),
24248
24403
  [size, imageSize2]
24249
24404
  );
24250
- const viewport = useMemo36(
24405
+ const viewport = useMemo37(
24251
24406
  () => ({ fit, scale: scale2, panX: pan.x, panY: pan.y }),
24252
24407
  [fit, scale2, pan]
24253
24408
  );
@@ -24877,7 +25032,7 @@ function PanoMap({
24877
25032
  import {
24878
25033
  useEffect as useEffect51,
24879
25034
  useId as useId5,
24880
- useMemo as useMemo37,
25035
+ useMemo as useMemo38,
24881
25036
  useRef as useRef53,
24882
25037
  useState as useState33
24883
25038
  } from "react";
@@ -25044,7 +25199,7 @@ function PanoMapOverlay({
25044
25199
  );
25045
25200
  const width = widthProp ?? basePresentation.width ?? (presentationMode === "dialog" ? 48 : 220);
25046
25201
  const height = heightProp ?? basePresentation.height ?? (presentationMode === "dialog" ? 48 : 160);
25047
- const resolvedMap = useMemo37(() => {
25202
+ const resolvedMap = useMemo38(() => {
25048
25203
  if (interaction === void 0 && maxScale === void 0 && iconSize === void 0) {
25049
25204
  return map;
25050
25205
  }
@@ -25072,7 +25227,7 @@ function PanoMapOverlay({
25072
25227
  if (presentationMode === "dialog") setOpen(false);
25073
25228
  }
25074
25229
  };
25075
- const editorBox = useMemo37(() => {
25230
+ const editorBox = useMemo38(() => {
25076
25231
  const panelWidth = widthProp !== void 0 && Number.isFinite(widthProp) && widthProp > 0 ? widthProp : Math.min(420, Math.max(240, size.width * 0.45));
25077
25232
  const panelHeight = heightProp !== void 0 && Number.isFinite(heightProp) && heightProp > 0 ? heightProp : Math.min(320, Math.max(180, size.height * 0.45));
25078
25233
  return {
@@ -25198,7 +25353,7 @@ function PanoMapOverlay({
25198
25353
  import {
25199
25354
  useContext as useContext38,
25200
25355
  useEffect as useEffect52,
25201
- useMemo as useMemo38,
25356
+ useMemo as useMemo39,
25202
25357
  useRef as useRef54,
25203
25358
  useState as useState34,
25204
25359
  useSyncExternalStore as useSyncExternalStore17
@@ -25426,7 +25581,7 @@ function MenuSurface({
25426
25581
  timer: 0
25427
25582
  });
25428
25583
  const group = useRef54(null);
25429
- const direction = useMemo38(() => new Vector325(), []);
25584
+ const direction = useMemo39(() => new Vector325(), []);
25430
25585
  const [reduced, setReduced] = useState34(false);
25431
25586
  useEffect52(() => {
25432
25587
  const media = matchMedia("(prefers-reduced-motion: reduce)");
@@ -25470,7 +25625,7 @@ function MenuSurface({
25470
25625
  const h = controls.height;
25471
25626
  const radius = bounded(distance, 2, 1.5, 3);
25472
25627
  const menuScale = bounded(scale2, 1, 0.75, 1.25);
25473
- const surfaces = useMemo38(() => {
25628
+ const surfaces = useMemo39(() => {
25474
25629
  const width = open ? W : 1024;
25475
25630
  const result = [];
25476
25631
  for (const { x: left, y: top, w, h: atlasHeight } of sceneMenuAtlasRects(
@@ -25589,7 +25744,7 @@ function MenuSurface({
25589
25744
  },
25590
25745
  [surfaces]
25591
25746
  );
25592
- const blocker = useMemo38(() => {
25747
+ const blocker = useMemo39(() => {
25593
25748
  if (!open || layout !== "arc") return null;
25594
25749
  const geometry = new PlaneGeometry7(1, 1, Math.ceil(degrees / 3), 1);
25595
25750
  const positions = geometry.attributes.position;