@ericchen1990/pano-view 3.7.0 → 3.9.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
@@ -9011,7 +9011,7 @@ var PanoViewer = forwardRef2(
9011
9011
  contextMenuOverlay,
9012
9012
  hotspotAccessibilityControls
9013
9013
  ] }),
9014
- /* @__PURE__ */ jsx13(PanoModalHostContext.Provider, { value: modalHost, children: /* @__PURE__ */ jsx13(PanoOverlayContext.Provider, { value: overlayContext, children: /* @__PURE__ */ jsx13(
9014
+ /* @__PURE__ */ jsx13(PanoEventBusContext.Provider, { value: eventBus, children: /* @__PURE__ */ jsx13(PanoModalHostContext.Provider, { value: modalHost, children: /* @__PURE__ */ jsx13(PanoOverlayContext.Provider, { value: overlayContext, children: /* @__PURE__ */ jsx13(
9015
9015
  "div",
9016
9016
  {
9017
9017
  "data-pano-overlay": "",
@@ -9019,7 +9019,7 @@ var PanoViewer = forwardRef2(
9019
9019
  style: { position: "absolute", inset: 0, zIndex: 30, pointerEvents: "none" },
9020
9020
  children: renderOverlay?.(overlayContext)
9021
9021
  }
9022
- ) }) })
9022
+ ) }) }) })
9023
9023
  ]
9024
9024
  }
9025
9025
  );
@@ -18410,189 +18410,1097 @@ var WebVR = forwardRef4(function WebVR2({
18410
18410
  ] });
18411
18411
  });
18412
18412
 
18413
- // src/use-pano-viewer.ts
18414
- import { useMemo as useMemo27, useRef as useRef40 } from "react";
18415
- function createPanoViewerHandle(ref) {
18416
- return {
18417
- getView: () => ref.current?.getView() ?? { yaw: 0, pitch: 0, fov: 75 },
18418
- setView: (view, options) => {
18419
- ref.current?.setView(view, options);
18420
- },
18421
- reset: () => {
18422
- ref.current?.reset();
18423
- },
18424
- lookTo: (position, options) => ref.current?.lookTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
18425
- moveTo: (position, options) => ref.current?.moveTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
18426
- zoomTo: (fov, options) => ref.current?.zoomTo(fov, options) ?? Promise.resolve({ status: "cancelled" }),
18427
- lookToHotspot: (id, options) => ref.current?.lookToHotspot(id, options) ?? Promise.resolve({ status: "not-found" }),
18428
- activateMedia: () => ref.current?.activateMedia() ?? Promise.resolve(),
18429
- enterFullscreen: () => ref.current?.enterFullscreen() ?? Promise.resolve(),
18430
- exitFullscreen: () => ref.current?.exitFullscreen() ?? Promise.resolve(),
18431
- toggleFullscreen: () => ref.current?.toggleFullscreen() ?? Promise.resolve(),
18432
- isFullscreen: () => ref.current?.isFullscreen() ?? false,
18433
- setScene: (id) => ref.current?.setScene(id) ?? false,
18434
- nextScene: () => ref.current?.nextScene() ?? false,
18435
- previousScene: () => ref.current?.previousScene() ?? false,
18436
- getActiveSceneId: () => ref.current?.getActiveSceneId() ?? null,
18437
- getSceneIds: () => ref.current?.getSceneIds() ?? [],
18438
- isSceneTransitioning: () => ref.current?.isSceneTransitioning() ?? false,
18439
- enterVR: () => ref.current?.enterVR() ?? Promise.resolve(false),
18440
- exitVR: () => ref.current?.exitVR() ?? Promise.resolve(),
18441
- toggleVR: () => ref.current?.toggleVR() ?? Promise.resolve(false),
18442
- isVRAvailable: () => ref.current?.isVRAvailable() ?? false,
18443
- isVREnabled: () => ref.current?.isVREnabled() ?? false,
18444
- getVRMode: () => ref.current?.getVRMode() ?? null,
18445
- requestVRPermission: () => ref.current?.requestVRPermission() ?? Promise.resolve(false),
18446
- getVideo: () => ref.current?.getVideo() ?? null,
18447
- subscribeVideo: (onStoreChange) => ref.current?.subscribeVideo(onStoreChange) ?? (() => {
18448
- }),
18449
- playVideo: () => ref.current?.playVideo() ?? Promise.resolve(),
18450
- pauseVideo: () => {
18451
- ref.current?.pauseVideo();
18452
- },
18453
- toggleVideo: () => {
18454
- ref.current?.toggleVideo();
18455
- },
18456
- seekVideo: (time) => {
18457
- ref.current?.seekVideo(time);
18458
- },
18459
- setVideoVolume: (volume) => {
18460
- ref.current?.setVideoVolume(volume);
18461
- },
18462
- setVideoMuted: (muted) => {
18463
- ref.current?.setVideoMuted(muted);
18464
- },
18465
- toggleVideoMuted: () => {
18466
- ref.current?.toggleVideoMuted();
18467
- },
18468
- getBackgroundAudio: () => ref.current?.getBackgroundAudio() ?? null,
18469
- subscribeBackgroundAudio: (onStoreChange) => ref.current?.subscribeBackgroundAudio(onStoreChange) ?? (() => {
18470
- }),
18471
- playBackgroundAudio: () => ref.current?.playBackgroundAudio() ?? Promise.resolve(),
18472
- pauseBackgroundAudio: () => {
18473
- ref.current?.pauseBackgroundAudio();
18474
- },
18475
- toggleBackgroundAudio: () => {
18476
- ref.current?.toggleBackgroundAudio();
18477
- },
18478
- setBackgroundAudioVolume: (volume) => {
18479
- ref.current?.setBackgroundAudioVolume(volume);
18480
- },
18481
- setBackgroundAudioMuted: (muted) => {
18482
- ref.current?.setBackgroundAudioMuted(muted);
18483
- },
18484
- toggleBackgroundAudioMuted: () => {
18485
- ref.current?.toggleBackgroundAudioMuted();
18486
- },
18487
- startAutoRotate: () => {
18488
- ref.current?.startAutoRotate();
18489
- },
18490
- stopAutoRotate: () => {
18491
- ref.current?.stopAutoRotate();
18492
- }
18493
- };
18494
- }
18495
- function usePanoViewer() {
18496
- const ref = useRef40(null);
18497
- const handle = useMemo27(() => createPanoViewerHandle(ref), []);
18498
- return useMemo27(() => ({ ref, ...handle }), [handle]);
18499
- }
18500
-
18501
- // src/video/pano-video.tsx
18502
- import { useThree as useThree29 } from "@react-three/fiber";
18413
+ // src/pano-scene-menu.tsx
18503
18414
  import {
18504
18415
  useCallback as useCallback11,
18505
- useContext as useContext28,
18506
18416
  useEffect as useEffect41,
18417
+ useId as useId3,
18507
18418
  useLayoutEffect as useLayoutEffect9,
18508
- useMemo as useMemo28,
18509
- useRef as useRef41,
18419
+ useMemo as useMemo27,
18420
+ useRef as useRef40,
18510
18421
  useState as useState25
18511
18422
  } from "react";
