@ericchen1990/pano-view 3.7.0 → 3.8.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
  );
@@ -19240,6 +19240,976 @@ function PanoVideo({
19240
19240
  captionsEnabled ? /* @__PURE__ */ jsx41(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
19241
19241
  ] });
19242
19242
  }
19243
+
19244
+ // src/map/pano-map.tsx
19245
+ import {
19246
+ useCallback as useCallback12,
19247
+ useEffect as useEffect43,
19248
+ useId as useId3,
19249
+ useLayoutEffect as useLayoutEffect10,
19250
+ useMemo as useMemo29,
19251
+ useRef as useRef43,
19252
+ useState as useState27
19253
+ } from "react";
19254
+
19255
+ // src/map/geometry.ts
19256
+ function sectorRotation(heading, yaw) {
19257
+ return heading + yaw;
19258
+ }
19259
+ function horizontalFov(verticalFov, aspect) {
19260
+ if (!(aspect > 0) || !(verticalFov > 0) || verticalFov >= 180) return verticalFov;
19261
+ const verticalRad = verticalFov * Math.PI / 180;
19262
+ const horizontalRad = 2 * Math.atan(Math.tan(verticalRad / 2) * aspect);
19263
+ return horizontalRad * 180 / Math.PI;
19264
+ }
19265
+ function containRect(containerWidth, containerHeight, imageWidth, imageHeight) {
19266
+ if (!(containerWidth > 0) || !(containerHeight > 0) || !(imageWidth > 0) || !(imageHeight > 0)) {
19267
+ return { left: 0, top: 0, width: 0, height: 0 };
19268
+ }
19269
+ const scale = Math.min(
19270
+ containerWidth / imageWidth,
19271
+ containerHeight / imageHeight
19272
+ );
19273
+ const width = imageWidth * scale;
19274
+ const height = imageHeight * scale;
19275
+ return {
19276
+ left: (containerWidth - width) / 2,
19277
+ top: (containerHeight - height) / 2,
19278
+ width,
19279
+ height
19280
+ };
19281
+ }
19282
+ function imagePointToContainer(point, viewport) {
19283
+ const { fit, scale, panX, panY } = viewport;
19284
+ return {
19285
+ x: fit.left + panX + point.x * fit.width * scale,
19286
+ y: fit.top + panY + point.y * fit.height * scale
19287
+ };
19288
+ }
19289
+ function containerPointToImage(point, viewport) {
19290
+ const { fit, scale, panX, panY } = viewport;
19291
+ const width = fit.width * scale;
19292
+ const height = fit.height * scale;
19293
+ if (!(width > 0) || !(height > 0)) return null;
19294
+ return {
19295
+ x: (point.x - fit.left - panX) / width,
19296
+ y: (point.y - fit.top - panY) / height
19297
+ };
19298
+ }
19299
+ function clamp012(value) {
19300
+ return Math.min(1, Math.max(0, value));
19301
+ }
19302
+ function headingFromVector(dx, dy) {
19303
+ if (dx === 0 && dy === 0) return 0;
19304
+ const radians = Math.atan2(dx, -dy);
19305
+ return radians * 180 / Math.PI;
19306
+ }
19307
+ function normalizeDegrees(value) {
19308
+ const wrapped = (value % 360 + 360) % 360;
19309
+ return wrapped > 180 ? wrapped - 360 : wrapped;
19310
+ }
19311
+
19312
+ // src/map/use-map-view.ts
19313
+ import { useContext as useContext29, useEffect as useEffect42, useRef as useRef42, useState as useState26 } from "react";
19314
+ var DEFAULT_VIEW2 = { yaw: 0, pitch: 0, fov: 75 };
19315
+ function useMapView(controlled) {
19316
+ const eventBus = useContext29(PanoEventBusContext);
19317
+ const [view, setView] = useState26(controlled ?? DEFAULT_VIEW2);
19318
+ const controlledRef = useRef42(controlled);
19319
+ controlledRef.current = controlled;
19320
+ useEffect42(() => {
19321
+ if (controlled !== void 0) {
19322
+ setView(controlled);
19323
+ return;
19324
+ }
19325
+ if (!eventBus) return;
19326
+ return eventBus.subscribe("viewchange", (next) => {
19327
+ if (controlledRef.current !== void 0) return;
19328
+ setView(next);
19329
+ });
19330
+ }, [controlled, eventBus]);
19331
+ return controlled ?? view;
19332
+ }
19333
+
19334
+ // src/map/pano-map.tsx
19335
+ import { Fragment as Fragment8, jsx as jsx42, jsxs as jsxs23 } from "react/jsx-runtime";
19336
+ var DEFAULT_ICON_SIZE2 = 28;
19337
+ var DEFAULT_SECTOR_COLOR = "#ffffff";
19338
+ var DEFAULT_SECTOR_OPACITY = 0.55;
19339
+ var MIN_SCALE = 1;
19340
+ var DEFAULT_MAX_SCALE = 8;
19341
+ var ZOOM_STEP = 1.25;
19342
+ function defaultGradientId(prefix) {
19343
+ return `${prefix}-sector-gradient`;
19344
+ }
19345
+ function SectorShape({
19346
+ sector,
19347
+ angle,
19348
+ rotation,
19349
+ gradientId,
19350
+ interactive,
19351
+ onHeadingPointerDown,
19352
+ transformRef
19353
+ }) {
19354
+ const half = angle / 2;
19355
+ const start = (-half - 90) * Math.PI / 180;
19356
+ const end = (half - 90) * Math.PI / 180;
19357
+ const radius = sector.radius;
19358
+ const largeArc = angle > 180 ? 1 : 0;
19359
+ const x1 = Math.cos(start) * radius;
19360
+ const y1 = Math.sin(start) * radius;
19361
+ const x2 = Math.cos(end) * radius;
19362
+ const y2 = Math.sin(end) * radius;
19363
+ const path = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
19364
+ const opacity = sector.opacity ?? DEFAULT_SECTOR_OPACITY;
19365
+ const color = sector.color ?? DEFAULT_SECTOR_COLOR;
19366
+ const gradient = sector.gradient;
19367
+ const strokeWidth = sector.strokeWidth ?? 0;
19368
+ const strokeColor = sector.strokeColor ?? color;
19369
+ const strokeOpacity = sector.strokeOpacity ?? 1;
19370
+ const gradientStrength = gradient && gradient.stops.length > 0 ? Math.max(
19371
+ ...gradient.stops.map((stop) => stop.opacity ?? 1),
19372
+ Number.EPSILON
19373
+ ) : 1;
19374
+ return /* @__PURE__ */ jsxs23("g", { ref: transformRef, transform: `rotate(${rotation})`, style: { pointerEvents: "none" }, children: [
19375
+ gradient ? /* @__PURE__ */ jsx42("defs", { children: gradient.type === "radial" ? /* @__PURE__ */ jsx42("radialGradient", { id: gradientId, cx: "0", cy: "0", r: radius, gradientUnits: "userSpaceOnUse", children: gradient.stops.map((stop, index) => /* @__PURE__ */ jsx42(
19376
+ "stop",
19377
+ {
19378
+ offset: `${stop.offset * 100}%`,
19379
+ stopColor: stop.color,
19380
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
19381
+ },
19382
+ `${stop.offset}:${index}`
19383
+ )) }) : /* @__PURE__ */ jsx42(
19384
+ "linearGradient",
19385
+ {
19386
+ id: gradientId,
19387
+ x1: "0",
19388
+ y1: "0",
19389
+ x2: "0",
19390
+ y2: -radius,
19391
+ gradientUnits: "userSpaceOnUse",
19392
+ children: gradient.stops.map((stop, index) => /* @__PURE__ */ jsx42(
19393
+ "stop",
19394
+ {
19395
+ offset: `${stop.offset * 100}%`,
19396
+ stopColor: stop.color,
19397
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
19398
+ },
19399
+ `${stop.offset}:${index}`
19400
+ ))
19401
+ }
19402
+ ) }) : null,
19403
+ /* @__PURE__ */ jsx42(
19404
+ "path",
19405
+ {
19406
+ d: path,
19407
+ fill: gradient ? `url(#${gradientId})` : color,
19408
+ fillOpacity: opacity,
19409
+ stroke: strokeWidth > 0 ? strokeColor : "none",
19410
+ strokeWidth: strokeWidth > 0 ? strokeWidth : void 0,
19411
+ strokeOpacity: strokeWidth > 0 ? strokeOpacity : void 0,
19412
+ vectorEffect: "non-scaling-stroke",
19413
+ style: {
19414
+ pointerEvents: interactive ? "auto" : "none",
19415
+ cursor: interactive ? "grab" : void 0
19416
+ },
19417
+ onPointerDown: interactive ? onHeadingPointerDown : void 0
19418
+ }
19419
+ )
19420
+ ] });
19421
+ }
19422
+ function DefaultSpotIcon({ active, size }) {
19423
+ return /* @__PURE__ */ jsxs23(
19424
+ "svg",
19425
+ {
19426
+ width: size,
19427
+ height: size,
19428
+ viewBox: "0 0 32 32",
19429
+ "aria-hidden": "true",
19430
+ style: { display: "block" },
19431
+ children: [
19432
+ /* @__PURE__ */ jsx42(
19433
+ "rect",
19434
+ {
19435
+ x: "3",
19436
+ y: "7",
19437
+ width: "26",
19438
+ height: "18",
19439
+ rx: "5",
19440
+ fill: active ? "rgba(15,23,42,0.92)" : "rgba(15,23,42,0.72)",
19441
+ stroke: "rgba(255,255,255,0.92)",
19442
+ strokeWidth: "2"
19443
+ }
19444
+ ),
19445
+ /* @__PURE__ */ jsx42("circle", { cx: "16", cy: "16", r: "5", fill: "none", stroke: "rgba(255,255,255,0.92)", strokeWidth: "2" }),
19446
+ /* @__PURE__ */ jsx42("circle", { cx: "16", cy: "16", r: "2", fill: active ? "#ef4444" : "rgba(255,255,255,0.85)" })
19447
+ ]
19448
+ }
19449
+ );
19450
+ }
19451
+ function PanoMap({
19452
+ map,
19453
+ activeSceneId,
19454
+ onSelectScene,
19455
+ view: controlledView,
19456
+ mode = "view",
19457
+ selectedSpotId = null,
19458
+ onSelectedSpotIdChange,
19459
+ onEditChange,
19460
+ placementSceneId,
19461
+ className,
19462
+ style,
19463
+ showZoomControls,
19464
+ showCollapseControl,
19465
+ collapsed: controlledCollapsed,
19466
+ onCollapsedChange,
19467
+ renderChrome
19468
+ }) {
19469
+ const reactId = useId3().replace(/:/g, "");
19470
+ const view = useMapView(controlledView);
19471
+ const rootRef = useRef43(null);
19472
+ const containerRef = useRef43(null);
19473
+ const sectorNodeRef = useRef43(null);
19474
+ const yawRef = useRef43(view.yaw);
19475
+ const [size, setSize] = useState27({ width: 0, height: 0 });
19476
+ const [imageSize2, setImageSize] = useState27({ width: 0, height: 0 });
19477
+ const [scale, setScale] = useState27(1);
19478
+ const [pan, setPan] = useState27({ x: 0, y: 0 });
19479
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState27(false);
19480
+ const dragRef = useRef43(null);
19481
+ const spots = map.spots;
19482
+ const iconSize = map.iconSize ?? DEFAULT_ICON_SIZE2;
19483
+ const interaction = map.interaction ?? "fixed";
19484
+ const gestureZoom = interaction === "zoom";
19485
+ const maxScale = Math.max(MIN_SCALE, map.maxScale ?? DEFAULT_MAX_SCALE);
19486
+ const zoomControls = gestureZoom && (showZoomControls ?? map.controls?.zoom === true);
19487
+ const collapseControl = showCollapseControl ?? map.controls?.collapse === true;
19488
+ const collapsed = controlledCollapsed ?? uncontrolledCollapsed;
19489
+ const editing = mode === "edit";
19490
+ const fit = useMemo29(
19491
+ () => containRect(size.width, size.height, imageSize2.width, imageSize2.height),
19492
+ [size, imageSize2]
19493
+ );
19494
+ const viewport = useMemo29(
19495
+ () => ({ fit, scale, panX: pan.x, panY: pan.y }),
19496
+ [fit, scale, pan]
19497
+ );
19498
+ useLayoutEffect10(() => {
19499
+ if (collapsed) return;
19500
+ const node = containerRef.current;
19501
+ if (!node) return;
19502
+ const measure = () => {
19503
+ const width = node.clientWidth;
19504
+ const height = node.clientHeight;
19505
+ if (!(width > 0) || !(height > 0)) return;
19506
+ setSize(
19507
+ (previous) => previous.width === width && previous.height === height ? previous : { width, height }
19508
+ );
19509
+ };
19510
+ measure();
19511
+ const observer = new ResizeObserver(measure);
19512
+ observer.observe(node);
19513
+ return () => observer.disconnect();
19514
+ }, [collapsed]);
19515
+ useEffect43(() => {
19516
+ yawRef.current = view.yaw;
19517
+ const node = sectorNodeRef.current;
19518
+ if (!node) return;
19519
+ const active = spots.find((spot) => spot.sceneId === activeSceneId);
19520
+ if (!active || !map.sector) return;
19521
+ node.setAttribute(
19522
+ "transform",
19523
+ `rotate(${sectorRotation(active.heading, view.yaw)})`
19524
+ );
19525
+ }, [view.yaw, activeSceneId, spots, map.sector]);
19526
+ const setCollapsed = (next) => {
19527
+ onCollapsedChange?.(next);
19528
+ if (controlledCollapsed === void 0) setUncontrolledCollapsed(next);
19529
+ };
19530
+ const clampPan = useCallback12(
19531
+ (nextScale, nextPan) => {
19532
+ const width = fit.width * nextScale;
19533
+ const height = fit.height * nextScale;
19534
+ if (!(fit.width > 0) || !(fit.height > 0)) return nextPan;
19535
+ const minX = Math.min(0, size.width - (fit.left + width));
19536
+ const maxX = Math.max(0, -fit.left);
19537
+ const minY = Math.min(0, size.height - (fit.top + height));
19538
+ const maxY = Math.max(0, -fit.top);
19539
+ return {
19540
+ x: Math.min(maxX, Math.max(minX, nextPan.x)),
19541
+ y: Math.min(maxY, Math.max(minY, nextPan.y))
19542
+ };
19543
+ },
19544
+ [fit, size]
19545
+ );
19546
+ useEffect43(() => {
19547
+ if (!gestureZoom) {
19548
+ setScale(1);
19549
+ setPan({ x: 0, y: 0 });
19550
+ return;
19551
+ }
19552
+ setScale((previous) => {
19553
+ const next = Math.min(previous, maxScale);
19554
+ if (next !== previous) {
19555
+ setPan((currentPan) => clampPan(next, currentPan));
19556
+ }
19557
+ return next;
19558
+ });
19559
+ }, [clampPan, gestureZoom, maxScale]);
19560
+ const applyZoom = useCallback12(
19561
+ (nextScale, origin) => {
19562
+ if (!gestureZoom) return;
19563
+ const clamped = Math.min(maxScale, Math.max(MIN_SCALE, nextScale));
19564
+ const pivot = origin ?? { x: size.width / 2, y: size.height / 2 };
19565
+ const ratio = clamped / scale;
19566
+ const nextPan = {
19567
+ x: pivot.x - (pivot.x - (fit.left + pan.x)) * ratio - fit.left,
19568
+ y: pivot.y - (pivot.y - (fit.top + pan.y)) * ratio - fit.top
19569
+ };
19570
+ setScale(clamped);
19571
+ setPan(clampPan(clamped, nextPan));
19572
+ },
19573
+ [
19574
+ clampPan,
19575
+ fit.left,
19576
+ fit.top,
19577
+ gestureZoom,
19578
+ maxScale,
19579
+ pan.x,
19580
+ pan.y,
19581
+ scale,
19582
+ size.height,
19583
+ size.width
19584
+ ]
19585
+ );
19586
+ useEffect43(() => {
19587
+ const root = rootRef.current;
19588
+ if (!root) return;
19589
+ const onWheel = (event) => {
19590
+ event.preventDefault();
19591
+ event.stopPropagation();
19592
+ if (!gestureZoom || collapsed) return;
19593
+ const viewport2 = containerRef.current;
19594
+ if (!viewport2) return;
19595
+ const rect = viewport2.getBoundingClientRect();
19596
+ const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
19597
+ applyZoom(scale * factor, {
19598
+ x: event.clientX - rect.left,
19599
+ y: event.clientY - rect.top
19600
+ });
19601
+ };
19602
+ root.addEventListener("wheel", onWheel, { passive: false });
19603
+ return () => root.removeEventListener("wheel", onWheel);
19604
+ }, [applyZoom, collapsed, gestureZoom, scale]);
19605
+ const updateSpots = (next, selected) => {
19606
+ onEditChange?.({
19607
+ spots: next,
19608
+ ...selected === void 0 ? {} : { selectedSpotId: selected }
19609
+ });
19610
+ };
19611
+ const localPoint = (event) => {
19612
+ const rect = containerRef.current?.getBoundingClientRect();
19613
+ if (!rect) return null;
19614
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
19615
+ };
19616
+ const onPointerDownSurface = (event) => {
19617
+ if (collapsed) return;
19618
+ if (event.button !== 0) return;
19619
+ const point = localPoint(event);
19620
+ if (!point) return;
19621
+ if (editing && placementSceneId) {
19622
+ const image = containerPointToImage(point, viewport);
19623
+ if (!image) return;
19624
+ const spot = {
19625
+ id: crypto.randomUUID(),
19626
+ sceneId: placementSceneId,
19627
+ x: clamp012(image.x),
19628
+ y: clamp012(image.y),
19629
+ heading: normalizeDegrees(-view.yaw)
19630
+ };
19631
+ updateSpots([...spots, spot], spot.id);
19632
+ onSelectedSpotIdChange?.(spot.id);
19633
+ event.preventDefault();
19634
+ event.stopPropagation();
19635
+ return;
19636
+ }
19637
+ if (gestureZoom && scale > 1) {
19638
+ dragRef.current = {
19639
+ kind: "pan",
19640
+ pointerId: event.pointerId,
19641
+ originX: point.x,
19642
+ originY: point.y,
19643
+ panX: pan.x,
19644
+ panY: pan.y
19645
+ };
19646
+ event.currentTarget.setPointerCapture(event.pointerId);
19647
+ event.preventDefault();
19648
+ event.stopPropagation();
19649
+ }
19650
+ };
19651
+ const onPointerMoveSurface = (event) => {
19652
+ const drag = dragRef.current;
19653
+ if (!drag || drag.pointerId !== event.pointerId) return;
19654
+ const point = localPoint(event);
19655
+ if (!point) return;
19656
+ if (drag.kind === "pan") {
19657
+ setPan(
19658
+ clampPan(scale, {
19659
+ x: drag.panX + (point.x - drag.originX),
19660
+ y: drag.panY + (point.y - drag.originY)
19661
+ })
19662
+ );
19663
+ event.preventDefault();
19664
+ event.stopPropagation();
19665
+ return;
19666
+ }
19667
+ if (drag.kind === "spot") {
19668
+ const image = containerPointToImage(
19669
+ { x: point.x - drag.offsetX, y: point.y - drag.offsetY },
19670
+ viewport
19671
+ );
19672
+ if (!image) return;
19673
+ updateSpots(
19674
+ spots.map(
19675
+ (spot2) => spot2.id === drag.spotId ? { ...spot2, x: clamp012(image.x), y: clamp012(image.y) } : spot2
19676
+ )
19677
+ );
19678
+ event.preventDefault();
19679
+ event.stopPropagation();
19680
+ return;
19681
+ }
19682
+ const spot = spots.find((entry) => entry.id === drag.spotId);
19683
+ if (!spot) return;
19684
+ const center = imagePointToContainer(spot, viewport);
19685
+ const heading = normalizeDegrees(
19686
+ headingFromVector(point.x - center.x, point.y - center.y) - view.yaw
19687
+ );
19688
+ updateSpots(
19689
+ spots.map(
19690
+ (entry) => entry.id === drag.spotId ? { ...entry, heading } : entry
19691
+ )
19692
+ );
19693
+ if (sectorNodeRef.current && spot.sceneId === activeSceneId) {
19694
+ sectorNodeRef.current.setAttribute(
19695
+ "transform",
19696
+ `rotate(${sectorRotation(heading, view.yaw)})`
19697
+ );
19698
+ }
19699
+ event.preventDefault();
19700
+ event.stopPropagation();
19701
+ };
19702
+ const endDrag = (event) => {
19703
+ if (dragRef.current?.pointerId !== event.pointerId) return;
19704
+ dragRef.current = null;
19705
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
19706
+ event.currentTarget.releasePointerCapture(event.pointerId);
19707
+ }
19708
+ };
19709
+ const sectorAngle = (() => {
19710
+ if (!map.sector) return 0;
19711
+ if (!map.sector.followFov) return map.sector.angle;
19712
+ const aspect = size.width > 0 && size.height > 0 ? size.width / size.height : 1;
19713
+ return horizontalFov(view.fov, aspect);
19714
+ })();
19715
+ const activeSpot = spots.find((spot) => spot.sceneId === activeSceneId);
19716
+ const selectedSpot = spots.find((spot) => spot.id === selectedSpotId);
19717
+ const controls = /* @__PURE__ */ jsxs23("div", { className: "pano-map-controls", style: { pointerEvents: "auto" }, children: [
19718
+ collapseControl ? /* @__PURE__ */ jsx42(
19719
+ "button",
19720
+ {
19721
+ type: "button",
19722
+ className: "pano-map-control",
19723
+ "aria-label": collapsed ? "Expand map" : "Collapse map",
19724
+ "aria-pressed": !collapsed,
19725
+ style: {
19726
+ background: "rgba(15, 23, 42, 0.92)",
19727
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19728
+ color: "#f8fafc"
19729
+ },
19730
+ onClick: () => setCollapsed(!collapsed),
19731
+ children: collapsed ? "\u25A3" : "\u25A2"
19732
+ }
19733
+ ) : null,
19734
+ zoomControls && !collapsed ? /* @__PURE__ */ jsxs23(Fragment8, { children: [
19735
+ /* @__PURE__ */ jsx42(
19736
+ "button",
19737
+ {
19738
+ type: "button",
19739
+ className: "pano-map-control",
19740
+ "aria-label": "Zoom in",
19741
+ style: {
19742
+ background: "rgba(15, 23, 42, 0.92)",
19743
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19744
+ color: "#f8fafc"
19745
+ },
19746
+ onClick: () => applyZoom(scale * ZOOM_STEP),
19747
+ children: "+"
19748
+ }
19749
+ ),
19750
+ /* @__PURE__ */ jsx42(
19751
+ "button",
19752
+ {
19753
+ type: "button",
19754
+ className: "pano-map-control",
19755
+ "aria-label": "Zoom out",
19756
+ style: {
19757
+ background: "rgba(15, 23, 42, 0.92)",
19758
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19759
+ color: "#f8fafc"
19760
+ },
19761
+ onClick: () => applyZoom(scale / ZOOM_STEP),
19762
+ children: "\u2212"
19763
+ }
19764
+ )
19765
+ ] }) : null
19766
+ ] });
19767
+ const syncImageSize = (image) => {
19768
+ if (!(image.naturalWidth > 0) || !(image.naturalHeight > 0)) return;
19769
+ setImageSize(
19770
+ (previous) => previous.width === image.naturalWidth && previous.height === image.naturalHeight ? previous : { width: image.naturalWidth, height: image.naturalHeight }
19771
+ );
19772
+ };
19773
+ return /* @__PURE__ */ jsxs23(
19774
+ "div",
19775
+ {
19776
+ ref: rootRef,
19777
+ className: ["pano-map", collapsed ? "pano-map-collapsed" : "", className].filter(Boolean).join(" "),
19778
+ style: {
19779
+ position: "relative",
19780
+ overflow: "hidden",
19781
+ width: "100%",
19782
+ height: "100%",
19783
+ pointerEvents: "auto",
19784
+ touchAction: gestureZoom && !collapsed ? "none" : "auto",
19785
+ ...style
19786
+ },
19787
+ "data-pano-map": "",
19788
+ "data-mode": mode,
19789
+ "data-collapsed": collapsed ? "true" : void 0,
19790
+ children: [
19791
+ renderChrome ? renderChrome(controls) : /* @__PURE__ */ jsx42("div", { className: "pano-map-chrome", children: controls }),
19792
+ /* @__PURE__ */ jsxs23(
19793
+ "div",
19794
+ {
19795
+ ref: containerRef,
19796
+ className: "pano-map-viewport",
19797
+ hidden: collapsed,
19798
+ style: {
19799
+ position: "absolute",
19800
+ inset: 0,
19801
+ overflow: "hidden",
19802
+ visibility: collapsed ? "hidden" : "visible"
19803
+ },
19804
+ onPointerDown: onPointerDownSurface,
19805
+ onPointerMove: onPointerMoveSurface,
19806
+ onPointerUp: endDrag,
19807
+ onPointerCancel: endDrag,
19808
+ children: [
19809
+ fit.width > 0 && fit.height > 0 ? /* @__PURE__ */ jsx42(
19810
+ "div",
19811
+ {
19812
+ className: "pano-map-image-layer",
19813
+ style: {
19814
+ position: "absolute",
19815
+ left: fit.left + pan.x,
19816
+ top: fit.top + pan.y,
19817
+ width: fit.width * scale,
19818
+ height: fit.height * scale,
19819
+ transformOrigin: "0 0"
19820
+ },
19821
+ children: /* @__PURE__ */ jsx42(
19822
+ "img",
19823
+ {
19824
+ src: map.image,
19825
+ alt: "",
19826
+ draggable: false,
19827
+ ref: (node) => {
19828
+ if (node) syncImageSize(node);
19829
+ },
19830
+ onLoad: (event) => {
19831
+ syncImageSize(event.currentTarget);
19832
+ },
19833
+ style: {
19834
+ width: "100%",
19835
+ height: "100%",
19836
+ display: "block",
19837
+ userSelect: "none",
19838
+ pointerEvents: "none"
19839
+ }
19840
+ }
19841
+ )
19842
+ }
19843
+ ) : /* @__PURE__ */ jsx42(
19844
+ "img",
19845
+ {
19846
+ src: map.image,
19847
+ alt: "",
19848
+ hidden: true,
19849
+ ref: (node) => {
19850
+ if (node) syncImageSize(node);
19851
+ },
19852
+ onLoad: (event) => {
19853
+ syncImageSize(event.currentTarget);
19854
+ }
19855
+ }
19856
+ ),
19857
+ activeSpot && map.sector ? /* @__PURE__ */ jsx42(
19858
+ "svg",
19859
+ {
19860
+ className: "pano-map-sector-layer",
19861
+ width: size.width,
19862
+ height: size.height,
19863
+ style: {
19864
+ position: "absolute",
19865
+ inset: 0,
19866
+ overflow: "visible",
19867
+ pointerEvents: "none"
19868
+ },
19869
+ children: /* @__PURE__ */ jsx42(
19870
+ "g",
19871
+ {
19872
+ transform: `translate(${imagePointToContainer(activeSpot, viewport).x} ${imagePointToContainer(activeSpot, viewport).y})`,
19873
+ children: /* @__PURE__ */ jsx42(
19874
+ SectorShape,
19875
+ {
19876
+ sector: map.sector,
19877
+ angle: sectorAngle,
19878
+ rotation: sectorRotation(activeSpot.heading, view.yaw),
19879
+ gradientId: defaultGradientId(reactId),
19880
+ interactive: editing,
19881
+ onHeadingPointerDown: (event) => {
19882
+ event.stopPropagation();
19883
+ if (event.button !== 0) return;
19884
+ onSelectedSpotIdChange?.(activeSpot.id);
19885
+ dragRef.current = {
19886
+ kind: "heading",
19887
+ pointerId: event.pointerId,
19888
+ spotId: activeSpot.id
19889
+ };
19890
+ event.currentTarget.setPointerCapture(event.pointerId);
19891
+ },
19892
+ transformRef: (node) => {
19893
+ sectorNodeRef.current = node;
19894
+ }
19895
+ }
19896
+ )
19897
+ }
19898
+ )
19899
+ }
19900
+ ) : null,
19901
+ spots.map((spot) => {
19902
+ const point = imagePointToContainer(spot, viewport);
19903
+ const active = spot.sceneId === activeSceneId;
19904
+ const selected = spot.id === selectedSpotId;
19905
+ return /* @__PURE__ */ jsx42(
19906
+ "button",
19907
+ {
19908
+ type: "button",
19909
+ className: "pano-map-spot",
19910
+ "aria-label": spot.sceneId,
19911
+ "aria-current": active ? "true" : void 0,
19912
+ "data-active": active ? "true" : void 0,
19913
+ "data-selected": selected ? "true" : void 0,
19914
+ style: {
19915
+ position: "absolute",
19916
+ left: point.x,
19917
+ top: point.y,
19918
+ width: iconSize,
19919
+ height: iconSize,
19920
+ transform: "translate(-50%, -50%)",
19921
+ padding: 0,
19922
+ border: selected ? "2px solid #38bdf8" : "0",
19923
+ borderRadius: 8,
19924
+ background: "transparent",
19925
+ cursor: editing ? "grab" : "pointer",
19926
+ pointerEvents: "auto"
19927
+ },
19928
+ onPointerDown: (event) => {
19929
+ event.stopPropagation();
19930
+ if (event.button !== 0) return;
19931
+ onSelectedSpotIdChange?.(spot.id);
19932
+ if (!editing) return;
19933
+ const pointLocal = localPoint(event);
19934
+ if (!pointLocal) return;
19935
+ const center = imagePointToContainer(spot, viewport);
19936
+ dragRef.current = {
19937
+ kind: "spot",
19938
+ pointerId: event.pointerId,
19939
+ spotId: spot.id,
19940
+ offsetX: pointLocal.x - center.x,
19941
+ offsetY: pointLocal.y - center.y
19942
+ };
19943
+ event.currentTarget.setPointerCapture(event.pointerId);
19944
+ },
19945
+ onClick: (event) => {
19946
+ event.stopPropagation();
19947
+ if (editing) return;
19948
+ onSelectScene?.(spot.sceneId, spot);
19949
+ },
19950
+ children: spot.icon ? /* @__PURE__ */ jsx42(
19951
+ "img",
19952
+ {
19953
+ src: spot.icon,
19954
+ alt: "",
19955
+ draggable: false,
19956
+ style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" }
19957
+ }
19958
+ ) : /* @__PURE__ */ jsx42(DefaultSpotIcon, { active, size: iconSize })
19959
+ },
19960
+ spot.id
19961
+ );
19962
+ })
19963
+ ]
19964
+ }
19965
+ )
19966
+ ]
19967
+ }
19968
+ );
19969
+ }
19970
+
19971
+ // src/map/pano-map-overlay.tsx
19972
+ import {
19973
+ useEffect as useEffect44,
19974
+ useId as useId4,
19975
+ useMemo as useMemo30,
19976
+ useRef as useRef44,
19977
+ useState as useState28
19978
+ } from "react";
19979
+ import { Fragment as Fragment9, jsx as jsx43, jsxs as jsxs24 } from "react/jsx-runtime";
19980
+ function dockedBox(corner, offsetX, offsetY, width, height) {
19981
+ const horizontal = corner === "top-right" || corner === "bottom-right" ? { left: "auto", right: offsetX } : { left: offsetX, right: "auto" };
19982
+ const vertical = corner === "bottom-left" || corner === "bottom-right" ? { top: "auto", bottom: offsetY } : { top: offsetY, bottom: "auto" };
19983
+ return { ...horizontal, ...vertical, width, height };
19984
+ }
19985
+ function PanoMapDialogShell({
19986
+ open,
19987
+ onOpenChange,
19988
+ title,
19989
+ closeLabel,
19990
+ children
19991
+ }) {
19992
+ usePanoModal(open);
19993
+ const { size } = usePanoOverlay();
19994
+ const titleId = useId4();
19995
+ const panelRef = useRef44(null);
19996
+ const previouslyFocused = useRef44(null);
19997
+ useEffect44(() => {
19998
+ if (!open) return;
19999
+ const active = panelRef.current?.ownerDocument.activeElement;
20000
+ previouslyFocused.current = active instanceof HTMLElement ? active : null;
20001
+ const close = panelRef.current?.querySelector("[data-pano-map-dialog-close]");
20002
+ close?.focus({ preventScroll: true });
20003
+ const onKeyDown = (event) => {
20004
+ if (event.key === "Escape") {
20005
+ event.preventDefault();
20006
+ event.stopPropagation();
20007
+ onOpenChange(false);
20008
+ }
20009
+ };
20010
+ const doc = panelRef.current?.ownerDocument;
20011
+ doc?.addEventListener("keydown", onKeyDown, true);
20012
+ return () => {
20013
+ doc?.removeEventListener("keydown", onKeyDown, true);
20014
+ const previous = previouslyFocused.current;
20015
+ if (previous?.isConnected) previous.focus({ preventScroll: true });
20016
+ };
20017
+ }, [open, onOpenChange]);
20018
+ if (!open) return null;
20019
+ const width = Math.min(720, Math.max(0, size.width - 32));
20020
+ const height = Math.min(520, Math.max(0, size.height - 32));
20021
+ const left = Math.max(0, (size.width - width) / 2);
20022
+ const top = Math.max(0, (size.height - height) / 2);
20023
+ return /* @__PURE__ */ jsxs24(Fragment9, { children: [
20024
+ /* @__PURE__ */ jsx43(
20025
+ "div",
20026
+ {
20027
+ className: "pano-map-dialog-backdrop",
20028
+ "aria-hidden": "true",
20029
+ style: {
20030
+ position: "absolute",
20031
+ inset: 0,
20032
+ pointerEvents: "auto"
20033
+ },
20034
+ onClick: () => onOpenChange(false)
20035
+ }
20036
+ ),
20037
+ /* @__PURE__ */ jsxs24(
20038
+ "div",
20039
+ {
20040
+ ref: panelRef,
20041
+ className: "pano-map-dialog",
20042
+ role: "dialog",
20043
+ "aria-modal": "true",
20044
+ "aria-labelledby": titleId,
20045
+ "data-pano-map-dialog": "",
20046
+ style: {
20047
+ position: "absolute",
20048
+ left,
20049
+ top,
20050
+ width,
20051
+ height,
20052
+ boxSizing: "border-box",
20053
+ display: "flex",
20054
+ flexDirection: "column",
20055
+ overflow: "hidden",
20056
+ pointerEvents: "auto",
20057
+ zIndex: 1
20058
+ },
20059
+ children: [
20060
+ /* @__PURE__ */ jsxs24("header", { className: "pano-map-dialog-header", style: { flexShrink: 0 }, children: [
20061
+ /* @__PURE__ */ jsx43("h2", { id: titleId, className: "pano-map-dialog-title", children: title }),
20062
+ /* @__PURE__ */ jsx43(
20063
+ "button",
20064
+ {
20065
+ type: "button",
20066
+ className: "pano-map-dialog-close",
20067
+ "data-pano-map-dialog-close": "",
20068
+ "aria-label": closeLabel,
20069
+ onClick: () => onOpenChange(false),
20070
+ children: /* @__PURE__ */ jsx43("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx43("path", { d: "m4 4 8 8M12 4l-8 8" }) })
20071
+ }
20072
+ )
20073
+ ] }),
20074
+ /* @__PURE__ */ jsx43("div", { className: "pano-map-dialog-body", style: { minHeight: 0, flex: 1 }, children })
20075
+ ]
20076
+ }
20077
+ )
20078
+ ] });
20079
+ }
20080
+ function PanoMapOverlay({
20081
+ map,
20082
+ activeSceneId,
20083
+ onSelectScene,
20084
+ view,
20085
+ mode = "view",
20086
+ selectedSpotId,
20087
+ onSelectedSpotIdChange,
20088
+ onEditChange,
20089
+ placementSceneId,
20090
+ presentationOverride,
20091
+ openLabel = "Map",
20092
+ closeLabel = "Close",
20093
+ title = "Map",
20094
+ className,
20095
+ style,
20096
+ editorSurface = false
20097
+ }) {
20098
+ const { size } = usePanoOverlay();
20099
+ const [open, setOpen] = useState28(false);
20100
+ const [collapsed, setCollapsed] = useState28(false);
20101
+ const presentation = presentationOverride ?? map.presentation ?? {
20102
+ mode: "docked",
20103
+ corner: "bottom-left",
20104
+ width: 220,
20105
+ height: 160,
20106
+ offsetX: 16,
20107
+ offsetY: 16
20108
+ };
20109
+ const corner = presentation.corner ?? "bottom-left";
20110
+ const offsetX = presentation.offsetX ?? 16;
20111
+ const offsetY = presentation.offsetY ?? 16;
20112
+ const width = presentation.width ?? (presentation.mode === "dialog" ? 48 : 220);
20113
+ const height = presentation.height ?? (presentation.mode === "dialog" ? 48 : 160);
20114
+ const mapProps = {
20115
+ map,
20116
+ activeSceneId,
20117
+ view,
20118
+ mode,
20119
+ selectedSpotId,
20120
+ onSelectedSpotIdChange,
20121
+ onEditChange,
20122
+ placementSceneId,
20123
+ onSelectScene: (sceneId, spot) => {
20124
+ onSelectScene?.(sceneId, spot);
20125
+ if (presentation.mode === "dialog") setOpen(false);
20126
+ }
20127
+ };
20128
+ const editorBox = useMemo30(() => {
20129
+ const panelWidth2 = Math.min(420, Math.max(240, size.width * 0.45));
20130
+ const panelHeight2 = Math.min(320, Math.max(180, size.height * 0.45));
20131
+ return {
20132
+ left: 16,
20133
+ right: "auto",
20134
+ top: "auto",
20135
+ bottom: 16,
20136
+ width: panelWidth2,
20137
+ height: panelHeight2
20138
+ };
20139
+ }, [size.height, size.width]);
20140
+ if (editorSurface) {
20141
+ return /* @__PURE__ */ jsx43(
20142
+ "div",
20143
+ {
20144
+ className: ["pano-map-overlay", "pano-map-overlay-editor", className].filter(Boolean).join(" "),
20145
+ style: {
20146
+ position: "absolute",
20147
+ pointerEvents: "auto",
20148
+ ...editorBox,
20149
+ ...style
20150
+ },
20151
+ "data-pano-map-overlay": "",
20152
+ children: /* @__PURE__ */ jsx43(PanoMap, { ...mapProps, showCollapseControl: false })
20153
+ }
20154
+ );
20155
+ }
20156
+ if (presentation.mode === "dialog") {
20157
+ const launcherBox = dockedBox(corner, offsetX, offsetY, width, height);
20158
+ return /* @__PURE__ */ jsxs24("div", { className: ["pano-map-overlay", className].filter(Boolean).join(" "), "data-pano-map-overlay": "", children: [
20159
+ /* @__PURE__ */ jsx43(
20160
+ "button",
20161
+ {
20162
+ type: "button",
20163
+ className: "pano-map-launcher",
20164
+ "aria-label": openLabel,
20165
+ style: {
20166
+ position: "absolute",
20167
+ pointerEvents: "auto",
20168
+ ...launcherBox,
20169
+ ...style
20170
+ },
20171
+ onClick: () => setOpen(true),
20172
+ children: openLabel
20173
+ }
20174
+ ),
20175
+ /* @__PURE__ */ jsx43(
20176
+ PanoMapDialogShell,
20177
+ {
20178
+ open,
20179
+ onOpenChange: setOpen,
20180
+ title,
20181
+ closeLabel,
20182
+ children: /* @__PURE__ */ jsx43("div", { style: { height: "100%" }, children: /* @__PURE__ */ jsx43(PanoMap, { ...mapProps, showCollapseControl: false }) })
20183
+ }
20184
+ )
20185
+ ] });
20186
+ }
20187
+ const panelWidth = collapsed ? Math.max(44, Math.min(width, 56)) : width;
20188
+ const panelHeight = collapsed ? Math.max(44, Math.min(height, 56)) : height;
20189
+ const box = dockedBox(corner, offsetX, offsetY, panelWidth, panelHeight);
20190
+ return /* @__PURE__ */ jsx43(
20191
+ "div",
20192
+ {
20193
+ className: ["pano-map-overlay", className].filter(Boolean).join(" "),
20194
+ style: {
20195
+ position: "absolute",
20196
+ pointerEvents: "auto",
20197
+ ...box,
20198
+ ...style
20199
+ },
20200
+ "data-pano-map-overlay": "",
20201
+ "data-presentation": "docked",
20202
+ children: /* @__PURE__ */ jsx43(
20203
+ PanoMap,
20204
+ {
20205
+ ...mapProps,
20206
+ collapsed,
20207
+ onCollapsedChange: setCollapsed
20208
+ }
20209
+ )
20210
+ }
20211
+ );
20212
+ }
19243
20213
  export {
19244
20214
  AudioHotspot,
19245
20215
  AutoRotate,
@@ -19264,6 +20234,8 @@ export {
19264
20234
  PanoContextMenu,
19265
20235
  PanoEvents,
19266
20236
  PanoFilter,
20237
+ PanoMap,
20238
+ PanoMapOverlay,
19267
20239
  PanoOverlayPortalProvider,
19268
20240
  PanoVideo,
19269
20241
  PanoVideoControls,
@@ -19284,13 +20256,19 @@ export {
19284
20256
  WebVR,
19285
20257
  clampPanoPitch,
19286
20258
  composePanoContextMenuItems,
20259
+ containRect,
20260
+ containerPointToImage,
19287
20261
  createDefaultPanoContextMenuItems,
19288
20262
  createPanoContextMenuPresets,
19289
20263
  cycleSceneId,
20264
+ headingFromVector,
20265
+ horizontalFov,
20266
+ imagePointToContainer,
19290
20267
  normalizePanoPosition,
19291
20268
  normalizePanoYaw,
19292
20269
  panoPositionToVector3,
19293
20270
  resolvePanoContextMenuEntries,
20271
+ sectorRotation,
19294
20272
  unwrapPolygonVertices,
19295
20273
  usePanoEvents,
19296
20274
  usePanoModal,