18512
- import {
18513
- BackSide as BackSide4,
18514
- LinearFilter as LinearFilter11,
18515
- LinearMipmapLinearFilter as LinearMipmapLinearFilter7,
18516
- MathUtils as MathUtils13,
18517
- RepeatWrapping as RepeatWrapping2,
18518
- SRGBColorSpace as SRGBColorSpace10,
18519
- TextureLoader as TextureLoader8,
18520
- VideoTexture as VideoTexture2
18521
- } from "three";
18522
- import { Fragment as Fragment7, jsx as jsx41, jsxs as jsxs22 } from "react/jsx-runtime";
18523
- var EMPTY_TRACKS = [];
18524
- var EMPTY_RATES = DEFAULT_PANO_VIDEO_PLAYBACK_RATES;
18525
- function createEmptySnapshot() {
18423
+
18424
+ // src/drag-motion.ts
18425
+ var DRAG_INERTIA_DAMPING = 8;
18426
+ var DRAG_FRICTION_STOP = 24;
18427
+ var DRAG_SPRING_STRENGTH = 28;
18428
+ var DRAG_RUBBER_CONSTANT = 0.55;
18429
+ var DRAG_SPRING_SETTLE_DISTANCE = 0.5;
18430
+ var DRAG_SPRING_SETTLE_VELOCITY = 12;
18431
+ function resolveDragMotion(motion) {
18432
+ if (motion === false) {
18433
+ return { inertia: false, bounce: false };
18434
+ }
18526
18435
  return {
18527
- ready: false,
18528
- playing: false,
18529
- blocked: false,
18530
- ended: false,
18531
- currentTime: 0,
18532
- duration: 0,
18533
- volume: 1,
18534
- volumeAdjustable: true,
18535
- muted: true,
18536
- playbackRate: 1,
18537
- variantId: "default",
18538
- variants: [],
18539
- trackId: null,
18540
- tracks: [],
18541
- captionText: "",
18542
- captionAppearance: DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
18543
- captionsEnabled: true,
18544
- playbackRates: EMPTY_RATES,
18545
- playbackState: "paused"
18436
+ inertia: motion?.inertia !== false,
18437
+ bounce: motion?.bounce !== false
18546
18438
  };
18547
18439
  }
18548
- function playbackStateFromVideo(video, blocked) {
18549
- if (blocked) {
18550
- return "blocked";
18440
+ function clampAxis(value, min, max) {
18441
+ if (max < min) return min;
18442
+ return Math.min(max, Math.max(min, value));
18443
+ }
18444
+ function constrainDragPosition(raw, min, max, dimension, bounce) {
18445
+ if (!(Number.isFinite(raw) && Number.isFinite(min) && Number.isFinite(max))) {
18446
+ return 0;
18551
18447
  }
18552
- if (video.ended) {
18553
- return "ended";
18448
+ if (raw >= min && raw <= max) {
18449
+ return raw;
18554
18450
  }
18555
- if (!video.paused) {
18556
- return "playing";
18451
+ if (!bounce) {
18452
+ return clampAxis(raw, min, max);
18557
18453
  }
18558
- return "paused";
18454
+ const size = Math.max(1, Math.abs(dimension) || 1);
18455
+ if (raw < min) {
18456
+ const overshoot2 = min - raw;
18457
+ return min - rubberBandDistance(overshoot2, size);
18458
+ }
18459
+ const overshoot = raw - max;
18460
+ return max + rubberBandDistance(overshoot, size);
18559
18461
  }
18560
- function collectCaptionText(track) {
18561
- if (!track?.activeCues || track.activeCues.length === 0) {
18562
- return "";
18462
+ function rubberBandDistance(overshoot, dimension, constant = DRAG_RUBBER_CONSTANT) {
18463
+ if (!(overshoot > 0)) return 0;
18464
+ const size = Math.max(1, Math.abs(dimension) || 1);
18465
+ return size * overshoot / (size + overshoot * constant);
18466
+ }
18467
+ function velocityFromSamples(samples, now) {
18468
+ if (samples.length < 2) return 0;
18469
+ const windowMs = 100;
18470
+ let oldest = samples[0];
18471
+ const newest = samples[samples.length - 1];
18472
+ const referenceTime = now ?? newest.t;
18473
+ for (let i = samples.length - 2; i >= 0; i -= 1) {
18474
+ const sample = samples[i];
18475
+ if (referenceTime - sample.t > windowMs) break;
18476
+ oldest = sample;
18477
+ }
18478
+ const dt = (newest.t - oldest.t) / 1e3;
18479
+ if (!(dt >= 1e-3)) return 0;
18480
+ return (newest.position - oldest.position) / dt;
18481
+ }
18482
+ function integrateAxis(state, deltaSeconds, options) {
18483
+ const dt = Math.max(0, deltaSeconds);
18484
+ if (!(dt > 0)) {
18485
+ return { ...state, settled: isAxisSettled(state, options) };
18486
+ }
18487
+ const damping = options.damping ?? DRAG_INERTIA_DAMPING;
18488
+ const frictionStop = options.frictionStop ?? DRAG_FRICTION_STOP;
18489
+ const spring = options.spring ?? DRAG_SPRING_STRENGTH;
18490
+ const { min, max } = options;
18491
+ let { position, velocity } = state;
18492
+ const pastMin = position < min;
18493
+ const pastMax = position > max;
18494
+ if (pastMin || pastMax) {
18495
+ if (!options.bounce) {
18496
+ position = clampAxis(position, min, max);
18497
+ return { position, velocity: 0, settled: true };
18498
+ }
18499
+ const edge = pastMin ? min : max;
18500
+ if (pastMin && velocity < 0) velocity = 0;
18501
+ if (pastMax && velocity > 0) velocity = 0;
18502
+ const displacement = edge - position;
18503
+ velocity += displacement * spring * dt;
18504
+ const decay = Math.exp(-damping * dt);
18505
+ velocity *= decay;
18506
+ position += velocity * dt;
18507
+ if (pastMin && position > min) {
18508
+ position = min;
18509
+ velocity = 0;
18510
+ }
18511
+ if (pastMax && position < max) {
18512
+ position = max;
18513
+ velocity = 0;
18514
+ }
18515
+ const settled = isAxisSettled({ position, velocity }, options);
18516
+ if (settled) {
18517
+ return { position: clampAxis(position, min, max), velocity: 0, settled: true };
18518
+ }
18519
+ return { position, velocity, settled: false };
18520
+ }
18521
+ if (!options.inertia || Math.abs(velocity) < frictionStop) {
18522
+ return { position, velocity: 0, settled: true };
18523
+ }
18524
+ position += velocity * dt;
18525
+ velocity *= Math.exp(-damping * dt);
18526
+ if (position < min || position > max) {
18527
+ if (!options.bounce) {
18528
+ position = clampAxis(position, min, max);
18529
+ return { position, velocity: 0, settled: true };
18530
+ }
18531
+ if (Math.abs(velocity) < frictionStop) {
18532
+ velocity = 0;
18533
+ }
18534
+ return { position, velocity, settled: false };
18535
+ }
18536
+ if (Math.abs(velocity) < frictionStop) {
18537
+ return { position, velocity: 0, settled: true };
18538
+ }
18539
+ return { position, velocity, settled: false };
18540
+ }
18541
+ function isAxisSettled(state, options) {
18542
+ const frictionStop = options.frictionStop ?? DRAG_FRICTION_STOP;
18543
+ const inside = state.position >= options.min - DRAG_SPRING_SETTLE_DISTANCE && state.position <= options.max + DRAG_SPRING_SETTLE_DISTANCE;
18544
+ const slow = Math.abs(state.velocity) < (options.frictionStop ?? DRAG_SPRING_SETTLE_VELOCITY);
18545
+ if (!inside) return false;
18546
+ return slow || Math.abs(state.velocity) < frictionStop;
18547
+ }
18548
+ function pushSample(samples, sample, maxCount = 8) {
18549
+ const next = samples.length >= maxCount ? samples.slice(samples.length - maxCount + 1) : samples.slice();
18550
+ next.push(sample);
18551
+ return next;
18552
+ }
18553
+
18554
+ // src/pano-scene-menu.tsx
18555
+ import { jsx as jsx41, jsxs as jsxs22 } from "react/jsx-runtime";
18556
+ var SCENE_MENU_DRAG_THRESHOLD_PX = 6;
18557
+ var DEFAULT_PANO_SCENE_MENU_APPEARANCE = {
18558
+ background: "rgba(16, 16, 16, 0.72)",
18559
+ padding: "8px 10px",
18560
+ itemWidth: 128,
18561
+ itemHeight: 80,
18562
+ gap: 8,
18563
+ borderRadius: 8,
18564
+ selectionColor: "#ffffff",
18565
+ selectionWidth: 2,
18566
+ labelBackground: "rgba(0, 0, 0, 0.55)",
18567
+ labelColor: "#ffffff",
18568
+ labelAlign: "start",
18569
+ fontSize: 12,
18570
+ controlColor: "#ffffff",
18571
+ controlBackground: "rgba(255, 255, 255, 0.12)"
18572
+ };
18573
+ var DEFAULT_INSET = 12;
18574
+ function cssLength(value) {
18575
+ return typeof value === "number" ? `${value}px` : value;
18576
+ }
18577
+ function resolveAppearance2(appearance) {
18578
+ return {
18579
+ ...DEFAULT_PANO_SCENE_MENU_APPEARANCE,
18580
+ ...appearance
18581
+ };
18582
+ }
18583
+ function resolveInset(inset) {
18584
+ if (inset === void 0) {
18585
+ const value = cssLength(DEFAULT_INSET);
18586
+ return { top: value, right: value, bottom: value, left: value };
18563
18587
  }
18564
- const parts = [];
18565
- for (let index = 0; index < track.activeCues.length; index += 1) {
18566
- const cue = track.activeCues[index];
18567
- if (!cue) {
18568
- continue;
18588
+ if (typeof inset === "number" || typeof inset === "string") {
18589
+ const value = cssLength(inset);
18590
+ return { top: value, right: value, bottom: value, left: value };
18591
+ }
18592
+ return {
18593
+ top: cssLength(inset.top ?? DEFAULT_INSET),
18594
+ right: cssLength(inset.right ?? DEFAULT_INSET),
18595
+ bottom: cssLength(inset.bottom ?? DEFAULT_INSET),
18596
+ left: cssLength(inset.left ?? DEFAULT_INSET)
18597
+ };
18598
+ }
18599
+ function itemLabel(item) {
18600
+ return item.title?.trim() || item.id;
18601
+ }
18602
+ function isHorizontal(placement) {
18603
+ return placement === "top" || placement === "bottom";
18604
+ }
18605
+ var CHEVRON_PATHS = {
18606
+ // Keep glyphs optically centered in the 16×16 viewBox.
18607
+ up: "M3 11l5-6 5 6",
18608
+ down: "M3 5l5 6 5-6",
18609
+ left: "M11 3 5 8l6 5",
18610
+ right: "M5 3l6 5-6 5"
18611
+ };
18612
+ function arrowChevron(direction, orientation2) {
18613
+ if (direction === "prev") {
18614
+ return orientation2 === "horizontal" ? "left" : "up";
18615
+ }
18616
+ return orientation2 === "horizontal" ? "right" : "down";
18617
+ }
18618
+ function toggleChevron(placement, expanded) {
18619
+ switch (placement) {
18620
+ case "bottom":
18621
+ return expanded ? "down" : "up";
18622
+ case "top":
18623
+ return expanded ? "up" : "down";
18624
+ case "left":
18625
+ return expanded ? "left" : "right";
18626
+ case "right":
18627
+ return expanded ? "right" : "left";
18628
+ default: {
18629
+ const exhaustive = placement;
18630
+ void exhaustive;
18631
+ return "down";
18569
18632
  }
18570
- const text = cueText(cue);
18571
- if (text) {
18572
- parts.push(text);
18633
+ }
18634
+ }
18635
+ function dockInset(placement, inset) {
18636
+ switch (placement) {
18637
+ case "top":
18638
+ return inset.top;
18639
+ case "bottom":
18640
+ return inset.bottom;
18641
+ case "left":
18642
+ return inset.left;
18643
+ case "right":
18644
+ return inset.right;
18645
+ default: {
18646
+ const exhaustive = placement;
18647
+ void exhaustive;
18648
+ return inset.bottom;
18573
18649
  }
18574
18650
  }
18575
- return parts.join("\n");
18576
18651
  }
18577
- function findTextTrack(video, tracks, trackId) {
18578
- if (!trackId) {
18579
- return null;
18652
+ function Chevron({ arrow }) {
18653
+ return /* @__PURE__ */ jsx41(
18654
+ "svg",
18655
+ {
18656
+ "aria-hidden": true,
18657
+ fill: "none",
18658
+ height: "16",
18659
+ stroke: "currentColor",
18660
+ strokeLinecap: "round",
18661
+ strokeLinejoin: "round",
18662
+ strokeWidth: "1.75",
18663
+ viewBox: "0 0 16 16",
18664
+ width: "16",
18665
+ children: /* @__PURE__ */ jsx41("path", { d: CHEVRON_PATHS[arrow] })
18666
+ }
18667
+ );
18668
+ }
18669
+ function ControlButton({
18670
+ className,
18671
+ disabled,
18672
+ label,
18673
+ onClick,
18674
+ children
18675
+ }) {
18676
+ return /* @__PURE__ */ jsx41(
18677
+ "button",
18678
+ {
18679
+ "aria-label": label,
18680
+ className: cn("pano-scene-menu-control", className),
18681
+ disabled,
18682
+ onClick,
18683
+ onPointerDown: (event) => {
18684
+ event.stopPropagation();
18685
+ },
18686
+ title: label,
18687
+ type: "button",
18688
+ children
18689
+ }
18690
+ );
18691
+ }
18692
+ function horizontalAlignStyle(align, inset, widthValue) {
18693
+ const left = `calc(${inset.left} + var(--pano-safe-left))`;
18694
+ const right = `calc(${inset.right} + var(--pano-safe-right))`;
18695
+ if (!widthValue) {
18696
+ return { left, right, width: void 0, marginInline: void 0 };
18697
+ }
18698
+ switch (align) {
18699
+ case "start":
18700
+ return { left, right: "auto", width: widthValue, marginInline: void 0 };
18701
+ case "end":
18702
+ return { left: "auto", right, width: widthValue, marginInline: void 0 };
18703
+ case "center":
18704
+ return { left, right, width: widthValue, marginInline: "auto" };
18705
+ default: {
18706
+ const exhaustive = align;
18707
+ void exhaustive;
18708
+ return { left, right, width: widthValue, marginInline: "auto" };
18709
+ }
18580
18710
  }
18581
- const configured = tracks.find((track) => panoVideoTrackId(track) === trackId);
18582
- if (!configured) {
18583
- return null;
18711
+ }
18712
+ function verticalAlignStyle(align, inset, heightValue) {
18713
+ const top = `calc(${inset.top} + var(--pano-safe-top))`;
18714
+ const bottom = `calc(${inset.bottom} + var(--pano-safe-bottom))`;
18715
+ if (!heightValue) {
18716
+ return { top, bottom, height: void 0, marginBlock: void 0 };
18584
18717
  }
18585
- const list = video.textTracks;
18586
- for (let index = 0; index < list.length; index += 1) {
18587
- const textTrack = list[index];
18588
- if (!textTrack) {
18589
- continue;
18718
+ switch (align) {
18719
+ case "start":
18720
+ return { top, bottom: "auto", height: heightValue, marginBlock: void 0 };
18721
+ case "end":
18722
+ return { top: "auto", bottom, height: heightValue, marginBlock: void 0 };
18723
+ case "center":
18724
+ return { top, bottom, height: heightValue, marginBlock: "auto" };
18725
+ default: {
18726
+ const exhaustive = align;
18727
+ void exhaustive;
18728
+ return { top, bottom, height: heightValue, marginBlock: "auto" };
18590
18729
  }
18591
- if (textTrack.language === configured.srcLang || textTrack.label === configured.label) {
18592
- return textTrack;
18730
+ }
18731
+ }
18732
+ function placementBoxStyle(placement, inset, width, height, align) {
18733
+ const widthValue = width === void 0 ? void 0 : cssLength(width);
18734
+ const heightValue = height === void 0 ? void 0 : cssLength(height);
18735
+ const maxWidth = `calc(100% - ${inset.left} - ${inset.right} - var(--pano-safe-left) - var(--pano-safe-right))`;
18736
+ const maxHeight = `calc(100% - ${inset.top} - ${inset.bottom} - var(--pano-safe-top) - var(--pano-safe-bottom))`;
18737
+ switch (placement) {
18738
+ case "top":
18739
+ return {
18740
+ ...horizontalAlignStyle(align, inset, widthValue),
18741
+ top: `calc(${inset.top} + var(--pano-safe-top))`,
18742
+ bottom: "auto",
18743
+ maxWidth,
18744
+ height: heightValue,
18745
+ maxHeight
18746
+ };
18747
+ case "bottom":
18748
+ return {
18749
+ ...horizontalAlignStyle(align, inset, widthValue),
18750
+ top: "auto",
18751
+ bottom: `calc(${inset.bottom} + var(--pano-safe-bottom))`,
18752
+ maxWidth,
18753
+ height: heightValue,
18754
+ maxHeight
18755
+ };
18756
+ case "left":
18757
+ return {
18758
+ ...verticalAlignStyle(align, inset, heightValue),
18759
+ right: "auto",
18760
+ left: `calc(${inset.left} + var(--pano-safe-left))`,
18761
+ width: widthValue,
18762
+ maxWidth,
18763
+ maxHeight
18764
+ };
18765
+ case "right":
18766
+ return {
18767
+ ...verticalAlignStyle(align, inset, heightValue),
18768
+ right: `calc(${inset.right} + var(--pano-safe-right))`,
18769
+ left: "auto",
18770
+ width: widthValue,
18771
+ maxWidth,
18772
+ maxHeight
18773
+ };
18774
+ default: {
18775
+ const exhaustive = placement;
18776
+ void exhaustive;
18777
+ return {};
18593
18778
  }
18594
18779
  }
18595
- return list.length > 0 ? list[0] : null;
18780
+ }
18781
+ function PanoSceneMenu({
18782
+ items,
18783
+ activeId = null,
18784
+ onSelect,
18785
+ placement = "bottom",
18786
+ align = "center",
18787
+ inset,
18788
+ width,
18789
+ height,
18790
+ collapsible = false,
18791
+ defaultExpanded = true,
18792
+ expanded: expandedProp,
18793
+ onExpandedChange,
18794
+ showArrows = false,
18795
+ loop = false,
18796
+ dragMotion: dragMotionProp,
18797
+ appearance,
18798
+ className,
18799
+ style,
18800
+ label = "Scenes",
18801
+ previousLabel = "Previous scene",
18802
+ nextLabel = "Next scene",
18803
+ expandLabel = "Show scenes",
18804
+ collapseLabel = "Hide scenes"
18805
+ }) {
18806
+ const listId = useId3();
18807
+ const listRef = useRef40(null);
18808
+ const trackRef = useRef40(null);
18809
+ const itemRefs = useRef40(/* @__PURE__ */ new Map());
18810
+ const resolved = resolveAppearance2(appearance);
18811
+ const resolvedInset = resolveInset(inset);
18812
+ const horizontal = isHorizontal(placement);
18813
+ const customDrag = dragMotionProp !== false;
18814
+ const dragMotion = resolveDragMotion(
18815
+ dragMotionProp === false ? false : dragMotionProp
18816
+ );
18817
+ const isExpandedControlled = expandedProp !== void 0;
18818
+ const [internalExpanded, setInternalExpanded] = useState25(defaultExpanded);
18819
+ const expanded = collapsible ? isExpandedControlled ? expandedProp : internalExpanded : true;
18820
+ const [offset, setOffset] = useState25(0);
18821
+ const [viewportSize, setViewportSize] = useState25(0);
18822
+ const [contentSize, setContentSize] = useState25(0);
18823
+ const offsetRef = useRef40(0);
18824
+ const dragRef = useRef40(null);
18825
+ const animRef = useRef40(null);
18826
+ const maxOffset = Math.max(0, contentSize - viewportSize);
18827
+ offsetRef.current = offset;
18828
+ const activeIndex = useMemo27(() => {
18829
+ if (activeId == null) {
18830
+ return -1;
18831
+ }
18832
+ return items.findIndex((item) => item.id === activeId);
18833
+ }, [activeId, items]);
18834
+ const cancelScrollAnimation = useCallback11(() => {
18835
+ const anim = animRef.current;
18836
+ if (anim?.raf != null) {
18837
+ cancelAnimationFrame(anim.raf);
18838
+ }
18839
+ animRef.current = null;
18840
+ }, []);
18841
+ const clampOffset = useCallback11(
18842
+ (value) => Math.min(maxOffset, Math.max(0, value)),
18843
+ [maxOffset]
18844
+ );
18845
+ const startScrollAnimation = useCallback11(
18846
+ (velocity) => {
18847
+ cancelScrollAnimation();
18848
+ const current = offsetRef.current;
18849
+ const needsSpring = dragMotion.bounce && (current < 0 || current > maxOffset);
18850
+ const nextVelocity = dragMotion.inertia ? velocity : 0;
18851
+ if (!needsSpring && Math.abs(nextVelocity) < 1) {
18852
+ setOffset(clampOffset(current));
18853
+ return;
18854
+ }
18855
+ if (!dragMotion.inertia && !dragMotion.bounce) {
18856
+ setOffset(clampOffset(current));
18857
+ return;
18858
+ }
18859
+ const anim = {
18860
+ raf: null,
18861
+ lastTime: performance.now(),
18862
+ position: current,
18863
+ velocity: nextVelocity
18864
+ };
18865
+ animRef.current = anim;
18866
+ const tick = (now) => {
18867
+ if (animRef.current !== anim) return;
18868
+ const dt = Math.min(0.064, Math.max(0, (now - anim.lastTime) / 1e3));
18869
+ anim.lastTime = now;
18870
+ const next = integrateAxis(
18871
+ { position: anim.position, velocity: anim.velocity },
18872
+ dt,
18873
+ {
18874
+ min: 0,
18875
+ max: maxOffset,
18876
+ inertia: dragMotion.inertia,
18877
+ bounce: dragMotion.bounce
18878
+ }
18879
+ );
18880
+ anim.position = next.position;
18881
+ anim.velocity = next.velocity;
18882
+ setOffset(next.position);
18883
+ if (next.settled) {
18884
+ setOffset(clampOffset(next.position));
18885
+ animRef.current = null;
18886
+ return;
18887
+ }
18888
+ anim.raf = requestAnimationFrame(tick);
18889
+ };
18890
+ anim.raf = requestAnimationFrame(tick);
18891
+ },
18892
+ [
18893
+ cancelScrollAnimation,
18894
+ clampOffset,
18895
+ dragMotion.bounce,
18896
+ dragMotion.inertia,
18897
+ maxOffset
18898
+ ]
18899
+ );
18900
+ useEffect41(() => () => cancelScrollAnimation(), [cancelScrollAnimation]);
18901
+ useLayoutEffect9(() => {
18902
+ if (!customDrag || !expanded) return;
18903
+ const list = listRef.current;
18904
+ const track = trackRef.current;
18905
+ if (!list || !track) return;
18906
+ const measure = () => {
18907
+ const nextViewport = horizontal ? list.clientWidth : list.clientHeight;
18908
+ const nextContent = horizontal ? track.scrollWidth : track.scrollHeight;
18909
+ setViewportSize(
18910
+ (previous) => previous === nextViewport ? previous : nextViewport
18911
+ );
18912
+ setContentSize(
18913
+ (previous) => previous === nextContent ? previous : nextContent
18914
+ );
18915
+ };
18916
+ measure();
18917
+ const observer = new ResizeObserver(measure);
18918
+ observer.observe(list);
18919
+ observer.observe(track);
18920
+ return () => observer.disconnect();
18921
+ }, [customDrag, expanded, horizontal, items]);
18922
+ useEffect41(() => {
18923
+ if (!customDrag) return;
18924
+ if (dragRef.current || animRef.current) return;
18925
+ setOffset((previous) => Math.min(maxOffset, Math.max(0, previous)));
18926
+ }, [customDrag, maxOffset]);
18927
+ useEffect41(() => {
18928
+ if (!expanded || activeId == null) {
18929
+ return;
18930
+ }
18931
+ const node = itemRefs.current.get(activeId);
18932
+ if (!node) return;
18933
+ if (!customDrag) {
18934
+ node.scrollIntoView({
18935
+ behavior: "smooth",
18936
+ block: "nearest",
18937
+ inline: "nearest"
18938
+ });
18939
+ return;
18940
+ }
18941
+ const list = listRef.current;
18942
+ if (!list) return;
18943
+ const listRect = list.getBoundingClientRect();
18944
+ const itemRect = node.getBoundingClientRect();
18945
+ if (horizontal) {
18946
+ const itemStart2 = itemRect.left - listRect.left + offsetRef.current;
18947
+ const itemEnd2 = itemStart2 + itemRect.width;
18948
+ let next2 = offsetRef.current;
18949
+ if (itemStart2 < next2) next2 = itemStart2;
18950
+ else if (itemEnd2 > next2 + listRect.width) next2 = itemEnd2 - listRect.width;
18951
+ cancelScrollAnimation();
18952
+ setOffset(clampOffset(next2));
18953
+ return;
18954
+ }
18955
+ const itemStart = itemRect.top - listRect.top + offsetRef.current;
18956
+ const itemEnd = itemStart + itemRect.height;
18957
+ let next = offsetRef.current;
18958
+ if (itemStart < next) next = itemStart;
18959
+ else if (itemEnd > next + listRect.height) next = itemEnd - listRect.height;
18960
+ cancelScrollAnimation();
18961
+ setOffset(clampOffset(next));
18962
+ }, [
18963
+ activeId,
18964
+ cancelScrollAnimation,
18965
+ clampOffset,
18966
+ customDrag,
18967
+ expanded,
18968
+ horizontal
18969
+ ]);
18970
+ const rootStyle = useMemo27(() => {
18971
+ const vars = {
18972
+ "--pano-scene-menu-background": resolved.background,
18973
+ "--pano-scene-menu-padding": cssLength(resolved.padding),
18974
+ "--pano-scene-menu-item-width": cssLength(resolved.itemWidth),
18975
+ "--pano-scene-menu-item-height": cssLength(resolved.itemHeight),
18976
+ "--pano-scene-menu-gap": cssLength(resolved.gap),
18977
+ "--pano-scene-menu-radius": cssLength(resolved.borderRadius),
18978
+ "--pano-scene-menu-selection-color": resolved.selectionColor,
18979
+ "--pano-scene-menu-selection-width": cssLength(resolved.selectionWidth),
18980
+ "--pano-scene-menu-label-background": resolved.labelBackground,
18981
+ "--pano-scene-menu-label-color": resolved.labelColor,
18982
+ "--pano-scene-menu-label-align": resolved.labelAlign,
18983
+ "--pano-scene-menu-font-size": cssLength(resolved.fontSize),
18984
+ "--pano-scene-menu-control-color": resolved.controlColor,
18985
+ "--pano-scene-menu-control-background": resolved.controlBackground,
18986
+ "--pano-scene-menu-dock-inset": dockInset(placement, resolvedInset)
18987
+ };
18988
+ return {
18989
+ ...vars,
18990
+ ...placementBoxStyle(placement, resolvedInset, width, height, align),
18991
+ ...style
18992
+ };
18993
+ }, [align, height, placement, resolved, resolvedInset, style, width]);
18994
+ const setExpanded = (next) => {
18995
+ if (!collapsible) {
18996
+ return;
18997
+ }
18998
+ if (!isExpandedControlled) {
18999
+ setInternalExpanded(next);
19000
+ }
19001
+ onExpandedChange?.(next);
19002
+ };
19003
+ const selectAt = (index) => {
19004
+ const item = items[index];
19005
+ if (!item) {
19006
+ return;
19007
+ }
19008
+ onSelect?.(item.id);
19009
+ itemRefs.current.get(item.id)?.focus({ preventScroll: true });
19010
+ };
19011
+ const selectRelative = (delta) => {
19012
+ if (items.length === 0) {
19013
+ return;
19014
+ }
19015
+ const current = activeIndex >= 0 ? activeIndex : 0;
19016
+ if (loop) {
19017
+ const next = ((current + delta) % items.length + items.length) % items.length;
19018
+ selectAt(next);
19019
+ return;
19020
+ }
19021
+ selectAt(Math.max(0, Math.min(items.length - 1, current + delta)));
19022
+ };
19023
+ const onKeyDown = (event) => {
19024
+ if (items.length === 0 || !expanded) {
19025
+ return;
19026
+ }
19027
+ const current = activeIndex >= 0 ? activeIndex : Math.max(
19028
+ 0,
19029
+ items.findIndex(
19030
+ (item) => itemRefs.current.get(item.id) === event.target
19031
+ )
19032
+ );
19033
+ const backward = horizontal ? event.key === "ArrowLeft" : event.key === "ArrowUp";
19034
+ const forward = horizontal ? event.key === "ArrowRight" : event.key === "ArrowDown";
19035
+ if (backward) {
19036
+ event.preventDefault();
19037
+ if (loop) {
19038
+ selectAt((current - 1 + items.length) % items.length);
19039
+ } else {
19040
+ selectAt(Math.max(0, current - 1));
19041
+ }
19042
+ return;
19043
+ }
19044
+ if (forward) {
19045
+ event.preventDefault();
19046
+ if (loop) {
19047
+ selectAt((current + 1) % items.length);
19048
+ } else {
19049
+ selectAt(Math.min(items.length - 1, current + 1));
19050
+ }
19051
+ return;
19052
+ }
19053
+ switch (event.key) {
19054
+ case "Home": {
19055
+ event.preventDefault();
19056
+ selectAt(0);
19057
+ break;
19058
+ }
19059
+ case "End": {
19060
+ event.preventDefault();
19061
+ selectAt(items.length - 1);
19062
+ break;
19063
+ }
19064
+ default:
19065
+ break;
19066
+ }
19067
+ };
19068
+ const onWheel = (event) => {
19069
+ const list = listRef.current;
19070
+ if (!list) {
19071
+ return;
19072
+ }
19073
+ if (!customDrag) {
19074
+ if (horizontal) {
19075
+ const canScroll2 = list.scrollWidth > list.clientWidth + 1;
19076
+ if (!canScroll2 || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
19077
+ return;
19078
+ }
19079
+ event.preventDefault();
19080
+ event.stopPropagation();
19081
+ list.scrollLeft += event.deltaY;
19082
+ return;
19083
+ }
19084
+ const canScroll = list.scrollHeight > list.clientHeight + 1;
19085
+ if (!canScroll || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) {
19086
+ return;
19087
+ }
19088
+ event.preventDefault();
19089
+ event.stopPropagation();
19090
+ list.scrollTop += event.deltaX;
19091
+ return;
19092
+ }
19093
+ if (horizontal) {
19094
+ if (maxOffset <= 0 || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
19095
+ return;
19096
+ }
19097
+ event.preventDefault();
19098
+ event.stopPropagation();
19099
+ cancelScrollAnimation();
19100
+ setOffset((previous) => clampOffset(previous + event.deltaY));
19101
+ return;
19102
+ }
19103
+ if (maxOffset <= 0 || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) {
19104
+ return;
19105
+ }
19106
+ event.preventDefault();
19107
+ event.stopPropagation();
19108
+ cancelScrollAnimation();
19109
+ setOffset((previous) => clampOffset(previous + event.deltaX));
19110
+ };
19111
+ const axisPoint = (event) => horizontal ? event.clientX : event.clientY;
19112
+ const endSuppressClickRef = useRef40(false);
19113
+ const beginListDrag = (event, captureTarget) => {
19114
+ if (!customDrag || event.button !== 0) return;
19115
+ cancelScrollAnimation();
19116
+ const now = performance.now();
19117
+ const origin = axisPoint(event);
19118
+ dragRef.current = {
19119
+ pointerId: event.pointerId,
19120
+ origin,
19121
+ startOffset: offsetRef.current,
19122
+ samples: [{ t: now, position: offsetRef.current }],
19123
+ moved: false,
19124
+ suppressClick: false
19125
+ };
19126
+ try {
19127
+ captureTarget.setPointerCapture(event.pointerId);
19128
+ } catch {
19129
+ }
19130
+ };
19131
+ const onListPointerMove = (event) => {
19132
+ const drag = dragRef.current;
19133
+ if (!drag || drag.pointerId !== event.pointerId) return;
19134
+ const point = axisPoint(event);
19135
+ const delta = point - drag.origin;
19136
+ if (!drag.moved && Math.abs(delta) >= SCENE_MENU_DRAG_THRESHOLD_PX) {
19137
+ drag.moved = true;
19138
+ drag.suppressClick = true;
19139
+ }
19140
+ if (!drag.moved) return;
19141
+ const raw = drag.startOffset - delta;
19142
+ const now = performance.now();
19143
+ drag.samples = pushSample(drag.samples, { t: now, position: raw });
19144
+ setOffset(
19145
+ constrainDragPosition(
19146
+ raw,
19147
+ 0,
19148
+ maxOffset,
19149
+ Math.max(1, viewportSize),
19150
+ dragMotion.bounce
19151
+ )
19152
+ );
19153
+ event.preventDefault();
19154
+ };
19155
+ const finishPointerWithClickGuard = (event) => {
19156
+ const drag = dragRef.current;
19157
+ if (!drag || drag.pointerId !== event.pointerId) return;
19158
+ const suppress = drag.suppressClick;
19159
+ dragRef.current = null;
19160
+ try {
19161
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
19162
+ event.currentTarget.releasePointerCapture(event.pointerId);
19163
+ }
19164
+ } catch {
19165
+ }
19166
+ if (suppress) {
19167
+ endSuppressClickRef.current = true;
19168
+ queueMicrotask(() => {
19169
+ endSuppressClickRef.current = false;
19170
+ });
19171
+ }
19172
+ if (!drag.moved) return;
19173
+ if (event.type === "pointercancel") {
19174
+ setOffset(clampOffset(offsetRef.current));
19175
+ return;
19176
+ }
19177
+ startScrollAnimation(velocityFromSamples(drag.samples));
19178
+ };
19179
+ const onItemClick = (event, id) => {
19180
+ if (endSuppressClickRef.current) {
19181
+ event.preventDefault();
19182
+ event.stopPropagation();
19183
+ return;
19184
+ }
19185
+ onSelect?.(id);
19186
+ };
19187
+ const canStep = items.length > 1;
19188
+ const canGoPrevious = canStep && (loop || activeIndex !== 0);
19189
+ const canGoNext = canStep && (loop || activeIndex !== items.length - 1);
19190
+ const orientation2 = horizontal ? "horizontal" : "vertical";
19191
+ const trackStyle = customDrag ? {
19192
+ transform: horizontal ? `translate3d(${-offset}px, 0, 0)` : `translate3d(0, ${-offset}px, 0)`,
19193
+ willChange: "transform"
19194
+ } : void 0;
19195
+ return /* @__PURE__ */ jsxs22(
19196
+ "div",
19197
+ {
19198
+ "aria-label": label,
19199
+ className: cn("pano-scene-menu", className),
19200
+ "data-align": align,
19201
+ "data-collapsible": collapsible ? "true" : void 0,
19202
+ "data-drag-motion": customDrag ? "custom" : "native",
19203
+ "data-expanded": expanded ? "true" : "false",
19204
+ "data-orientation": orientation2,
19205
+ "data-placement": placement,
19206
+ role: "group",
19207
+ style: rootStyle,
19208
+ children: [
19209
+ /* @__PURE__ */ jsxs22(
19210
+ "div",
19211
+ {
19212
+ "aria-hidden": expanded ? void 0 : true,
19213
+ className: "pano-scene-menu-body",
19214
+ ...!expanded ? { inert: true } : null,
19215
+ children: [
19216
+ showArrows ? /* @__PURE__ */ jsx41(
19217
+ ControlButton,
19218
+ {
19219
+ className: "pano-scene-menu-arrow",
19220
+ disabled: !canGoPrevious || !expanded,
19221
+ label: previousLabel,
19222
+ onClick: () => selectRelative(-1),
19223
+ children: /* @__PURE__ */ jsx41(Chevron, { arrow: arrowChevron("prev", orientation2) })
19224
+ }
19225
+ ) : null,
19226
+ /* @__PURE__ */ jsx41(
19227
+ "div",
19228
+ {
19229
+ "aria-label": label,
19230
+ className: "pano-scene-menu-list",
19231
+ id: listId,
19232
+ onKeyDown,
19233
+ onPointerCancel: customDrag ? finishPointerWithClickGuard : void 0,
19234
+ onPointerDown: customDrag ? (event) => beginListDrag(event, event.currentTarget) : void 0,
19235
+ onPointerMove: customDrag ? onListPointerMove : void 0,
19236
+ onPointerUp: customDrag ? finishPointerWithClickGuard : void 0,
19237
+ onWheel,
19238
+ ref: listRef,
19239
+ role: "radiogroup",
19240
+ children: /* @__PURE__ */ jsx41(
19241
+ "div",
19242
+ {
19243
+ className: "pano-scene-menu-track",
19244
+ ref: trackRef,
19245
+ style: trackStyle,
19246
+ children: items.map((item) => {
19247
+ const name = itemLabel(item);
19248
+ const selected = item.id === activeId;
19249
+ return /* @__PURE__ */ jsx41(
19250
+ "button",
19251
+ {
19252
+ "aria-checked": selected,
19253
+ className: "pano-scene-menu-item",
19254
+ "data-active": selected ? "true" : void 0,
19255
+ onClick: (event) => onItemClick(event, item.id),
19256
+ onPointerDown: (event) => {
19257
+ event.stopPropagation();
19258
+ const list = listRef.current;
19259
+ if (customDrag && list) {
19260
+ beginListDrag(event, list);
19261
+ }
19262
+ },
19263
+ ref: (node) => {
19264
+ if (node) {
19265
+ itemRefs.current.set(item.id, node);
19266
+ } else {
19267
+ itemRefs.current.delete(item.id);
19268
+ }
19269
+ },
19270
+ role: "radio",
19271
+ tabIndex: selected || activeId == null && item === items[0] ? 0 : -1,
19272
+ title: name,
19273
+ type: "button",
19274
+ children: /* @__PURE__ */ jsxs22("span", { className: "pano-scene-menu-thumb", children: [
19275
+ /* @__PURE__ */ jsx41(
19276
+ "img",
19277
+ {
19278
+ alt: "",
19279
+ decoding: "async",
19280
+ draggable: false,
19281
+ src: item.thumbnailUrl
19282
+ }
19283
+ ),
19284
+ /* @__PURE__ */ jsx41("span", { className: "pano-scene-menu-label", children: name })
19285
+ ] })
19286
+ },
19287
+ item.id
19288
+ );
19289
+ })
19290
+ }
19291
+ )
19292
+ }
19293
+ ),
19294
+ showArrows ? /* @__PURE__ */ jsx41(
19295
+ ControlButton,
19296
+ {
19297
+ className: "pano-scene-menu-arrow",
19298
+ disabled: !canGoNext || !expanded,
19299
+ label: nextLabel,
19300
+ onClick: () => selectRelative(1),
19301
+ children: /* @__PURE__ */ jsx41(Chevron, { arrow: arrowChevron("next", orientation2) })
19302
+ }
19303
+ ) : null
19304
+ ]
19305
+ }
19306
+ ),
19307
+ collapsible ? /* @__PURE__ */ jsx41(
19308
+ ControlButton,
19309
+ {
19310
+ className: "pano-scene-menu-toggle",
19311
+ label: expanded ? collapseLabel : expandLabel,
19312
+ onClick: () => setExpanded(!expanded),
19313
+ children: /* @__PURE__ */ jsx41(Chevron, { arrow: toggleChevron(placement, expanded) })
19314
+ }
19315
+ ) : null
19316
+ ]
19317
+ }
19318
+ );
19319
+ }
19320
+
19321
+ // src/use-pano-viewer.ts
19322
+ import { useMemo as useMemo28, useRef as useRef41 } from "react";
19323
+ function createPanoViewerHandle(ref) {
19324
+ return {
19325
+ getView: () => ref.current?.getView() ?? { yaw: 0, pitch: 0, fov: 75 },
19326
+ setView: (view, options) => {
19327
+ ref.current?.setView(view, options);
19328
+ },
19329
+ reset: () => {
19330
+ ref.current?.reset();
19331
+ },
19332
+ lookTo: (position, options) => ref.current?.lookTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
19333
+ moveTo: (position, options) => ref.current?.moveTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
19334
+ zoomTo: (fov, options) => ref.current?.zoomTo(fov, options) ?? Promise.resolve({ status: "cancelled" }),
19335
+ lookToHotspot: (id, options) => ref.current?.lookToHotspot(id, options) ?? Promise.resolve({ status: "not-found" }),
19336
+ activateMedia: () => ref.current?.activateMedia() ?? Promise.resolve(),
19337
+ enterFullscreen: () => ref.current?.enterFullscreen() ?? Promise.resolve(),
19338
+ exitFullscreen: () => ref.current?.exitFullscreen() ?? Promise.resolve(),
19339
+ toggleFullscreen: () => ref.current?.toggleFullscreen() ?? Promise.resolve(),
19340
+ isFullscreen: () => ref.current?.isFullscreen() ?? false,
19341
+ setScene: (id) => ref.current?.setScene(id) ?? false,
19342
+ nextScene: () => ref.current?.nextScene() ?? false,
19343
+ previousScene: () => ref.current?.previousScene() ?? false,
19344
+ getActiveSceneId: () => ref.current?.getActiveSceneId() ?? null,
19345
+ getSceneIds: () => ref.current?.getSceneIds() ?? [],
19346
+ isSceneTransitioning: () => ref.current?.isSceneTransitioning() ?? false,
19347
+ enterVR: () => ref.current?.enterVR() ?? Promise.resolve(false),
19348
+ exitVR: () => ref.current?.exitVR() ?? Promise.resolve(),
19349
+ toggleVR: () => ref.current?.toggleVR() ?? Promise.resolve(false),
19350
+ isVRAvailable: () => ref.current?.isVRAvailable() ?? false,
19351
+ isVREnabled: () => ref.current?.isVREnabled() ?? false,
19352
+ getVRMode: () => ref.current?.getVRMode() ?? null,
19353
+ requestVRPermission: () => ref.current?.requestVRPermission() ?? Promise.resolve(false),
19354
+ getVideo: () => ref.current?.getVideo() ?? null,
19355
+ subscribeVideo: (onStoreChange) => ref.current?.subscribeVideo(onStoreChange) ?? (() => {
19356
+ }),
19357
+ playVideo: () => ref.current?.playVideo() ?? Promise.resolve(),
19358
+ pauseVideo: () => {
19359
+ ref.current?.pauseVideo();
19360
+ },
19361
+ toggleVideo: () => {
19362
+ ref.current?.toggleVideo();
19363
+ },
19364
+ seekVideo: (time) => {
19365
+ ref.current?.seekVideo(time);
19366
+ },
19367
+ setVideoVolume: (volume) => {
19368
+ ref.current?.setVideoVolume(volume);
19369
+ },
19370
+ setVideoMuted: (muted) => {
19371
+ ref.current?.setVideoMuted(muted);
19372
+ },
19373
+ toggleVideoMuted: () => {
19374
+ ref.current?.toggleVideoMuted();
19375
+ },
19376
+ getBackgroundAudio: () => ref.current?.getBackgroundAudio() ?? null,
19377
+ subscribeBackgroundAudio: (onStoreChange) => ref.current?.subscribeBackgroundAudio(onStoreChange) ?? (() => {
19378
+ }),
19379
+ playBackgroundAudio: () => ref.current?.playBackgroundAudio() ?? Promise.resolve(),
19380
+ pauseBackgroundAudio: () => {
19381
+ ref.current?.pauseBackgroundAudio();
19382
+ },
19383
+ toggleBackgroundAudio: () => {
19384
+ ref.current?.toggleBackgroundAudio();
19385
+ },
19386
+ setBackgroundAudioVolume: (volume) => {
19387
+ ref.current?.setBackgroundAudioVolume(volume);
19388
+ },
19389
+ setBackgroundAudioMuted: (muted) => {
19390
+ ref.current?.setBackgroundAudioMuted(muted);
19391
+ },
19392
+ toggleBackgroundAudioMuted: () => {
19393
+ ref.current?.toggleBackgroundAudioMuted();
19394
+ },
19395
+ startAutoRotate: () => {
19396
+ ref.current?.startAutoRotate();
19397
+ },
19398
+ stopAutoRotate: () => {
19399
+ ref.current?.stopAutoRotate();
19400
+ }
19401
+ };
19402
+ }
19403
+ function usePanoViewer() {
19404
+ const ref = useRef41(null);
19405
+ const handle = useMemo28(() => createPanoViewerHandle(ref), []);
19406
+ return useMemo28(() => ({ ref, ...handle }), [handle]);
19407
+ }
19408
+
19409
+ // src/video/pano-video.tsx
19410
+ import { useThree as useThree29 } from "@react-three/fiber";
19411
+ import {
19412
+ useCallback as useCallback12,
19413
+ useContext as useContext28,
19414
+ useEffect as useEffect42,
19415
+ useLayoutEffect as useLayoutEffect10,
19416
+ useMemo as useMemo29,
19417
+ useRef as useRef42,
19418
+ useState as useState26
19419
+ } from "react";
19420
+ import {
19421
+ BackSide as BackSide4,
19422
+ LinearFilter as LinearFilter11,
19423
+ LinearMipmapLinearFilter as LinearMipmapLinearFilter7,
19424
+ MathUtils as MathUtils13,
19425
+ RepeatWrapping as RepeatWrapping2,
19426
+ SRGBColorSpace as SRGBColorSpace10,
19427
+ TextureLoader as TextureLoader8,
19428
+ VideoTexture as VideoTexture2
19429
+ } from "three";
19430
+ import { Fragment as Fragment7, jsx as jsx42, jsxs as jsxs23 } from "react/jsx-runtime";
19431
+ var EMPTY_TRACKS = [];
19432
+ var EMPTY_RATES = DEFAULT_PANO_VIDEO_PLAYBACK_RATES;
19433
+ function createEmptySnapshot() {
19434
+ return {
19435
+ ready: false,
19436
+ playing: false,
19437
+ blocked: false,
19438
+ ended: false,
19439
+ currentTime: 0,
19440
+ duration: 0,
19441
+ volume: 1,
19442
+ volumeAdjustable: true,
19443
+ muted: true,
19444
+ playbackRate: 1,
19445
+ variantId: "default",
19446
+ variants: [],
19447
+ trackId: null,
19448
+ tracks: [],
19449
+ captionText: "",
19450
+ captionAppearance: DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
19451
+ captionsEnabled: true,
19452
+ playbackRates: EMPTY_RATES,
19453
+ playbackState: "paused"
19454
+ };
19455
+ }
19456
+ function playbackStateFromVideo(video, blocked) {
19457
+ if (blocked) {
19458
+ return "blocked";
19459
+ }
19460
+ if (video.ended) {
19461
+ return "ended";
19462
+ }
19463
+ if (!video.paused) {
19464
+ return "playing";
19465
+ }
19466
+ return "paused";
19467
+ }
19468
+ function collectCaptionText(track) {
19469
+ if (!track?.activeCues || track.activeCues.length === 0) {
19470
+ return "";
19471
+ }
19472
+ const parts = [];
19473
+ for (let index = 0; index < track.activeCues.length; index += 1) {
19474
+ const cue = track.activeCues[index];
19475
+ if (!cue) {
19476
+ continue;
19477
+ }
19478
+ const text = cueText(cue);
19479
+ if (text) {
19480
+ parts.push(text);
19481
+ }
19482
+ }
19483
+ return parts.join("\n");
19484
+ }
19485
+ function findTextTrack(video, tracks, trackId) {
19486
+ if (!trackId) {
19487
+ return null;
19488
+ }
19489
+ const configured = tracks.find((track) => panoVideoTrackId(track) === trackId);
19490
+ if (!configured) {
19491
+ return null;
19492
+ }
19493
+ const list = video.textTracks;
19494
+ for (let index = 0; index < list.length; index += 1) {
19495
+ const textTrack = list[index];
19496
+ if (!textTrack) {
19497
+ continue;
19498
+ }
19499
+ if (textTrack.language === configured.srcLang || textTrack.label === configured.label) {
19500
+ return textTrack;
19501
+ }
19502
+ }
19503
+ return list.length > 0 ? list[0] : null;
18596
19504
  }
18597
19505
  function applyEquirectangularLayout(texture, anisotropy) {
18598
19506
  texture.colorSpace = SRGBColorSpace10;
@@ -18604,10 +19512,10 @@ function applyEquirectangularLayout(texture, anisotropy) {
18604
19512
  }
18605
19513
  function usePosterTexture2(poster, onError) {
18606
19514
  const gl = useThree29((state) => state.gl);
18607
- const [texture, setTexture] = useState25(null);
18608
- const onErrorRef = useRef41(onError);
19515
+ const [texture, setTexture] = useState26(null);
19516
+ const onErrorRef = useRef42(onError);
18609
19517
  onErrorRef.current = onError;
18610
- useEffect41(() => {
19518
+ useEffect42(() => {
18611
19519
  let active = true;
18612
19520
  setTexture(null);
18613
19521
  if (!poster) {
@@ -18644,7 +19552,7 @@ function usePosterTexture2(poster, onError) {
18644
19552
  active = false;
18645
19553
  };
18646
19554
  }, [gl, poster]);
18647
- useEffect41(
19555
+ useEffect42(
18648
19556
  () => () => {
18649
19557
  texture?.dispose();
18650
19558
  },
@@ -18704,36 +19612,36 @@ function PanoVideo({
18704
19612
  usePanoMediaActivationIntent(autoPlay);
18705
19613
  const gl = useThree29((state) => state.gl);
18706
19614
  const host = useContext28(PanoVideoHostContext);
18707
- const variants = useMemo28(
19615
+ const variants = useMemo29(
18708
19616
  () => resolvePanoVideoVariants({ poster, src, variants: variantsProp }),
18709
19617
  [poster, src, variantsProp]
18710
19618
  );
18711
19619
  const tracks = tracksProp ?? EMPTY_TRACKS;
18712
19620
  const captionsEnabled = captions !== false;
18713
- const captionAppearance = useMemo28(
19621
+ const captionAppearance = useMemo29(
18714
19622
  () => resolvePanoVideoCaptionAppearance(
18715
19623
  typeof captions === "object" ? captions : void 0
18716
19624
  ),
18717
19625
  [captions]
18718
19626
  );
18719
- const [variantId, setVariantId] = useState25(
19627
+ const [variantId, setVariantId] = useState26(
18720
19628
  () => resolvePanoVideoVariantId(variants, defaultVariantId)
18721
19629
  );
18722
- const [trackId, setTrackId] = useState25(
19630
+ const [trackId, setTrackId] = useState26(
18723
19631
  () => captionsEnabled ? defaultPanoVideoTrackId(tracks) : null
18724
19632
  );
18725
- const [ready, setReady] = useState25(false);
18726
- const autoPlayRef = useRef41(autoPlay);
19633
+ const [ready, setReady] = useState26(false);
19634
+ const autoPlayRef = useRef42(autoPlay);
18727
19635
  autoPlayRef.current = autoPlay;
18728
- const blockedRef = useRef41(false);
18729
- const captionAppearanceOverrideRef = useRef41(
19636
+ const blockedRef = useRef42(false);
19637
+ const captionAppearanceOverrideRef = useRef42(
18730
19638
  {}
18731
19639
  );
18732
- const mediaCapabilitiesRef = useRef41({
19640
+ const mediaCapabilitiesRef = useRef42({
18733
19641
  volumeAdjustable: true,
18734
19642
  playbackRates: [...DEFAULT_PANO_VIDEO_PLAYBACK_RATES]
18735
19643
  });
18736
- const storeRef = useRef41(null);
19644
+ const storeRef = useRef42(null);
18737
19645
  if (!storeRef.current) {
18738
19646
  storeRef.current = {
18739
19647
  snapshot: {
@@ -18743,7 +19651,7 @@ function PanoVideo({
18743
19651
  listeners: /* @__PURE__ */ new Set()
18744
19652
  };
18745
19653
  }
18746
- const actionsRef = useRef41({
19654
+ const actionsRef = useRef42({
18747
19655
  play: async () => {
18748
19656
  },
18749
19657
  pause: () => {
@@ -18763,19 +19671,19 @@ function PanoVideo({
18763
19671
  setCaptionAppearance: (_appearance) => {
18764
19672
  }
18765
19673
  });
18766
- const onLoadRef = useRef41(onLoad);
18767
- const onErrorRef = useRef41(onError);
18768
- const onPlaybackStateChangeRef = useRef41(onPlaybackStateChange);
18769
- const onEndedRef = useRef41(onEnded);
18770
- const onVariantChangeRef = useRef41(onVariantChange);
18771
- const onTrackChangeRef = useRef41(onTrackChange);
19674
+ const onLoadRef = useRef42(onLoad);
19675
+ const onErrorRef = useRef42(onError);
19676
+ const onPlaybackStateChangeRef = useRef42(onPlaybackStateChange);
19677
+ const onEndedRef = useRef42(onEnded);
19678
+ const onVariantChangeRef = useRef42(onVariantChange);
19679
+ const onTrackChangeRef = useRef42(onTrackChange);
18772
19680
  onLoadRef.current = onLoad;
18773
19681
  onErrorRef.current = onError;
18774
19682
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
18775
19683
  onEndedRef.current = onEnded;
18776
19684
  onVariantChangeRef.current = onVariantChange;
18777
19685
  onTrackChangeRef.current = onTrackChange;
18778
- const resource = useMemo28(() => {
19686
+ const resource = useMemo29(() => {
18779
19687
  if (typeof document === "undefined") {
18780
19688
  return null;
18781
19689
  }
@@ -18790,7 +19698,7 @@ function PanoVideo({
18790
19698
  texture.generateMipmaps = false;
18791
19699
  return { video, texture };
18792
19700
  }, []);
18793
- useEffect41(
19701
+ useEffect42(
18794
19702
  () => () => {
18795
19703
  if (!resource) {
18796
19704
  return;
@@ -18805,13 +19713,13 @@ function PanoVideo({
18805
19713
  },
18806
19714
  [resource]
18807
19715
  );
18808
- useEffect41(() => {
19716
+ useEffect42(() => {
18809
19717
  if (!resource) {
18810
19718
  return;
18811
19719
  }
18812
19720
  resource.texture.anisotropy = gl.capabilities.getMaxAnisotropy();
18813
19721
  }, [gl, resource]);
18814
- useLayoutEffect9(() => {
19722
+ useLayoutEffect10(() => {
18815
19723
  if (!resource) {
18816
19724
  return;
18817
19725
  }
@@ -18827,7 +19735,7 @@ function PanoVideo({
18827
19735
  activeVariant?.poster ?? poster,
18828
19736
  onError
18829
19737
  );
18830
- const emitSnapshot = useCallback11(
19738
+ const emitSnapshot = useCallback12(
18831
19739
  (patch) => {
18832
19740
  const store = storeRef.current;
18833
19741
  const video = resource?.video;
@@ -18879,7 +19787,7 @@ function PanoVideo({
18879
19787
  captionAppearance
18880
19788
  ]
18881
19789
  );
18882
- const controller = useMemo28(
19790
+ const controller = useMemo29(
18883
19791
  () => ({
18884
19792
  subscribe: (onStoreChange) => {
18885
19793
  const store = storeRef.current;
@@ -18931,7 +19839,7 @@ function PanoVideo({
18931
19839
  }),
18932
19840
  []
18933
19841
  );
18934
- useLayoutEffect9(() => {
19842
+ useLayoutEffect10(() => {
18935
19843
  if (!host || variants.length === 0) {
18936
19844
  return;
18937
19845
  }
@@ -18944,7 +19852,7 @@ function PanoVideo({
18944
19852
  notifyPanoVideoHost(host);
18945
19853
  };
18946
19854
  }, [captionsEnabled, controller, controls, host, variants.length]);
18947
- useLayoutEffect9(() => {
19855
+ useLayoutEffect10(() => {
18948
19856
  if (!resource) {
18949
19857
  return;
18950
19858
  }
@@ -18957,10 +19865,10 @@ function PanoVideo({
18957
19865
  };
18958
19866
  emitSnapshot();
18959
19867
  }, [emitSnapshot, playbackRates, resource]);
18960
- useEffect41(() => {
19868
+ useEffect42(() => {
18961
19869
  emitSnapshot();
18962
19870
  }, [emitSnapshot]);
18963
- useEffect41(() => {
19871
+ useEffect42(() => {
18964
19872
  if (!resource || !activeVariant || !mediaKey) {
18965
19873
  return;
18966
19874
  }
@@ -19017,7 +19925,7 @@ function PanoVideo({
19017
19925
  video.removeEventListener("error", handleError);
19018
19926
  };
19019
19927
  }, [mediaKey, resource]);
19020
- useEffect41(() => {
19928
+ useEffect42(() => {
19021
19929
  if (!resource) {
19022
19930
  return;
19023
19931
  }
@@ -19109,137 +20017,1306 @@ function PanoVideo({
19109
20017
  }
19110
20018
  };
19111
20019
  }, [emitSnapshot, resource]);
19112
- useLayoutEffect9(() => {
20020
+ useLayoutEffect10(() => {
20021
+ if (!resource) {
20022
+ return;
20023
+ }
20024
+ const { video } = resource;
20025
+ video.loop = loop;
20026
+ video.muted = muted;
20027
+ if (mediaCapabilitiesRef.current.volumeAdjustable) {
20028
+ video.volume = clampVolume(volume);
20029
+ }
20030
+ applyInlineVideoPlayback(video, playsInline);
20031
+ video.preload = preload;
20032
+ }, [loop, muted, playsInline, preload, resource, volume]);
20033
+ useEffect42(() => {
20034
+ if (!resource) {
20035
+ return;
20036
+ }
20037
+ const { video } = resource;
20038
+ let active = true;
20039
+ const handleTimeUpdate = () => {
20040
+ if (active) {
20041
+ emitSnapshot();
20042
+ }
20043
+ };
20044
+ const handlePlay = () => {
20045
+ blockedRef.current = false;
20046
+ onPlaybackStateChangeRef.current?.("playing");
20047
+ emitSnapshot();
20048
+ };
20049
+ const handlePause = () => {
20050
+ onPlaybackStateChangeRef.current?.(
20051
+ video.ended ? "ended" : "paused"
20052
+ );
20053
+ emitSnapshot();
20054
+ };
20055
+ const handleEnded = () => {
20056
+ onPlaybackStateChangeRef.current?.("ended");
20057
+ onEndedRef.current?.();
20058
+ emitSnapshot();
20059
+ };
20060
+ const handleVolume = () => {
20061
+ emitSnapshot();
20062
+ };
20063
+ video.addEventListener("timeupdate", handleTimeUpdate);
20064
+ video.addEventListener("durationchange", handleTimeUpdate);
20065
+ video.addEventListener("progress", handleTimeUpdate);
20066
+ video.addEventListener("play", handlePlay);
20067
+ video.addEventListener("pause", handlePause);
20068
+ video.addEventListener("ended", handleEnded);
20069
+ video.addEventListener("volumechange", handleVolume);
20070
+ video.addEventListener("ratechange", handleVolume);
20071
+ return () => {
20072
+ active = false;
20073
+ video.removeEventListener("timeupdate", handleTimeUpdate);
20074
+ video.removeEventListener("durationchange", handleTimeUpdate);
20075
+ video.removeEventListener("progress", handleTimeUpdate);
20076
+ video.removeEventListener("play", handlePlay);
20077
+ video.removeEventListener("pause", handlePause);
20078
+ video.removeEventListener("ended", handleEnded);
20079
+ video.removeEventListener("volumechange", handleVolume);
20080
+ video.removeEventListener("ratechange", handleVolume);
20081
+ };
20082
+ }, [emitSnapshot, resource]);
20083
+ useEffect42(() => {
19113
20084
  if (!resource) {
19114
20085
  return;
19115
20086
  }
19116
- const { video } = resource;
19117
- video.loop = loop;
19118
- video.muted = muted;
19119
- if (mediaCapabilitiesRef.current.volumeAdjustable) {
19120
- video.volume = clampVolume(volume);
19121
- }
19122
- applyInlineVideoPlayback(video, playsInline);
19123
- video.preload = preload;
19124
- }, [loop, muted, playsInline, preload, resource, volume]);
19125
- useEffect41(() => {
19126
- if (!resource) {
20087
+ const { video } = resource;
20088
+ const applyModes = () => {
20089
+ const selected2 = findTextTrack(video, tracks, trackId);
20090
+ const list = video.textTracks;
20091
+ for (let index = 0; index < list.length; index += 1) {
20092
+ const textTrack = list[index];
20093
+ if (!textTrack) {
20094
+ continue;
20095
+ }
20096
+ textTrack.mode = textTrack === selected2 ? "hidden" : "disabled";
20097
+ }
20098
+ emitSnapshot({ captionText: collectCaptionText(selected2) });
20099
+ };
20100
+ applyModes();
20101
+ const selected = findTextTrack(video, tracks, trackId);
20102
+ const handleCueChange = () => {
20103
+ emitSnapshot({
20104
+ captionText: collectCaptionText(
20105
+ findTextTrack(video, tracks, trackId)
20106
+ )
20107
+ });
20108
+ };
20109
+ selected?.addEventListener("cuechange", handleCueChange);
20110
+ video.textTracks.addEventListener("addtrack", applyModes);
20111
+ video.textTracks.addEventListener("change", applyModes);
20112
+ return () => {
20113
+ selected?.removeEventListener("cuechange", handleCueChange);
20114
+ video.textTracks.removeEventListener("addtrack", applyModes);
20115
+ video.textTracks.removeEventListener("change", applyModes);
20116
+ };
20117
+ }, [emitSnapshot, resource, trackId, tracks]);
20118
+ useEffect42(() => {
20119
+ if (!captionsEnabled) {
20120
+ setTrackId(null);
20121
+ return;
20122
+ }
20123
+ setTrackId(defaultPanoVideoTrackId(tracks));
20124
+ }, [captionsEnabled, tracks]);
20125
+ const activeTexture = ready && resource ? resource.texture : posterTexture;
20126
+ if (variants.length === 0) {
20127
+ return null;
20128
+ }
20129
+ return /* @__PURE__ */ jsxs23(Fragment7, { children: [
20130
+ activeTexture ? /* @__PURE__ */ jsxs23(
20131
+ "mesh",
20132
+ {
20133
+ rotation: [0, MathUtils13.degToRad(90 + yawOffset2), 0],
20134
+ visible,
20135
+ children: [
20136
+ /* @__PURE__ */ jsx42("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
20137
+ /* @__PURE__ */ jsx42(
20138
+ PanoBasicMaterial,
20139
+ {
20140
+ map: activeTexture,
20141
+ side: BackSide4,
20142
+ toneMapped: false
20143
+ }
20144
+ )
20145
+ ]
20146
+ }
20147
+ ) : null,
20148
+ captionsEnabled ? /* @__PURE__ */ jsx42(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
20149
+ ] });
20150
+ }
20151
+
20152
+ // src/map/pano-map.tsx
20153
+ import {
20154
+ useCallback as useCallback13,
20155
+ useEffect as useEffect44,
20156
+ useId as useId4,
20157
+ useLayoutEffect as useLayoutEffect11,
20158
+ useMemo as useMemo30,
20159
+ useRef as useRef44,
20160
+ useState as useState28
20161
+ } from "react";
20162
+
20163
+ // src/map/geometry.ts
20164
+ function sectorRotation(heading, yaw) {
20165
+ return heading + yaw;
20166
+ }
20167
+ function horizontalFov(verticalFov, aspect) {
20168
+ if (!(aspect > 0) || !(verticalFov > 0) || verticalFov >= 180) return verticalFov;
20169
+ const verticalRad = verticalFov * Math.PI / 180;
20170
+ const horizontalRad = 2 * Math.atan(Math.tan(verticalRad / 2) * aspect);
20171
+ return horizontalRad * 180 / Math.PI;
20172
+ }
20173
+ function containRect(containerWidth, containerHeight, imageWidth, imageHeight) {
20174
+ if (!(containerWidth > 0) || !(containerHeight > 0) || !(imageWidth > 0) || !(imageHeight > 0)) {
20175
+ return { left: 0, top: 0, width: 0, height: 0 };
20176
+ }
20177
+ const scale = Math.min(
20178
+ containerWidth / imageWidth,
20179
+ containerHeight / imageHeight
20180
+ );
20181
+ const width = imageWidth * scale;
20182
+ const height = imageHeight * scale;
20183
+ return {
20184
+ left: (containerWidth - width) / 2,
20185
+ top: (containerHeight - height) / 2,
20186
+ width,
20187
+ height
20188
+ };
20189
+ }
20190
+ function imagePointToContainer(point, viewport) {
20191
+ const { fit, scale, panX, panY } = viewport;
20192
+ return {
20193
+ x: fit.left + panX + point.x * fit.width * scale,
20194
+ y: fit.top + panY + point.y * fit.height * scale
20195
+ };
20196
+ }
20197
+ function containerPointToImage(point, viewport) {
20198
+ const { fit, scale, panX, panY } = viewport;
20199
+ const width = fit.width * scale;
20200
+ const height = fit.height * scale;
20201
+ if (!(width > 0) || !(height > 0)) return null;
20202
+ return {
20203
+ x: (point.x - fit.left - panX) / width,
20204
+ y: (point.y - fit.top - panY) / height
20205
+ };
20206
+ }
20207
+ function clamp012(value) {
20208
+ return Math.min(1, Math.max(0, value));
20209
+ }
20210
+ function headingFromVector(dx, dy) {
20211
+ if (dx === 0 && dy === 0) return 0;
20212
+ const radians = Math.atan2(dx, -dy);
20213
+ return radians * 180 / Math.PI;
20214
+ }
20215
+ function normalizeDegrees(value) {
20216
+ const wrapped = (value % 360 + 360) % 360;
20217
+ return wrapped > 180 ? wrapped - 360 : wrapped;
20218
+ }
20219
+
20220
+ // src/map/use-map-view.ts
20221
+ import { useContext as useContext29, useEffect as useEffect43, useRef as useRef43, useState as useState27 } from "react";
20222
+ var DEFAULT_VIEW2 = { yaw: 0, pitch: 0, fov: 75 };
20223
+ function useMapView(controlled) {
20224
+ const eventBus = useContext29(PanoEventBusContext);
20225
+ const [view, setView] = useState27(controlled ?? DEFAULT_VIEW2);
20226
+ const controlledRef = useRef43(controlled);
20227
+ controlledRef.current = controlled;
20228
+ useEffect43(() => {
20229
+ if (controlled !== void 0) {
20230
+ setView(controlled);
20231
+ return;
20232
+ }
20233
+ if (!eventBus) return;
20234
+ return eventBus.subscribe("viewchange", (next) => {
20235
+ if (controlledRef.current !== void 0) return;
20236
+ setView(next);
20237
+ });
20238
+ }, [controlled, eventBus]);
20239
+ return controlled ?? view;
20240
+ }
20241
+
20242
+ // src/map/pano-map.tsx
20243
+ import { Fragment as Fragment8, jsx as jsx43, jsxs as jsxs24 } from "react/jsx-runtime";
20244
+ var DEFAULT_ICON_SIZE2 = 28;
20245
+ var DEFAULT_SECTOR_COLOR = "#ffffff";
20246
+ var DEFAULT_SECTOR_OPACITY = 0.55;
20247
+ var MIN_SCALE = 1;
20248
+ var DEFAULT_MAX_SCALE = 8;
20249
+ var ZOOM_STEP = 1.25;
20250
+ function defaultGradientId(prefix) {
20251
+ return `${prefix}-sector-gradient`;
20252
+ }
20253
+ function SectorShape({
20254
+ sector,
20255
+ angle,
20256
+ rotation,
20257
+ gradientId,
20258
+ interactive,
20259
+ onHeadingPointerDown,
20260
+ transformRef
20261
+ }) {
20262
+ const half = angle / 2;
20263
+ const start = (-half - 90) * Math.PI / 180;
20264
+ const end = (half - 90) * Math.PI / 180;
20265
+ const radius = sector.radius;
20266
+ const largeArc = angle > 180 ? 1 : 0;
20267
+ const x1 = Math.cos(start) * radius;
20268
+ const y1 = Math.sin(start) * radius;
20269
+ const x2 = Math.cos(end) * radius;
20270
+ const y2 = Math.sin(end) * radius;
20271
+ const path = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
20272
+ const opacity = sector.opacity ?? DEFAULT_SECTOR_OPACITY;
20273
+ const color = sector.color ?? DEFAULT_SECTOR_COLOR;
20274
+ const gradient = sector.gradient;
20275
+ const strokeWidth = sector.strokeWidth ?? 0;
20276
+ const strokeColor = sector.strokeColor ?? color;
20277
+ const strokeOpacity = sector.strokeOpacity ?? 1;
20278
+ const gradientStrength = gradient && gradient.stops.length > 0 ? Math.max(
20279
+ ...gradient.stops.map((stop) => stop.opacity ?? 1),
20280
+ Number.EPSILON
20281
+ ) : 1;
20282
+ return /* @__PURE__ */ jsxs24("g", { ref: transformRef, transform: `rotate(${rotation})`, style: { pointerEvents: "none" }, children: [
20283
+ gradient ? /* @__PURE__ */ jsx43("defs", { children: gradient.type === "radial" ? /* @__PURE__ */ jsx43("radialGradient", { id: gradientId, cx: "0", cy: "0", r: radius, gradientUnits: "userSpaceOnUse", children: gradient.stops.map((stop, index) => /* @__PURE__ */ jsx43(
20284
+ "stop",
20285
+ {
20286
+ offset: `${stop.offset * 100}%`,
20287
+ stopColor: stop.color,
20288
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
20289
+ },
20290
+ `${stop.offset}:${index}`
20291
+ )) }) : /* @__PURE__ */ jsx43(
20292
+ "linearGradient",
20293
+ {
20294
+ id: gradientId,
20295
+ x1: "0",
20296
+ y1: "0",
20297
+ x2: "0",
20298
+ y2: -radius,
20299
+ gradientUnits: "userSpaceOnUse",
20300
+ children: gradient.stops.map((stop, index) => /* @__PURE__ */ jsx43(
20301
+ "stop",
20302
+ {
20303
+ offset: `${stop.offset * 100}%`,
20304
+ stopColor: stop.color,
20305
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
20306
+ },
20307
+ `${stop.offset}:${index}`
20308
+ ))
20309
+ }
20310
+ ) }) : null,
20311
+ /* @__PURE__ */ jsx43(
20312
+ "path",
20313
+ {
20314
+ d: path,
20315
+ fill: gradient ? `url(#${gradientId})` : color,
20316
+ fillOpacity: opacity,
20317
+ stroke: strokeWidth > 0 ? strokeColor : "none",
20318
+ strokeWidth: strokeWidth > 0 ? strokeWidth : void 0,
20319
+ strokeOpacity: strokeWidth > 0 ? strokeOpacity : void 0,
20320
+ vectorEffect: "non-scaling-stroke",
20321
+ style: {
20322
+ pointerEvents: interactive ? "auto" : "none",
20323
+ cursor: interactive ? "grab" : void 0
20324
+ },
20325
+ onPointerDown: interactive ? onHeadingPointerDown : void 0
20326
+ }
20327
+ )
20328
+ ] });
20329
+ }
20330
+ function DefaultSpotIcon({ active, size }) {
20331
+ return /* @__PURE__ */ jsxs24(
20332
+ "svg",
20333
+ {
20334
+ width: size,
20335
+ height: size,
20336
+ viewBox: "0 0 32 32",
20337
+ "aria-hidden": "true",
20338
+ style: { display: "block" },
20339
+ children: [
20340
+ /* @__PURE__ */ jsx43(
20341
+ "rect",
20342
+ {
20343
+ x: "3",
20344
+ y: "7",
20345
+ width: "26",
20346
+ height: "18",
20347
+ rx: "5",
20348
+ fill: active ? "rgba(15,23,42,0.92)" : "rgba(15,23,42,0.72)",
20349
+ stroke: "rgba(255,255,255,0.92)",
20350
+ strokeWidth: "2"
20351
+ }
20352
+ ),
20353
+ /* @__PURE__ */ jsx43("circle", { cx: "16", cy: "16", r: "5", fill: "none", stroke: "rgba(255,255,255,0.92)", strokeWidth: "2" }),
20354
+ /* @__PURE__ */ jsx43("circle", { cx: "16", cy: "16", r: "2", fill: active ? "#ef4444" : "rgba(255,255,255,0.85)" })
20355
+ ]
20356
+ }
20357
+ );
20358
+ }
20359
+ function PanoMap({
20360
+ map,
20361
+ activeSceneId,
20362
+ onSelectScene,
20363
+ view: controlledView,
20364
+ mode = "view",
20365
+ selectedSpotId = null,
20366
+ onSelectedSpotIdChange,
20367
+ onEditChange,
20368
+ placementSceneId,
20369
+ className,
20370
+ style,
20371
+ showZoomControls,
20372
+ showCollapseControl,
20373
+ collapsed: controlledCollapsed,
20374
+ onCollapsedChange,
20375
+ renderChrome,
20376
+ initialScale
20377
+ }) {
20378
+ const reactId = useId4().replace(/:/g, "");
20379
+ const view = useMapView(controlledView);
20380
+ const rootRef = useRef44(null);
20381
+ const containerRef = useRef44(null);
20382
+ const sectorNodeRef = useRef44(null);
20383
+ const yawRef = useRef44(view.yaw);
20384
+ const [size, setSize] = useState28({ width: 0, height: 0 });
20385
+ const [imageSize2, setImageSize] = useState28({ width: 0, height: 0 });
20386
+ const [scale, setScale] = useState28(1);
20387
+ const [pan, setPan] = useState28({ x: 0, y: 0 });
20388
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState28(false);
20389
+ const dragRef = useRef44(null);
20390
+ const panAnimRef = useRef44(null);
20391
+ const panRef = useRef44(pan);
20392
+ const scaleRef = useRef44(scale);
20393
+ const initialViewKeyRef = useRef44(null);
20394
+ const spots = map.spots;
20395
+ const iconSize = map.iconSize ?? DEFAULT_ICON_SIZE2;
20396
+ const interaction = map.interaction ?? "fixed";
20397
+ const gestureZoom = interaction === "zoom";
20398
+ const dragMotion = resolveDragMotion(map.dragMotion);
20399
+ const maxScale = Math.max(MIN_SCALE, map.maxScale ?? DEFAULT_MAX_SCALE);
20400
+ const resolvedInitialScale = Math.min(
20401
+ maxScale,
20402
+ Math.max(
20403
+ MIN_SCALE,
20404
+ Number.isFinite(initialScale) ? initialScale : MIN_SCALE
20405
+ )
20406
+ );
20407
+ const zoomControls = gestureZoom && (showZoomControls ?? map.controls?.zoom === true);
20408
+ const collapseControl = showCollapseControl ?? map.controls?.collapse === true;
20409
+ const collapsed = controlledCollapsed ?? uncontrolledCollapsed;
20410
+ const editing = mode === "edit";
20411
+ const fit = useMemo30(
20412
+ () => containRect(size.width, size.height, imageSize2.width, imageSize2.height),
20413
+ [size, imageSize2]
20414
+ );
20415
+ const viewport = useMemo30(
20416
+ () => ({ fit, scale, panX: pan.x, panY: pan.y }),
20417
+ [fit, scale, pan]
20418
+ );
20419
+ useLayoutEffect11(() => {
20420
+ if (collapsed) return;
20421
+ const node = containerRef.current;
20422
+ if (!node) return;
20423
+ const measure = () => {
20424
+ const width = node.clientWidth;
20425
+ const height = node.clientHeight;
20426
+ if (!(width > 0) || !(height > 0)) return;
20427
+ setSize(
20428
+ (previous) => previous.width === width && previous.height === height ? previous : { width, height }
20429
+ );
20430
+ };
20431
+ measure();
20432
+ const observer = new ResizeObserver(measure);
20433
+ observer.observe(node);
20434
+ return () => observer.disconnect();
20435
+ }, [collapsed]);
20436
+ useEffect44(() => {
20437
+ yawRef.current = view.yaw;
20438
+ const node = sectorNodeRef.current;
20439
+ if (!node) return;
20440
+ const active = spots.find((spot) => spot.sceneId === activeSceneId);
20441
+ if (!active || !map.sector) return;
20442
+ node.setAttribute(
20443
+ "transform",
20444
+ `rotate(${sectorRotation(active.heading, view.yaw)})`
20445
+ );
20446
+ }, [view.yaw, activeSceneId, spots, map.sector]);
20447
+ const setCollapsed = (next) => {
20448
+ onCollapsedChange?.(next);
20449
+ if (controlledCollapsed === void 0) setUncontrolledCollapsed(next);
20450
+ };
20451
+ panRef.current = pan;
20452
+ scaleRef.current = scale;
20453
+ const panBoundsFor = useCallback13(
20454
+ (nextScale) => {
20455
+ const width = fit.width * nextScale;
20456
+ const height = fit.height * nextScale;
20457
+ if (!(fit.width > 0) || !(fit.height > 0)) {
20458
+ return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
20459
+ }
20460
+ return {
20461
+ minX: Math.min(0, size.width - (fit.left + width)),
20462
+ maxX: Math.max(0, -fit.left),
20463
+ minY: Math.min(0, size.height - (fit.top + height)),
20464
+ maxY: Math.max(0, -fit.top)
20465
+ };
20466
+ },
20467
+ [fit, size]
20468
+ );
20469
+ const clampPan = useCallback13(
20470
+ (nextScale, nextPan) => {
20471
+ const bounds = panBoundsFor(nextScale);
20472
+ return {
20473
+ x: Math.min(bounds.maxX, Math.max(bounds.minX, nextPan.x)),
20474
+ y: Math.min(bounds.maxY, Math.max(bounds.minY, nextPan.y))
20475
+ };
20476
+ },
20477
+ [panBoundsFor]
20478
+ );
20479
+ const cancelPanAnimation = useCallback13(() => {
20480
+ const anim = panAnimRef.current;
20481
+ if (anim?.raf != null) {
20482
+ cancelAnimationFrame(anim.raf);
20483
+ }
20484
+ panAnimRef.current = null;
20485
+ }, []);
20486
+ const startPanAnimation = useCallback13(
20487
+ (velocity) => {
20488
+ cancelPanAnimation();
20489
+ const current = panRef.current;
20490
+ const currentScale = scaleRef.current;
20491
+ const bounds = panBoundsFor(currentScale);
20492
+ const needsSpring = dragMotion.bounce && (current.x < bounds.minX || current.x > bounds.maxX || current.y < bounds.minY || current.y > bounds.maxY);
20493
+ const vx = dragMotion.inertia ? velocity.x : 0;
20494
+ const vy = dragMotion.inertia ? velocity.y : 0;
20495
+ if (!needsSpring && Math.abs(vx) < 1 && Math.abs(vy) < 1) {
20496
+ setPan(clampPan(currentScale, current));
20497
+ return;
20498
+ }
20499
+ if (!dragMotion.inertia && !dragMotion.bounce) {
20500
+ setPan(clampPan(currentScale, current));
20501
+ return;
20502
+ }
20503
+ const anim = {
20504
+ raf: null,
20505
+ lastTime: performance.now(),
20506
+ x: { position: current.x, velocity: vx },
20507
+ y: { position: current.y, velocity: vy }
20508
+ };
20509
+ panAnimRef.current = anim;
20510
+ const tick = (now) => {
20511
+ if (panAnimRef.current !== anim) return;
20512
+ const dt = Math.min(0.064, Math.max(0, (now - anim.lastTime) / 1e3));
20513
+ anim.lastTime = now;
20514
+ const liveBounds = panBoundsFor(scaleRef.current);
20515
+ const nextX = integrateAxis(anim.x, dt, {
20516
+ min: liveBounds.minX,
20517
+ max: liveBounds.maxX,
20518
+ inertia: dragMotion.inertia,
20519
+ bounce: dragMotion.bounce
20520
+ });
20521
+ const nextY = integrateAxis(anim.y, dt, {
20522
+ min: liveBounds.minY,
20523
+ max: liveBounds.maxY,
20524
+ inertia: dragMotion.inertia,
20525
+ bounce: dragMotion.bounce
20526
+ });
20527
+ anim.x = { position: nextX.position, velocity: nextX.velocity };
20528
+ anim.y = { position: nextY.position, velocity: nextY.velocity };
20529
+ setPan({ x: nextX.position, y: nextY.position });
20530
+ if (nextX.settled && nextY.settled) {
20531
+ setPan(
20532
+ clampPan(scaleRef.current, {
20533
+ x: nextX.position,
20534
+ y: nextY.position
20535
+ })
20536
+ );
20537
+ panAnimRef.current = null;
20538
+ return;
20539
+ }
20540
+ anim.raf = requestAnimationFrame(tick);
20541
+ };
20542
+ anim.raf = requestAnimationFrame(tick);
20543
+ },
20544
+ [cancelPanAnimation, clampPan, dragMotion.bounce, dragMotion.inertia, panBoundsFor]
20545
+ );
20546
+ useEffect44(() => () => cancelPanAnimation(), [cancelPanAnimation]);
20547
+ useEffect44(() => {
20548
+ if (collapsed) cancelPanAnimation();
20549
+ }, [cancelPanAnimation, collapsed]);
20550
+ useEffect44(() => {
20551
+ if (!gestureZoom) {
20552
+ initialViewKeyRef.current = null;
20553
+ setScale(1);
20554
+ setPan({ x: 0, y: 0 });
20555
+ return;
20556
+ }
20557
+ setScale((previous) => {
20558
+ const next = Math.min(previous, maxScale);
20559
+ if (next !== previous) {
20560
+ setPan((currentPan) => clampPan(next, currentPan));
20561
+ }
20562
+ return next;
20563
+ });
20564
+ }, [clampPan, gestureZoom, maxScale]);
20565
+ useEffect44(() => {
20566
+ if (!gestureZoom) return;
20567
+ if (!(fit.width > 0) || !(fit.height > 0)) return;
20568
+ if (!(size.width > 0) || !(size.height > 0)) return;
20569
+ const key = `${interaction}:${initialScale ?? MIN_SCALE}`;
20570
+ if (initialViewKeyRef.current === key) return;
20571
+ initialViewKeyRef.current = key;
20572
+ const pivot = { x: size.width / 2, y: size.height / 2 };
20573
+ const nextPan = {
20574
+ x: pivot.x - (pivot.x - fit.left) * resolvedInitialScale - fit.left,
20575
+ y: pivot.y - (pivot.y - fit.top) * resolvedInitialScale - fit.top
20576
+ };
20577
+ setScale(resolvedInitialScale);
20578
+ setPan(clampPan(resolvedInitialScale, nextPan));
20579
+ }, [
20580
+ clampPan,
20581
+ fit.height,
20582
+ fit.left,
20583
+ fit.top,
20584
+ fit.width,
20585
+ gestureZoom,
20586
+ initialScale,
20587
+ interaction,
20588
+ resolvedInitialScale,
20589
+ size.height,
20590
+ size.width
20591
+ ]);
20592
+ const applyZoom = useCallback13(
20593
+ (nextScale, origin) => {
20594
+ if (!gestureZoom) return;
20595
+ cancelPanAnimation();
20596
+ const clamped = Math.min(maxScale, Math.max(MIN_SCALE, nextScale));
20597
+ const pivot = origin ?? { x: size.width / 2, y: size.height / 2 };
20598
+ const ratio = clamped / scale;
20599
+ const nextPan = {
20600
+ x: pivot.x - (pivot.x - (fit.left + pan.x)) * ratio - fit.left,
20601
+ y: pivot.y - (pivot.y - (fit.top + pan.y)) * ratio - fit.top
20602
+ };
20603
+ setScale(clamped);
20604
+ setPan(clampPan(clamped, nextPan));
20605
+ },
20606
+ [
20607
+ cancelPanAnimation,
20608
+ clampPan,
20609
+ fit.left,
20610
+ fit.top,
20611
+ gestureZoom,
20612
+ maxScale,
20613
+ pan.x,
20614
+ pan.y,
20615
+ scale,
20616
+ size.height,
20617
+ size.width
20618
+ ]
20619
+ );
20620
+ useEffect44(() => {
20621
+ const root = rootRef.current;
20622
+ if (!root) return;
20623
+ const onWheel = (event) => {
20624
+ event.preventDefault();
20625
+ event.stopPropagation();
20626
+ if (!gestureZoom || collapsed) return;
20627
+ const viewport2 = containerRef.current;
20628
+ if (!viewport2) return;
20629
+ const rect = viewport2.getBoundingClientRect();
20630
+ const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
20631
+ applyZoom(scale * factor, {
20632
+ x: event.clientX - rect.left,
20633
+ y: event.clientY - rect.top
20634
+ });
20635
+ };
20636
+ root.addEventListener("wheel", onWheel, { passive: false });
20637
+ return () => root.removeEventListener("wheel", onWheel);
20638
+ }, [applyZoom, collapsed, gestureZoom, scale]);
20639
+ const updateSpots = (next, selected) => {
20640
+ onEditChange?.({
20641
+ spots: next,
20642
+ ...selected === void 0 ? {} : { selectedSpotId: selected }
20643
+ });
20644
+ };
20645
+ const localPoint = (event) => {
20646
+ const rect = containerRef.current?.getBoundingClientRect();
20647
+ if (!rect) return null;
20648
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
20649
+ };
20650
+ const onPointerDownSurface = (event) => {
20651
+ if (collapsed) return;
20652
+ if (event.button !== 0) return;
20653
+ const point = localPoint(event);
20654
+ if (!point) return;
20655
+ if (editing && placementSceneId) {
20656
+ const image = containerPointToImage(point, viewport);
20657
+ if (!image) return;
20658
+ const spot = {
20659
+ id: crypto.randomUUID(),
20660
+ sceneId: placementSceneId,
20661
+ x: clamp012(image.x),
20662
+ y: clamp012(image.y),
20663
+ heading: normalizeDegrees(-view.yaw)
20664
+ };
20665
+ updateSpots([...spots, spot], spot.id);
20666
+ onSelectedSpotIdChange?.(spot.id);
20667
+ event.preventDefault();
20668
+ event.stopPropagation();
20669
+ return;
20670
+ }
20671
+ if (gestureZoom && scale > 1) {
20672
+ cancelPanAnimation();
20673
+ const now = performance.now();
20674
+ dragRef.current = {
20675
+ kind: "pan",
20676
+ pointerId: event.pointerId,
20677
+ originX: point.x,
20678
+ originY: point.y,
20679
+ panX: pan.x,
20680
+ panY: pan.y,
20681
+ samplesX: [{ t: now, position: pan.x }],
20682
+ samplesY: [{ t: now, position: pan.y }]
20683
+ };
20684
+ try {
20685
+ event.currentTarget.setPointerCapture(event.pointerId);
20686
+ } catch {
20687
+ }
20688
+ event.preventDefault();
20689
+ event.stopPropagation();
20690
+ }
20691
+ };
20692
+ const onPointerMoveSurface = (event) => {
20693
+ const drag = dragRef.current;
20694
+ if (!drag || drag.pointerId !== event.pointerId) return;
20695
+ const point = localPoint(event);
20696
+ if (!point) return;
20697
+ if (drag.kind === "pan") {
20698
+ const rawX = drag.panX + (point.x - drag.originX);
20699
+ const rawY = drag.panY + (point.y - drag.originY);
20700
+ const bounds = panBoundsFor(scale);
20701
+ const now = performance.now();
20702
+ drag.samplesX = pushSample(drag.samplesX, { t: now, position: rawX });
20703
+ drag.samplesY = pushSample(drag.samplesY, { t: now, position: rawY });
20704
+ setPan({
20705
+ x: constrainDragPosition(
20706
+ rawX,
20707
+ bounds.minX,
20708
+ bounds.maxX,
20709
+ size.width,
20710
+ dragMotion.bounce
20711
+ ),
20712
+ y: constrainDragPosition(
20713
+ rawY,
20714
+ bounds.minY,
20715
+ bounds.maxY,
20716
+ size.height,
20717
+ dragMotion.bounce
20718
+ )
20719
+ });
20720
+ event.preventDefault();
20721
+ event.stopPropagation();
20722
+ return;
20723
+ }
20724
+ if (drag.kind === "spot") {
20725
+ const image = containerPointToImage(
20726
+ { x: point.x - drag.offsetX, y: point.y - drag.offsetY },
20727
+ viewport
20728
+ );
20729
+ if (!image) return;
20730
+ updateSpots(
20731
+ spots.map(
20732
+ (spot2) => spot2.id === drag.spotId ? { ...spot2, x: clamp012(image.x), y: clamp012(image.y) } : spot2
20733
+ )
20734
+ );
20735
+ event.preventDefault();
20736
+ event.stopPropagation();
19127
20737
  return;
19128
20738
  }
19129
- const { video } = resource;
19130
- let active = true;
19131
- const handleTimeUpdate = () => {
19132
- if (active) {
19133
- emitSnapshot();
19134
- }
19135
- };
19136
- const handlePlay = () => {
19137
- blockedRef.current = false;
19138
- onPlaybackStateChangeRef.current?.("playing");
19139
- emitSnapshot();
19140
- };
19141
- const handlePause = () => {
19142
- onPlaybackStateChangeRef.current?.(
19143
- video.ended ? "ended" : "paused"
20739
+ const spot = spots.find((entry) => entry.id === drag.spotId);
20740
+ if (!spot) return;
20741
+ const center = imagePointToContainer(spot, viewport);
20742
+ const heading = normalizeDegrees(
20743
+ headingFromVector(point.x - center.x, point.y - center.y) - view.yaw
20744
+ );
20745
+ updateSpots(
20746
+ spots.map(
20747
+ (entry) => entry.id === drag.spotId ? { ...entry, heading } : entry
20748
+ )
20749
+ );
20750
+ if (sectorNodeRef.current && spot.sceneId === activeSceneId) {
20751
+ sectorNodeRef.current.setAttribute(
20752
+ "transform",
20753
+ `rotate(${sectorRotation(heading, view.yaw)})`
19144
20754
  );
19145
- emitSnapshot();
19146
- };
19147
- const handleEnded = () => {
19148
- onPlaybackStateChangeRef.current?.("ended");
19149
- onEndedRef.current?.();
19150
- emitSnapshot();
19151
- };
19152
- const handleVolume = () => {
19153
- emitSnapshot();
19154
- };
19155
- video.addEventListener("timeupdate", handleTimeUpdate);
19156
- video.addEventListener("durationchange", handleTimeUpdate);
19157
- video.addEventListener("progress", handleTimeUpdate);
19158
- video.addEventListener("play", handlePlay);
19159
- video.addEventListener("pause", handlePause);
19160
- video.addEventListener("ended", handleEnded);
19161
- video.addEventListener("volumechange", handleVolume);
19162
- video.addEventListener("ratechange", handleVolume);
19163
- return () => {
19164
- active = false;
19165
- video.removeEventListener("timeupdate", handleTimeUpdate);
19166
- video.removeEventListener("durationchange", handleTimeUpdate);
19167
- video.removeEventListener("progress", handleTimeUpdate);
19168
- video.removeEventListener("play", handlePlay);
19169
- video.removeEventListener("pause", handlePause);
19170
- video.removeEventListener("ended", handleEnded);
19171
- video.removeEventListener("volumechange", handleVolume);
19172
- video.removeEventListener("ratechange", handleVolume);
19173
- };
19174
- }, [emitSnapshot, resource]);
19175
- useEffect41(() => {
19176
- if (!resource) {
20755
+ }
20756
+ event.preventDefault();
20757
+ event.stopPropagation();
20758
+ };
20759
+ const endDrag = (event) => {
20760
+ const drag = dragRef.current;
20761
+ if (drag?.pointerId !== event.pointerId) return;
20762
+ dragRef.current = null;
20763
+ try {
20764
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
20765
+ event.currentTarget.releasePointerCapture(event.pointerId);
20766
+ }
20767
+ } catch {
20768
+ }
20769
+ if (drag.kind !== "pan") return;
20770
+ if (event.type === "pointercancel") {
20771
+ setPan(clampPan(scale, panRef.current));
19177
20772
  return;
19178
20773
  }
19179
- const { video } = resource;
19180
- const applyModes = () => {
19181
- const selected2 = findTextTrack(video, tracks, trackId);
19182
- const list = video.textTracks;
19183
- for (let index = 0; index < list.length; index += 1) {
19184
- const textTrack = list[index];
19185
- if (!textTrack) {
19186
- continue;
19187
- }
19188
- textTrack.mode = textTrack === selected2 ? "hidden" : "disabled";
20774
+ startPanAnimation({
20775
+ x: velocityFromSamples(drag.samplesX),
20776
+ y: velocityFromSamples(drag.samplesY)
20777
+ });
20778
+ };
20779
+ const sectorAngle = (() => {
20780
+ if (!map.sector) return 0;
20781
+ if (!map.sector.followFov) return map.sector.angle;
20782
+ const aspect = size.width > 0 && size.height > 0 ? size.width / size.height : 1;
20783
+ return horizontalFov(view.fov, aspect);
20784
+ })();
20785
+ const activeSpot = spots.find((spot) => spot.sceneId === activeSceneId);
20786
+ const selectedSpot = spots.find((spot) => spot.id === selectedSpotId);
20787
+ const controls = /* @__PURE__ */ jsxs24("div", { className: "pano-map-controls", style: { pointerEvents: "auto" }, children: [
20788
+ collapseControl ? /* @__PURE__ */ jsx43(
20789
+ "button",
20790
+ {
20791
+ type: "button",
20792
+ className: "pano-map-control",
20793
+ "aria-label": collapsed ? "Expand map" : "Collapse map",
20794
+ "aria-pressed": !collapsed,
20795
+ style: {
20796
+ background: "rgba(15, 23, 42, 0.92)",
20797
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20798
+ color: "#f8fafc"
20799
+ },
20800
+ onClick: () => setCollapsed(!collapsed),
20801
+ children: collapsed ? "\u25A3" : "\u25A2"
19189
20802
  }
19190
- emitSnapshot({ captionText: collectCaptionText(selected2) });
19191
- };
19192
- applyModes();
19193
- const selected = findTextTrack(video, tracks, trackId);
19194
- const handleCueChange = () => {
19195
- emitSnapshot({
19196
- captionText: collectCaptionText(
19197
- findTextTrack(video, tracks, trackId)
20803
+ ) : null,
20804
+ zoomControls && !collapsed ? /* @__PURE__ */ jsxs24(Fragment8, { children: [
20805
+ /* @__PURE__ */ jsx43(
20806
+ "button",
20807
+ {
20808
+ type: "button",
20809
+ className: "pano-map-control",
20810
+ "aria-label": "Zoom in",
20811
+ style: {
20812
+ background: "rgba(15, 23, 42, 0.92)",
20813
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20814
+ color: "#f8fafc"
20815
+ },
20816
+ onClick: () => applyZoom(scale * ZOOM_STEP),
20817
+ children: "+"
20818
+ }
20819
+ ),
20820
+ /* @__PURE__ */ jsx43(
20821
+ "button",
20822
+ {
20823
+ type: "button",
20824
+ className: "pano-map-control",
20825
+ "aria-label": "Zoom out",
20826
+ style: {
20827
+ background: "rgba(15, 23, 42, 0.92)",
20828
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20829
+ color: "#f8fafc"
20830
+ },
20831
+ onClick: () => applyZoom(scale / ZOOM_STEP),
20832
+ children: "\u2212"
20833
+ }
20834
+ )
20835
+ ] }) : null
20836
+ ] });
20837
+ const syncImageSize = (image) => {
20838
+ if (!(image.naturalWidth > 0) || !(image.naturalHeight > 0)) return;
20839
+ setImageSize(
20840
+ (previous) => previous.width === image.naturalWidth && previous.height === image.naturalHeight ? previous : { width: image.naturalWidth, height: image.naturalHeight }
20841
+ );
20842
+ };
20843
+ return /* @__PURE__ */ jsxs24(
20844
+ "div",
20845
+ {
20846
+ ref: rootRef,
20847
+ className: ["pano-map", collapsed ? "pano-map-collapsed" : "", className].filter(Boolean).join(" "),
20848
+ style: {
20849
+ position: "relative",
20850
+ overflow: "hidden",
20851
+ width: "100%",
20852
+ height: "100%",
20853
+ pointerEvents: "auto",
20854
+ touchAction: gestureZoom && !collapsed ? "none" : "auto",
20855
+ ...style
20856
+ },
20857
+ "data-pano-map": "",
20858
+ "data-mode": mode,
20859
+ "data-collapsed": collapsed ? "true" : void 0,
20860
+ children: [
20861
+ renderChrome ? renderChrome(controls) : /* @__PURE__ */ jsx43("div", { className: "pano-map-chrome", children: controls }),
20862
+ /* @__PURE__ */ jsxs24(
20863
+ "div",
20864
+ {
20865
+ ref: containerRef,
20866
+ className: "pano-map-viewport",
20867
+ hidden: collapsed,
20868
+ style: {
20869
+ position: "absolute",
20870
+ inset: 0,
20871
+ overflow: "hidden",
20872
+ visibility: collapsed ? "hidden" : "visible"
20873
+ },
20874
+ onPointerDown: onPointerDownSurface,
20875
+ onPointerMove: onPointerMoveSurface,
20876
+ onPointerUp: endDrag,
20877
+ onPointerCancel: endDrag,
20878
+ children: [
20879
+ fit.width > 0 && fit.height > 0 ? /* @__PURE__ */ jsx43(
20880
+ "div",
20881
+ {
20882
+ className: "pano-map-image-layer",
20883
+ style: {
20884
+ position: "absolute",
20885
+ left: fit.left + pan.x,
20886
+ top: fit.top + pan.y,
20887
+ width: fit.width * scale,
20888
+ height: fit.height * scale,
20889
+ transformOrigin: "0 0"
20890
+ },
20891
+ children: /* @__PURE__ */ jsx43(
20892
+ "img",
20893
+ {
20894
+ src: map.image,
20895
+ alt: "",
20896
+ draggable: false,
20897
+ ref: (node) => {
20898
+ if (node) syncImageSize(node);
20899
+ },
20900
+ onLoad: (event) => {
20901
+ syncImageSize(event.currentTarget);
20902
+ },
20903
+ style: {
20904
+ width: "100%",
20905
+ height: "100%",
20906
+ display: "block",
20907
+ userSelect: "none",
20908
+ pointerEvents: "none"
20909
+ }
20910
+ }
20911
+ )
20912
+ }
20913
+ ) : /* @__PURE__ */ jsx43(
20914
+ "img",
20915
+ {
20916
+ src: map.image,
20917
+ alt: "",
20918
+ hidden: true,
20919
+ ref: (node) => {
20920
+ if (node) syncImageSize(node);
20921
+ },
20922
+ onLoad: (event) => {
20923
+ syncImageSize(event.currentTarget);
20924
+ }
20925
+ }
20926
+ ),
20927
+ activeSpot && map.sector ? /* @__PURE__ */ jsx43(
20928
+ "svg",
20929
+ {
20930
+ className: "pano-map-sector-layer",
20931
+ width: size.width,
20932
+ height: size.height,
20933
+ style: {
20934
+ position: "absolute",
20935
+ inset: 0,
20936
+ overflow: "visible",
20937
+ pointerEvents: "none"
20938
+ },
20939
+ children: /* @__PURE__ */ jsx43(
20940
+ "g",
20941
+ {
20942
+ transform: `translate(${imagePointToContainer(activeSpot, viewport).x} ${imagePointToContainer(activeSpot, viewport).y})`,
20943
+ children: /* @__PURE__ */ jsx43(
20944
+ SectorShape,
20945
+ {
20946
+ sector: map.sector,
20947
+ angle: sectorAngle,
20948
+ rotation: sectorRotation(activeSpot.heading, view.yaw),
20949
+ gradientId: defaultGradientId(reactId),
20950
+ interactive: editing,
20951
+ onHeadingPointerDown: (event) => {
20952
+ event.stopPropagation();
20953
+ if (event.button !== 0) return;
20954
+ onSelectedSpotIdChange?.(activeSpot.id);
20955
+ dragRef.current = {
20956
+ kind: "heading",
20957
+ pointerId: event.pointerId,
20958
+ spotId: activeSpot.id
20959
+ };
20960
+ event.currentTarget.setPointerCapture(event.pointerId);
20961
+ },
20962
+ transformRef: (node) => {
20963
+ sectorNodeRef.current = node;
20964
+ }
20965
+ }
20966
+ )
20967
+ }
20968
+ )
20969
+ }
20970
+ ) : null,
20971
+ spots.map((spot) => {
20972
+ const point = imagePointToContainer(spot, viewport);
20973
+ const active = spot.sceneId === activeSceneId;
20974
+ const selected = spot.id === selectedSpotId;
20975
+ return /* @__PURE__ */ jsx43(
20976
+ "button",
20977
+ {
20978
+ type: "button",
20979
+ className: "pano-map-spot",
20980
+ "aria-label": spot.sceneId,
20981
+ "aria-current": active ? "true" : void 0,
20982
+ "data-active": active ? "true" : void 0,
20983
+ "data-selected": selected ? "true" : void 0,
20984
+ style: {
20985
+ position: "absolute",
20986
+ left: point.x,
20987
+ top: point.y,
20988
+ width: iconSize,
20989
+ height: iconSize,
20990
+ transform: "translate(-50%, -50%)",
20991
+ padding: 0,
20992
+ border: selected ? "2px solid #38bdf8" : "0",
20993
+ borderRadius: 8,
20994
+ background: "transparent",
20995
+ cursor: editing ? "grab" : "pointer",
20996
+ pointerEvents: "auto"
20997
+ },
20998
+ onPointerDown: (event) => {
20999
+ event.stopPropagation();
21000
+ if (event.button !== 0) return;
21001
+ onSelectedSpotIdChange?.(spot.id);
21002
+ if (!editing) return;
21003
+ const pointLocal = localPoint(event);
21004
+ if (!pointLocal) return;
21005
+ const center = imagePointToContainer(spot, viewport);
21006
+ dragRef.current = {
21007
+ kind: "spot",
21008
+ pointerId: event.pointerId,
21009
+ spotId: spot.id,
21010
+ offsetX: pointLocal.x - center.x,
21011
+ offsetY: pointLocal.y - center.y
21012
+ };
21013
+ event.currentTarget.setPointerCapture(event.pointerId);
21014
+ },
21015
+ onClick: (event) => {
21016
+ event.stopPropagation();
21017
+ if (editing) return;
21018
+ onSelectScene?.(spot.sceneId, spot);
21019
+ },
21020
+ children: spot.icon ? /* @__PURE__ */ jsx43(
21021
+ "img",
21022
+ {
21023
+ src: spot.icon,
21024
+ alt: "",
21025
+ draggable: false,
21026
+ style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" }
21027
+ }
21028
+ ) : /* @__PURE__ */ jsx43(DefaultSpotIcon, { active, size: iconSize })
21029
+ },
21030
+ spot.id
21031
+ );
21032
+ })
21033
+ ]
21034
+ }
19198
21035
  )
19199
- });
21036
+ ]
21037
+ }
21038
+ );
21039
+ }
21040
+
21041
+ // src/map/pano-map-overlay.tsx
21042
+ import {
21043
+ useEffect as useEffect45,
21044
+ useId as useId5,
21045
+ useMemo as useMemo31,
21046
+ useRef as useRef45,
21047
+ useState as useState29
21048
+ } from "react";
21049
+ import { Fragment as Fragment9, jsx as jsx44, jsxs as jsxs25 } from "react/jsx-runtime";
21050
+ var DEFAULT_DIALOG_WIDTH = 720;
21051
+ var DEFAULT_DIALOG_HEIGHT = 520;
21052
+ function dockedBox(corner, offsetX, offsetY, width, height) {
21053
+ const horizontal = corner === "top-right" || corner === "bottom-right" ? { left: "auto", right: offsetX } : { left: offsetX, right: "auto" };
21054
+ const vertical = corner === "bottom-left" || corner === "bottom-right" ? { top: "auto", bottom: offsetY } : { top: offsetY, bottom: "auto" };
21055
+ return { ...horizontal, ...vertical, width, height };
21056
+ }
21057
+ function PanoMapDialogShell({
21058
+ open,
21059
+ onOpenChange,
21060
+ title,
21061
+ closeLabel,
21062
+ dialogWidth,
21063
+ dialogHeight,
21064
+ children
21065
+ }) {
21066
+ usePanoModal(open);
21067
+ const { size } = usePanoOverlay();
21068
+ const titleId = useId5();
21069
+ const panelRef = useRef45(null);
21070
+ const previouslyFocused = useRef45(null);
21071
+ useEffect45(() => {
21072
+ if (!open) return;
21073
+ const active = panelRef.current?.ownerDocument.activeElement;
21074
+ previouslyFocused.current = active instanceof HTMLElement ? active : null;
21075
+ const close = panelRef.current?.querySelector("[data-pano-map-dialog-close]");
21076
+ close?.focus({ preventScroll: true });
21077
+ const onKeyDown = (event) => {
21078
+ if (event.key === "Escape") {
21079
+ event.preventDefault();
21080
+ event.stopPropagation();
21081
+ onOpenChange(false);
21082
+ }
19200
21083
  };
19201
- selected?.addEventListener("cuechange", handleCueChange);
19202
- video.textTracks.addEventListener("addtrack", applyModes);
19203
- video.textTracks.addEventListener("change", applyModes);
21084
+ const doc = panelRef.current?.ownerDocument;
21085
+ doc?.addEventListener("keydown", onKeyDown, true);
19204
21086
  return () => {
19205
- selected?.removeEventListener("cuechange", handleCueChange);
19206
- video.textTracks.removeEventListener("addtrack", applyModes);
19207
- video.textTracks.removeEventListener("change", applyModes);
21087
+ doc?.removeEventListener("keydown", onKeyDown, true);
21088
+ const previous = previouslyFocused.current;
21089
+ if (previous?.isConnected) previous.focus({ preventScroll: true });
19208
21090
  };
19209
- }, [emitSnapshot, resource, trackId, tracks]);
19210
- useEffect41(() => {
19211
- if (!captionsEnabled) {
19212
- setTrackId(null);
19213
- return;
19214
- }
19215
- setTrackId(defaultPanoVideoTrackId(tracks));
19216
- }, [captionsEnabled, tracks]);
19217
- const activeTexture = ready && resource ? resource.texture : posterTexture;
19218
- if (variants.length === 0) {
19219
- return null;
19220
- }
19221
- return /* @__PURE__ */ jsxs22(Fragment7, { children: [
19222
- activeTexture ? /* @__PURE__ */ jsxs22(
19223
- "mesh",
21091
+ }, [open, onOpenChange]);
21092
+ if (!open) return null;
21093
+ const preferredWidth = Number.isFinite(dialogWidth) && dialogWidth > 0 ? dialogWidth : DEFAULT_DIALOG_WIDTH;
21094
+ const preferredHeight = Number.isFinite(dialogHeight) && dialogHeight > 0 ? dialogHeight : DEFAULT_DIALOG_HEIGHT;
21095
+ const width = Math.min(preferredWidth, Math.max(0, size.width - 32));
21096
+ const height = Math.min(preferredHeight, Math.max(0, size.height - 32));
21097
+ const left = Math.max(0, (size.width - width) / 2);
21098
+ const top = Math.max(0, (size.height - height) / 2);
21099
+ return /* @__PURE__ */ jsxs25(Fragment9, { children: [
21100
+ /* @__PURE__ */ jsx44(
21101
+ "div",
19224
21102
  {
19225
- rotation: [0, MathUtils13.degToRad(90 + yawOffset2), 0],
19226
- visible,
21103
+ className: "pano-map-dialog-backdrop",
21104
+ "aria-hidden": "true",
21105
+ style: {
21106
+ position: "absolute",
21107
+ inset: 0,
21108
+ pointerEvents: "auto"
21109
+ },
21110
+ onClick: () => onOpenChange(false)
21111
+ }
21112
+ ),
21113
+ /* @__PURE__ */ jsxs25(
21114
+ "div",
21115
+ {
21116
+ ref: panelRef,
21117
+ className: "pano-map-dialog",
21118
+ role: "dialog",
21119
+ "aria-modal": "true",
21120
+ "aria-labelledby": titleId,
21121
+ "data-pano-map-dialog": "",
21122
+ style: {
21123
+ position: "absolute",
21124
+ left,
21125
+ top,
21126
+ width,
21127
+ height,
21128
+ boxSizing: "border-box",
21129
+ display: "flex",
21130
+ flexDirection: "column",
21131
+ overflow: "hidden",
21132
+ pointerEvents: "auto",
21133
+ zIndex: 1
21134
+ },
19227
21135
  children: [
19228
- /* @__PURE__ */ jsx41("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
19229
- /* @__PURE__ */ jsx41(
19230
- PanoBasicMaterial,
19231
- {
19232
- map: activeTexture,
19233
- side: BackSide4,
19234
- toneMapped: false
19235
- }
19236
- )
21136
+ /* @__PURE__ */ jsxs25("header", { className: "pano-map-dialog-header", style: { flexShrink: 0 }, children: [
21137
+ /* @__PURE__ */ jsx44("h2", { id: titleId, className: "pano-map-dialog-title", children: title }),
21138
+ /* @__PURE__ */ jsx44(
21139
+ "button",
21140
+ {
21141
+ type: "button",
21142
+ className: "pano-map-dialog-close",
21143
+ "data-pano-map-dialog-close": "",
21144
+ "aria-label": closeLabel,
21145
+ onClick: () => onOpenChange(false),
21146
+ children: /* @__PURE__ */ jsx44("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx44("path", { d: "m4 4 8 8M12 4l-8 8" }) })
21147
+ }
21148
+ )
21149
+ ] }),
21150
+ /* @__PURE__ */ jsx44("div", { className: "pano-map-dialog-body", style: { minHeight: 0, flex: 1 }, children })
19237
21151
  ]
19238
21152
  }
19239
- ) : null,
19240
- captionsEnabled ? /* @__PURE__ */ jsx41(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
21153
+ )
19241
21154
  ] });
19242
21155
  }
21156
+ function PanoMapOverlay({
21157
+ map,
21158
+ activeSceneId,
21159
+ onSelectScene,
21160
+ view,
21161
+ mode = "view",
21162
+ selectedSpotId,
21163
+ onSelectedSpotIdChange,
21164
+ onEditChange,
21165
+ placementSceneId,
21166
+ presentationOverride,
21167
+ presentation: presentationModeProp,
21168
+ corner: cornerProp,
21169
+ width: widthProp,
21170
+ height: heightProp,
21171
+ offsetX: offsetXProp,
21172
+ offsetY: offsetYProp,
21173
+ dialogWidth = DEFAULT_DIALOG_WIDTH,
21174
+ dialogHeight = DEFAULT_DIALOG_HEIGHT,
21175
+ interaction,
21176
+ maxScale,
21177
+ initialScale,
21178
+ showZoomControls,
21179
+ showCollapseControl,
21180
+ iconSize,
21181
+ openLabel = "Map",
21182
+ closeLabel = "Close",
21183
+ title = "Map",
21184
+ className,
21185
+ style,
21186
+ editorSurface = false
21187
+ }) {
21188
+ const { size } = usePanoOverlay();
21189
+ const [open, setOpen] = useState29(false);
21190
+ const [collapsed, setCollapsed] = useState29(false);
21191
+ const basePresentation = presentationOverride ?? map.presentation ?? {
21192
+ mode: "docked",
21193
+ corner: "bottom-left",
21194
+ width: 220,
21195
+ height: 160,
21196
+ offsetX: 16,
21197
+ offsetY: 16
21198
+ };
21199
+ const presentationMode = presentationModeProp ?? basePresentation.mode;
21200
+ const corner = cornerProp ?? basePresentation.corner ?? "bottom-left";
21201
+ const offsetX = offsetXProp ?? basePresentation.offsetX ?? 16;
21202
+ const offsetY = offsetYProp ?? basePresentation.offsetY ?? 16;
21203
+ const width = widthProp ?? basePresentation.width ?? (presentationMode === "dialog" ? 48 : 220);
21204
+ const height = heightProp ?? basePresentation.height ?? (presentationMode === "dialog" ? 48 : 160);
21205
+ const resolvedMap = useMemo31(() => {
21206
+ if (interaction === void 0 && maxScale === void 0 && iconSize === void 0) {
21207
+ return map;
21208
+ }
21209
+ return {
21210
+ ...map,
21211
+ ...interaction === void 0 ? {} : { interaction },
21212
+ ...maxScale === void 0 ? {} : { maxScale },
21213
+ ...iconSize === void 0 ? {} : { iconSize }
21214
+ };
21215
+ }, [iconSize, interaction, map, maxScale]);
21216
+ const mapProps = {
21217
+ map: resolvedMap,
21218
+ activeSceneId,
21219
+ view,
21220
+ mode,
21221
+ selectedSpotId,
21222
+ onSelectedSpotIdChange,
21223
+ onEditChange,
21224
+ placementSceneId,
21225
+ initialScale,
21226
+ showZoomControls,
21227
+ showCollapseControl,
21228
+ onSelectScene: (sceneId, spot) => {
21229
+ onSelectScene?.(sceneId, spot);
21230
+ if (presentationMode === "dialog") setOpen(false);
21231
+ }
21232
+ };
21233
+ const editorBox = useMemo31(() => {
21234
+ const panelWidth2 = widthProp !== void 0 && Number.isFinite(widthProp) && widthProp > 0 ? widthProp : Math.min(420, Math.max(240, size.width * 0.45));
21235
+ const panelHeight2 = heightProp !== void 0 && Number.isFinite(heightProp) && heightProp > 0 ? heightProp : Math.min(320, Math.max(180, size.height * 0.45));
21236
+ return {
21237
+ left: 16,
21238
+ right: "auto",
21239
+ top: "auto",
21240
+ bottom: 16,
21241
+ width: panelWidth2,
21242
+ height: panelHeight2
21243
+ };
21244
+ }, [heightProp, size.height, size.width, widthProp]);
21245
+ if (editorSurface) {
21246
+ return /* @__PURE__ */ jsx44(
21247
+ "div",
21248
+ {
21249
+ className: ["pano-map-overlay", "pano-map-overlay-editor", className].filter(Boolean).join(" "),
21250
+ style: {
21251
+ position: "absolute",
21252
+ pointerEvents: "auto",
21253
+ ...editorBox,
21254
+ ...style
21255
+ },
21256
+ "data-pano-map-overlay": "",
21257
+ children: /* @__PURE__ */ jsx44(PanoMap, { ...mapProps, showCollapseControl: false })
21258
+ }
21259
+ );
21260
+ }
21261
+ if (presentationMode === "dialog") {
21262
+ const launcherBox = dockedBox(corner, offsetX, offsetY, width, height);
21263
+ return /* @__PURE__ */ jsxs25("div", { className: ["pano-map-overlay", className].filter(Boolean).join(" "), "data-pano-map-overlay": "", children: [
21264
+ /* @__PURE__ */ jsx44(
21265
+ "button",
21266
+ {
21267
+ type: "button",
21268
+ className: "pano-map-launcher",
21269
+ "aria-label": openLabel,
21270
+ style: {
21271
+ position: "absolute",
21272
+ pointerEvents: "auto",
21273
+ ...launcherBox,
21274
+ ...style
21275
+ },
21276
+ onClick: () => setOpen(true),
21277
+ children: openLabel
21278
+ }
21279
+ ),
21280
+ /* @__PURE__ */ jsx44(
21281
+ PanoMapDialogShell,
21282
+ {
21283
+ open,
21284
+ onOpenChange: setOpen,
21285
+ title,
21286
+ closeLabel,
21287
+ dialogWidth,
21288
+ dialogHeight,
21289
+ children: /* @__PURE__ */ jsx44("div", { style: { height: "100%" }, children: /* @__PURE__ */ jsx44(PanoMap, { ...mapProps, showCollapseControl: false }) })
21290
+ }
21291
+ )
21292
+ ] });
21293
+ }
21294
+ const panelWidth = collapsed ? Math.max(44, Math.min(width, 56)) : width;
21295
+ const panelHeight = collapsed ? Math.max(44, Math.min(height, 56)) : height;
21296
+ const box = dockedBox(corner, offsetX, offsetY, panelWidth, panelHeight);
21297
+ return /* @__PURE__ */ jsx44(
21298
+ "div",
21299
+ {
21300
+ className: ["pano-map-overlay", className].filter(Boolean).join(" "),
21301
+ style: {
21302
+ position: "absolute",
21303
+ pointerEvents: "auto",
21304
+ ...box,
21305
+ ...style
21306
+ },
21307
+ "data-pano-map-overlay": "",
21308
+ "data-presentation": "docked",
21309
+ children: /* @__PURE__ */ jsx44(
21310
+ PanoMap,
21311
+ {
21312
+ ...mapProps,
21313
+ collapsed,
21314
+ onCollapsedChange: setCollapsed
21315
+ }
21316
+ )
21317
+ }
21318
+ );
21319
+ }
19243
21320
  export {
19244
21321
  AudioHotspot,
19245
21322
  AutoRotate,
@@ -19247,6 +21324,7 @@ export {
19247
21324
  DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE,
19248
21325
  DEFAULT_HOTSPOT_TOOLTIP_OFFSET,
19249
21326
  DEFAULT_PANO_CURSORS,
21327
+ DEFAULT_PANO_SCENE_MENU_APPEARANCE,
19250
21328
  DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
19251
21329
  DEFAULT_PANO_VIDEO_CONTROLS_APPEARANCE,
19252
21330
  DEFAULT_PANO_VIDEO_PLAYBACK_RATES,
@@ -19264,7 +21342,10 @@ export {
19264
21342
  PanoContextMenu,
19265
21343
  PanoEvents,
19266
21344
  PanoFilter,
21345
+ PanoMap,
21346
+ PanoMapOverlay,
19267
21347
  PanoOverlayPortalProvider,
21348
+ PanoSceneMenu,
19268
21349
  PanoVideo,
19269
21350
  PanoVideoControls,
19270
21351
  PanoViewer,
@@ -19284,13 +21365,19 @@ export {
19284
21365
  WebVR,
19285
21366
  clampPanoPitch,
19286
21367
  composePanoContextMenuItems,
21368
+ containRect,
21369
+ containerPointToImage,
19287
21370
  createDefaultPanoContextMenuItems,
19288
21371
  createPanoContextMenuPresets,
19289
21372
  cycleSceneId,
21373
+ headingFromVector,
21374
+ horizontalFov,
21375
+ imagePointToContainer,
19290
21376
  normalizePanoPosition,
19291
21377
  normalizePanoYaw,
19292
21378
  panoPositionToVector3,
19293
21379
  resolvePanoContextMenuEntries,
21380
+ sectorRotation,
19294
21381
  unwrapPolygonVertices,
19295
21382
  usePanoEvents,
19296
21383
  usePanoModal,