@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.cjs CHANGED
@@ -2271,6 +2271,8 @@ __export(index_exports, {
2271
2271
  PanoContextMenu: () => PanoContextMenu,
2272
2272
  PanoEvents: () => PanoEvents,
2273
2273
  PanoFilter: () => PanoFilter,
2274
+ PanoMap: () => PanoMap,
2275
+ PanoMapOverlay: () => PanoMapOverlay,
2274
2276
  PanoOverlayPortalProvider: () => PanoOverlayPortalProvider,
2275
2277
  PanoVideo: () => PanoVideo,
2276
2278
  PanoVideoControls: () => PanoVideoControls,
@@ -2291,13 +2293,19 @@ __export(index_exports, {
2291
2293
  WebVR: () => WebVR,
2292
2294
  clampPanoPitch: () => clampPanoPitch,
2293
2295
  composePanoContextMenuItems: () => composePanoContextMenuItems,
2296
+ containRect: () => containRect,
2297
+ containerPointToImage: () => containerPointToImage,
2294
2298
  createDefaultPanoContextMenuItems: () => createDefaultPanoContextMenuItems,
2295
2299
  createPanoContextMenuPresets: () => createPanoContextMenuPresets,
2296
2300
  cycleSceneId: () => cycleSceneId,
2301
+ headingFromVector: () => headingFromVector,
2302
+ horizontalFov: () => horizontalFov,
2303
+ imagePointToContainer: () => imagePointToContainer,
2297
2304
  normalizePanoPosition: () => normalizePanoPosition,
2298
2305
  normalizePanoYaw: () => normalizePanoYaw,
2299
2306
  panoPositionToVector3: () => panoPositionToVector3,
2300
2307
  resolvePanoContextMenuEntries: () => resolvePanoContextMenuEntries,
2308
+ sectorRotation: () => sectorRotation,
2301
2309
  unwrapPolygonVertices: () => unwrapPolygonVertices,
2302
2310
  usePanoEvents: () => usePanoEvents,
2303
2311
  usePanoModal: () => usePanoModal,
@@ -9025,7 +9033,7 @@ var PanoViewer = (0, import_react29.forwardRef)(
9025
9033
  contextMenuOverlay,
9026
9034
  hotspotAccessibilityControls
9027
9035
  ] }),
9028
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PanoModalHostContext.Provider, { value: modalHost, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PanoOverlayContext.Provider, { value: overlayContext, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
9036
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PanoEventBusContext.Provider, { value: eventBus, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PanoModalHostContext.Provider, { value: modalHost, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PanoOverlayContext.Provider, { value: overlayContext, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
9029
9037
  "div",
9030
9038
  {
9031
9039
  "data-pano-overlay": "",
@@ -9033,7 +9041,7 @@ var PanoViewer = (0, import_react29.forwardRef)(
9033
9041
  style: { position: "absolute", inset: 0, zIndex: 30, pointerEvents: "none" },
9034
9042
  children: renderOverlay?.(overlayContext)
9035
9043
  }
9036
- ) }) })
9044
+ ) }) }) })
9037
9045
  ]
9038
9046
  }
9039
9047
  );
@@ -19045,6 +19053,962 @@ function PanoVideo({
19045
19053
  captionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
19046
19054
  ] });
19047
19055
  }
19056
+
19057
+ // src/map/pano-map.tsx
19058
+ var import_react65 = require("react");
19059
+
19060
+ // src/map/geometry.ts
19061
+ function sectorRotation(heading, yaw) {
19062
+ return heading + yaw;
19063
+ }
19064
+ function horizontalFov(verticalFov, aspect) {
19065
+ if (!(aspect > 0) || !(verticalFov > 0) || verticalFov >= 180) return verticalFov;
19066
+ const verticalRad = verticalFov * Math.PI / 180;
19067
+ const horizontalRad = 2 * Math.atan(Math.tan(verticalRad / 2) * aspect);
19068
+ return horizontalRad * 180 / Math.PI;
19069
+ }
19070
+ function containRect(containerWidth, containerHeight, imageWidth, imageHeight) {
19071
+ if (!(containerWidth > 0) || !(containerHeight > 0) || !(imageWidth > 0) || !(imageHeight > 0)) {
19072
+ return { left: 0, top: 0, width: 0, height: 0 };
19073
+ }
19074
+ const scale = Math.min(
19075
+ containerWidth / imageWidth,
19076
+ containerHeight / imageHeight
19077
+ );
19078
+ const width = imageWidth * scale;
19079
+ const height = imageHeight * scale;
19080
+ return {
19081
+ left: (containerWidth - width) / 2,
19082
+ top: (containerHeight - height) / 2,
19083
+ width,
19084
+ height
19085
+ };
19086
+ }
19087
+ function imagePointToContainer(point, viewport) {
19088
+ const { fit, scale, panX, panY } = viewport;
19089
+ return {
19090
+ x: fit.left + panX + point.x * fit.width * scale,
19091
+ y: fit.top + panY + point.y * fit.height * scale
19092
+ };
19093
+ }
19094
+ function containerPointToImage(point, viewport) {
19095
+ const { fit, scale, panX, panY } = viewport;
19096
+ const width = fit.width * scale;
19097
+ const height = fit.height * scale;
19098
+ if (!(width > 0) || !(height > 0)) return null;
19099
+ return {
19100
+ x: (point.x - fit.left - panX) / width,
19101
+ y: (point.y - fit.top - panY) / height
19102
+ };
19103
+ }
19104
+ function clamp012(value) {
19105
+ return Math.min(1, Math.max(0, value));
19106
+ }
19107
+ function headingFromVector(dx, dy) {
19108
+ if (dx === 0 && dy === 0) return 0;
19109
+ const radians = Math.atan2(dx, -dy);
19110
+ return radians * 180 / Math.PI;
19111
+ }
19112
+ function normalizeDegrees(value) {
19113
+ const wrapped = (value % 360 + 360) % 360;
19114
+ return wrapped > 180 ? wrapped - 360 : wrapped;
19115
+ }
19116
+
19117
+ // src/map/use-map-view.ts
19118
+ var import_react64 = require("react");
19119
+ var DEFAULT_VIEW2 = { yaw: 0, pitch: 0, fov: 75 };
19120
+ function useMapView(controlled) {
19121
+ const eventBus = (0, import_react64.useContext)(PanoEventBusContext);
19122
+ const [view, setView] = (0, import_react64.useState)(controlled ?? DEFAULT_VIEW2);
19123
+ const controlledRef = (0, import_react64.useRef)(controlled);
19124
+ controlledRef.current = controlled;
19125
+ (0, import_react64.useEffect)(() => {
19126
+ if (controlled !== void 0) {
19127
+ setView(controlled);
19128
+ return;
19129
+ }
19130
+ if (!eventBus) return;
19131
+ return eventBus.subscribe("viewchange", (next) => {
19132
+ if (controlledRef.current !== void 0) return;
19133
+ setView(next);
19134
+ });
19135
+ }, [controlled, eventBus]);
19136
+ return controlled ?? view;
19137
+ }
19138
+
19139
+ // src/map/pano-map.tsx
19140
+ var import_jsx_runtime42 = require("react/jsx-runtime");
19141
+ var DEFAULT_ICON_SIZE2 = 28;
19142
+ var DEFAULT_SECTOR_COLOR = "#ffffff";
19143
+ var DEFAULT_SECTOR_OPACITY = 0.55;
19144
+ var MIN_SCALE = 1;
19145
+ var DEFAULT_MAX_SCALE = 8;
19146
+ var ZOOM_STEP = 1.25;
19147
+ function defaultGradientId(prefix) {
19148
+ return `${prefix}-sector-gradient`;
19149
+ }
19150
+ function SectorShape({
19151
+ sector,
19152
+ angle,
19153
+ rotation,
19154
+ gradientId,
19155
+ interactive,
19156
+ onHeadingPointerDown,
19157
+ transformRef
19158
+ }) {
19159
+ const half = angle / 2;
19160
+ const start = (-half - 90) * Math.PI / 180;
19161
+ const end = (half - 90) * Math.PI / 180;
19162
+ const radius = sector.radius;
19163
+ const largeArc = angle > 180 ? 1 : 0;
19164
+ const x1 = Math.cos(start) * radius;
19165
+ const y1 = Math.sin(start) * radius;
19166
+ const x2 = Math.cos(end) * radius;
19167
+ const y2 = Math.sin(end) * radius;
19168
+ const path = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
19169
+ const opacity = sector.opacity ?? DEFAULT_SECTOR_OPACITY;
19170
+ const color = sector.color ?? DEFAULT_SECTOR_COLOR;
19171
+ const gradient = sector.gradient;
19172
+ const strokeWidth = sector.strokeWidth ?? 0;
19173
+ const strokeColor = sector.strokeColor ?? color;
19174
+ const strokeOpacity = sector.strokeOpacity ?? 1;
19175
+ const gradientStrength = gradient && gradient.stops.length > 0 ? Math.max(
19176
+ ...gradient.stops.map((stop) => stop.opacity ?? 1),
19177
+ Number.EPSILON
19178
+ ) : 1;
19179
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("g", { ref: transformRef, transform: `rotate(${rotation})`, style: { pointerEvents: "none" }, children: [
19180
+ gradient ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("defs", { children: gradient.type === "radial" ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("radialGradient", { id: gradientId, cx: "0", cy: "0", r: radius, gradientUnits: "userSpaceOnUse", children: gradient.stops.map((stop, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19181
+ "stop",
19182
+ {
19183
+ offset: `${stop.offset * 100}%`,
19184
+ stopColor: stop.color,
19185
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
19186
+ },
19187
+ `${stop.offset}:${index}`
19188
+ )) }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19189
+ "linearGradient",
19190
+ {
19191
+ id: gradientId,
19192
+ x1: "0",
19193
+ y1: "0",
19194
+ x2: "0",
19195
+ y2: -radius,
19196
+ gradientUnits: "userSpaceOnUse",
19197
+ children: gradient.stops.map((stop, index) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19198
+ "stop",
19199
+ {
19200
+ offset: `${stop.offset * 100}%`,
19201
+ stopColor: stop.color,
19202
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
19203
+ },
19204
+ `${stop.offset}:${index}`
19205
+ ))
19206
+ }
19207
+ ) }) : null,
19208
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19209
+ "path",
19210
+ {
19211
+ d: path,
19212
+ fill: gradient ? `url(#${gradientId})` : color,
19213
+ fillOpacity: opacity,
19214
+ stroke: strokeWidth > 0 ? strokeColor : "none",
19215
+ strokeWidth: strokeWidth > 0 ? strokeWidth : void 0,
19216
+ strokeOpacity: strokeWidth > 0 ? strokeOpacity : void 0,
19217
+ vectorEffect: "non-scaling-stroke",
19218
+ style: {
19219
+ pointerEvents: interactive ? "auto" : "none",
19220
+ cursor: interactive ? "grab" : void 0
19221
+ },
19222
+ onPointerDown: interactive ? onHeadingPointerDown : void 0
19223
+ }
19224
+ )
19225
+ ] });
19226
+ }
19227
+ function DefaultSpotIcon({ active, size }) {
19228
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
19229
+ "svg",
19230
+ {
19231
+ width: size,
19232
+ height: size,
19233
+ viewBox: "0 0 32 32",
19234
+ "aria-hidden": "true",
19235
+ style: { display: "block" },
19236
+ children: [
19237
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19238
+ "rect",
19239
+ {
19240
+ x: "3",
19241
+ y: "7",
19242
+ width: "26",
19243
+ height: "18",
19244
+ rx: "5",
19245
+ fill: active ? "rgba(15,23,42,0.92)" : "rgba(15,23,42,0.72)",
19246
+ stroke: "rgba(255,255,255,0.92)",
19247
+ strokeWidth: "2"
19248
+ }
19249
+ ),
19250
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("circle", { cx: "16", cy: "16", r: "5", fill: "none", stroke: "rgba(255,255,255,0.92)", strokeWidth: "2" }),
19251
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("circle", { cx: "16", cy: "16", r: "2", fill: active ? "#ef4444" : "rgba(255,255,255,0.85)" })
19252
+ ]
19253
+ }
19254
+ );
19255
+ }
19256
+ function PanoMap({
19257
+ map,
19258
+ activeSceneId,
19259
+ onSelectScene,
19260
+ view: controlledView,
19261
+ mode = "view",
19262
+ selectedSpotId = null,
19263
+ onSelectedSpotIdChange,
19264
+ onEditChange,
19265
+ placementSceneId,
19266
+ className,
19267
+ style,
19268
+ showZoomControls,
19269
+ showCollapseControl,
19270
+ collapsed: controlledCollapsed,
19271
+ onCollapsedChange,
19272
+ renderChrome
19273
+ }) {
19274
+ const reactId = (0, import_react65.useId)().replace(/:/g, "");
19275
+ const view = useMapView(controlledView);
19276
+ const rootRef = (0, import_react65.useRef)(null);
19277
+ const containerRef = (0, import_react65.useRef)(null);
19278
+ const sectorNodeRef = (0, import_react65.useRef)(null);
19279
+ const yawRef = (0, import_react65.useRef)(view.yaw);
19280
+ const [size, setSize] = (0, import_react65.useState)({ width: 0, height: 0 });
19281
+ const [imageSize2, setImageSize] = (0, import_react65.useState)({ width: 0, height: 0 });
19282
+ const [scale, setScale] = (0, import_react65.useState)(1);
19283
+ const [pan, setPan] = (0, import_react65.useState)({ x: 0, y: 0 });
19284
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react65.useState)(false);
19285
+ const dragRef = (0, import_react65.useRef)(null);
19286
+ const spots = map.spots;
19287
+ const iconSize = map.iconSize ?? DEFAULT_ICON_SIZE2;
19288
+ const interaction = map.interaction ?? "fixed";
19289
+ const gestureZoom = interaction === "zoom";
19290
+ const maxScale = Math.max(MIN_SCALE, map.maxScale ?? DEFAULT_MAX_SCALE);
19291
+ const zoomControls = gestureZoom && (showZoomControls ?? map.controls?.zoom === true);
19292
+ const collapseControl = showCollapseControl ?? map.controls?.collapse === true;
19293
+ const collapsed = controlledCollapsed ?? uncontrolledCollapsed;
19294
+ const editing = mode === "edit";
19295
+ const fit = (0, import_react65.useMemo)(
19296
+ () => containRect(size.width, size.height, imageSize2.width, imageSize2.height),
19297
+ [size, imageSize2]
19298
+ );
19299
+ const viewport = (0, import_react65.useMemo)(
19300
+ () => ({ fit, scale, panX: pan.x, panY: pan.y }),
19301
+ [fit, scale, pan]
19302
+ );
19303
+ (0, import_react65.useLayoutEffect)(() => {
19304
+ if (collapsed) return;
19305
+ const node = containerRef.current;
19306
+ if (!node) return;
19307
+ const measure = () => {
19308
+ const width = node.clientWidth;
19309
+ const height = node.clientHeight;
19310
+ if (!(width > 0) || !(height > 0)) return;
19311
+ setSize(
19312
+ (previous) => previous.width === width && previous.height === height ? previous : { width, height }
19313
+ );
19314
+ };
19315
+ measure();
19316
+ const observer = new ResizeObserver(measure);
19317
+ observer.observe(node);
19318
+ return () => observer.disconnect();
19319
+ }, [collapsed]);
19320
+ (0, import_react65.useEffect)(() => {
19321
+ yawRef.current = view.yaw;
19322
+ const node = sectorNodeRef.current;
19323
+ if (!node) return;
19324
+ const active = spots.find((spot) => spot.sceneId === activeSceneId);
19325
+ if (!active || !map.sector) return;
19326
+ node.setAttribute(
19327
+ "transform",
19328
+ `rotate(${sectorRotation(active.heading, view.yaw)})`
19329
+ );
19330
+ }, [view.yaw, activeSceneId, spots, map.sector]);
19331
+ const setCollapsed = (next) => {
19332
+ onCollapsedChange?.(next);
19333
+ if (controlledCollapsed === void 0) setUncontrolledCollapsed(next);
19334
+ };
19335
+ const clampPan = (0, import_react65.useCallback)(
19336
+ (nextScale, nextPan) => {
19337
+ const width = fit.width * nextScale;
19338
+ const height = fit.height * nextScale;
19339
+ if (!(fit.width > 0) || !(fit.height > 0)) return nextPan;
19340
+ const minX = Math.min(0, size.width - (fit.left + width));
19341
+ const maxX = Math.max(0, -fit.left);
19342
+ const minY = Math.min(0, size.height - (fit.top + height));
19343
+ const maxY = Math.max(0, -fit.top);
19344
+ return {
19345
+ x: Math.min(maxX, Math.max(minX, nextPan.x)),
19346
+ y: Math.min(maxY, Math.max(minY, nextPan.y))
19347
+ };
19348
+ },
19349
+ [fit, size]
19350
+ );
19351
+ (0, import_react65.useEffect)(() => {
19352
+ if (!gestureZoom) {
19353
+ setScale(1);
19354
+ setPan({ x: 0, y: 0 });
19355
+ return;
19356
+ }
19357
+ setScale((previous) => {
19358
+ const next = Math.min(previous, maxScale);
19359
+ if (next !== previous) {
19360
+ setPan((currentPan) => clampPan(next, currentPan));
19361
+ }
19362
+ return next;
19363
+ });
19364
+ }, [clampPan, gestureZoom, maxScale]);
19365
+ const applyZoom = (0, import_react65.useCallback)(
19366
+ (nextScale, origin) => {
19367
+ if (!gestureZoom) return;
19368
+ const clamped = Math.min(maxScale, Math.max(MIN_SCALE, nextScale));
19369
+ const pivot = origin ?? { x: size.width / 2, y: size.height / 2 };
19370
+ const ratio = clamped / scale;
19371
+ const nextPan = {
19372
+ x: pivot.x - (pivot.x - (fit.left + pan.x)) * ratio - fit.left,
19373
+ y: pivot.y - (pivot.y - (fit.top + pan.y)) * ratio - fit.top
19374
+ };
19375
+ setScale(clamped);
19376
+ setPan(clampPan(clamped, nextPan));
19377
+ },
19378
+ [
19379
+ clampPan,
19380
+ fit.left,
19381
+ fit.top,
19382
+ gestureZoom,
19383
+ maxScale,
19384
+ pan.x,
19385
+ pan.y,
19386
+ scale,
19387
+ size.height,
19388
+ size.width
19389
+ ]
19390
+ );
19391
+ (0, import_react65.useEffect)(() => {
19392
+ const root = rootRef.current;
19393
+ if (!root) return;
19394
+ const onWheel = (event) => {
19395
+ event.preventDefault();
19396
+ event.stopPropagation();
19397
+ if (!gestureZoom || collapsed) return;
19398
+ const viewport2 = containerRef.current;
19399
+ if (!viewport2) return;
19400
+ const rect = viewport2.getBoundingClientRect();
19401
+ const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
19402
+ applyZoom(scale * factor, {
19403
+ x: event.clientX - rect.left,
19404
+ y: event.clientY - rect.top
19405
+ });
19406
+ };
19407
+ root.addEventListener("wheel", onWheel, { passive: false });
19408
+ return () => root.removeEventListener("wheel", onWheel);
19409
+ }, [applyZoom, collapsed, gestureZoom, scale]);
19410
+ const updateSpots = (next, selected) => {
19411
+ onEditChange?.({
19412
+ spots: next,
19413
+ ...selected === void 0 ? {} : { selectedSpotId: selected }
19414
+ });
19415
+ };
19416
+ const localPoint = (event) => {
19417
+ const rect = containerRef.current?.getBoundingClientRect();
19418
+ if (!rect) return null;
19419
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
19420
+ };
19421
+ const onPointerDownSurface = (event) => {
19422
+ if (collapsed) return;
19423
+ if (event.button !== 0) return;
19424
+ const point = localPoint(event);
19425
+ if (!point) return;
19426
+ if (editing && placementSceneId) {
19427
+ const image = containerPointToImage(point, viewport);
19428
+ if (!image) return;
19429
+ const spot = {
19430
+ id: crypto.randomUUID(),
19431
+ sceneId: placementSceneId,
19432
+ x: clamp012(image.x),
19433
+ y: clamp012(image.y),
19434
+ heading: normalizeDegrees(-view.yaw)
19435
+ };
19436
+ updateSpots([...spots, spot], spot.id);
19437
+ onSelectedSpotIdChange?.(spot.id);
19438
+ event.preventDefault();
19439
+ event.stopPropagation();
19440
+ return;
19441
+ }
19442
+ if (gestureZoom && scale > 1) {
19443
+ dragRef.current = {
19444
+ kind: "pan",
19445
+ pointerId: event.pointerId,
19446
+ originX: point.x,
19447
+ originY: point.y,
19448
+ panX: pan.x,
19449
+ panY: pan.y
19450
+ };
19451
+ event.currentTarget.setPointerCapture(event.pointerId);
19452
+ event.preventDefault();
19453
+ event.stopPropagation();
19454
+ }
19455
+ };
19456
+ const onPointerMoveSurface = (event) => {
19457
+ const drag = dragRef.current;
19458
+ if (!drag || drag.pointerId !== event.pointerId) return;
19459
+ const point = localPoint(event);
19460
+ if (!point) return;
19461
+ if (drag.kind === "pan") {
19462
+ setPan(
19463
+ clampPan(scale, {
19464
+ x: drag.panX + (point.x - drag.originX),
19465
+ y: drag.panY + (point.y - drag.originY)
19466
+ })
19467
+ );
19468
+ event.preventDefault();
19469
+ event.stopPropagation();
19470
+ return;
19471
+ }
19472
+ if (drag.kind === "spot") {
19473
+ const image = containerPointToImage(
19474
+ { x: point.x - drag.offsetX, y: point.y - drag.offsetY },
19475
+ viewport
19476
+ );
19477
+ if (!image) return;
19478
+ updateSpots(
19479
+ spots.map(
19480
+ (spot2) => spot2.id === drag.spotId ? { ...spot2, x: clamp012(image.x), y: clamp012(image.y) } : spot2
19481
+ )
19482
+ );
19483
+ event.preventDefault();
19484
+ event.stopPropagation();
19485
+ return;
19486
+ }
19487
+ const spot = spots.find((entry) => entry.id === drag.spotId);
19488
+ if (!spot) return;
19489
+ const center = imagePointToContainer(spot, viewport);
19490
+ const heading = normalizeDegrees(
19491
+ headingFromVector(point.x - center.x, point.y - center.y) - view.yaw
19492
+ );
19493
+ updateSpots(
19494
+ spots.map(
19495
+ (entry) => entry.id === drag.spotId ? { ...entry, heading } : entry
19496
+ )
19497
+ );
19498
+ if (sectorNodeRef.current && spot.sceneId === activeSceneId) {
19499
+ sectorNodeRef.current.setAttribute(
19500
+ "transform",
19501
+ `rotate(${sectorRotation(heading, view.yaw)})`
19502
+ );
19503
+ }
19504
+ event.preventDefault();
19505
+ event.stopPropagation();
19506
+ };
19507
+ const endDrag = (event) => {
19508
+ if (dragRef.current?.pointerId !== event.pointerId) return;
19509
+ dragRef.current = null;
19510
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
19511
+ event.currentTarget.releasePointerCapture(event.pointerId);
19512
+ }
19513
+ };
19514
+ const sectorAngle = (() => {
19515
+ if (!map.sector) return 0;
19516
+ if (!map.sector.followFov) return map.sector.angle;
19517
+ const aspect = size.width > 0 && size.height > 0 ? size.width / size.height : 1;
19518
+ return horizontalFov(view.fov, aspect);
19519
+ })();
19520
+ const activeSpot = spots.find((spot) => spot.sceneId === activeSceneId);
19521
+ const selectedSpot = spots.find((spot) => spot.id === selectedSpotId);
19522
+ const controls = /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "pano-map-controls", style: { pointerEvents: "auto" }, children: [
19523
+ collapseControl ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19524
+ "button",
19525
+ {
19526
+ type: "button",
19527
+ className: "pano-map-control",
19528
+ "aria-label": collapsed ? "Expand map" : "Collapse map",
19529
+ "aria-pressed": !collapsed,
19530
+ style: {
19531
+ background: "rgba(15, 23, 42, 0.92)",
19532
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19533
+ color: "#f8fafc"
19534
+ },
19535
+ onClick: () => setCollapsed(!collapsed),
19536
+ children: collapsed ? "\u25A3" : "\u25A2"
19537
+ }
19538
+ ) : null,
19539
+ zoomControls && !collapsed ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
19540
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19541
+ "button",
19542
+ {
19543
+ type: "button",
19544
+ className: "pano-map-control",
19545
+ "aria-label": "Zoom in",
19546
+ style: {
19547
+ background: "rgba(15, 23, 42, 0.92)",
19548
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19549
+ color: "#f8fafc"
19550
+ },
19551
+ onClick: () => applyZoom(scale * ZOOM_STEP),
19552
+ children: "+"
19553
+ }
19554
+ ),
19555
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19556
+ "button",
19557
+ {
19558
+ type: "button",
19559
+ className: "pano-map-control",
19560
+ "aria-label": "Zoom out",
19561
+ style: {
19562
+ background: "rgba(15, 23, 42, 0.92)",
19563
+ border: "1px solid rgba(255, 255, 255, 0.28)",
19564
+ color: "#f8fafc"
19565
+ },
19566
+ onClick: () => applyZoom(scale / ZOOM_STEP),
19567
+ children: "\u2212"
19568
+ }
19569
+ )
19570
+ ] }) : null
19571
+ ] });
19572
+ const syncImageSize = (image) => {
19573
+ if (!(image.naturalWidth > 0) || !(image.naturalHeight > 0)) return;
19574
+ setImageSize(
19575
+ (previous) => previous.width === image.naturalWidth && previous.height === image.naturalHeight ? previous : { width: image.naturalWidth, height: image.naturalHeight }
19576
+ );
19577
+ };
19578
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
19579
+ "div",
19580
+ {
19581
+ ref: rootRef,
19582
+ className: ["pano-map", collapsed ? "pano-map-collapsed" : "", className].filter(Boolean).join(" "),
19583
+ style: {
19584
+ position: "relative",
19585
+ overflow: "hidden",
19586
+ width: "100%",
19587
+ height: "100%",
19588
+ pointerEvents: "auto",
19589
+ touchAction: gestureZoom && !collapsed ? "none" : "auto",
19590
+ ...style
19591
+ },
19592
+ "data-pano-map": "",
19593
+ "data-mode": mode,
19594
+ "data-collapsed": collapsed ? "true" : void 0,
19595
+ children: [
19596
+ renderChrome ? renderChrome(controls) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "pano-map-chrome", children: controls }),
19597
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
19598
+ "div",
19599
+ {
19600
+ ref: containerRef,
19601
+ className: "pano-map-viewport",
19602
+ hidden: collapsed,
19603
+ style: {
19604
+ position: "absolute",
19605
+ inset: 0,
19606
+ overflow: "hidden",
19607
+ visibility: collapsed ? "hidden" : "visible"
19608
+ },
19609
+ onPointerDown: onPointerDownSurface,
19610
+ onPointerMove: onPointerMoveSurface,
19611
+ onPointerUp: endDrag,
19612
+ onPointerCancel: endDrag,
19613
+ children: [
19614
+ fit.width > 0 && fit.height > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19615
+ "div",
19616
+ {
19617
+ className: "pano-map-image-layer",
19618
+ style: {
19619
+ position: "absolute",
19620
+ left: fit.left + pan.x,
19621
+ top: fit.top + pan.y,
19622
+ width: fit.width * scale,
19623
+ height: fit.height * scale,
19624
+ transformOrigin: "0 0"
19625
+ },
19626
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19627
+ "img",
19628
+ {
19629
+ src: map.image,
19630
+ alt: "",
19631
+ draggable: false,
19632
+ ref: (node) => {
19633
+ if (node) syncImageSize(node);
19634
+ },
19635
+ onLoad: (event) => {
19636
+ syncImageSize(event.currentTarget);
19637
+ },
19638
+ style: {
19639
+ width: "100%",
19640
+ height: "100%",
19641
+ display: "block",
19642
+ userSelect: "none",
19643
+ pointerEvents: "none"
19644
+ }
19645
+ }
19646
+ )
19647
+ }
19648
+ ) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19649
+ "img",
19650
+ {
19651
+ src: map.image,
19652
+ alt: "",
19653
+ hidden: true,
19654
+ ref: (node) => {
19655
+ if (node) syncImageSize(node);
19656
+ },
19657
+ onLoad: (event) => {
19658
+ syncImageSize(event.currentTarget);
19659
+ }
19660
+ }
19661
+ ),
19662
+ activeSpot && map.sector ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19663
+ "svg",
19664
+ {
19665
+ className: "pano-map-sector-layer",
19666
+ width: size.width,
19667
+ height: size.height,
19668
+ style: {
19669
+ position: "absolute",
19670
+ inset: 0,
19671
+ overflow: "visible",
19672
+ pointerEvents: "none"
19673
+ },
19674
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19675
+ "g",
19676
+ {
19677
+ transform: `translate(${imagePointToContainer(activeSpot, viewport).x} ${imagePointToContainer(activeSpot, viewport).y})`,
19678
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19679
+ SectorShape,
19680
+ {
19681
+ sector: map.sector,
19682
+ angle: sectorAngle,
19683
+ rotation: sectorRotation(activeSpot.heading, view.yaw),
19684
+ gradientId: defaultGradientId(reactId),
19685
+ interactive: editing,
19686
+ onHeadingPointerDown: (event) => {
19687
+ event.stopPropagation();
19688
+ if (event.button !== 0) return;
19689
+ onSelectedSpotIdChange?.(activeSpot.id);
19690
+ dragRef.current = {
19691
+ kind: "heading",
19692
+ pointerId: event.pointerId,
19693
+ spotId: activeSpot.id
19694
+ };
19695
+ event.currentTarget.setPointerCapture(event.pointerId);
19696
+ },
19697
+ transformRef: (node) => {
19698
+ sectorNodeRef.current = node;
19699
+ }
19700
+ }
19701
+ )
19702
+ }
19703
+ )
19704
+ }
19705
+ ) : null,
19706
+ spots.map((spot) => {
19707
+ const point = imagePointToContainer(spot, viewport);
19708
+ const active = spot.sceneId === activeSceneId;
19709
+ const selected = spot.id === selectedSpotId;
19710
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19711
+ "button",
19712
+ {
19713
+ type: "button",
19714
+ className: "pano-map-spot",
19715
+ "aria-label": spot.sceneId,
19716
+ "aria-current": active ? "true" : void 0,
19717
+ "data-active": active ? "true" : void 0,
19718
+ "data-selected": selected ? "true" : void 0,
19719
+ style: {
19720
+ position: "absolute",
19721
+ left: point.x,
19722
+ top: point.y,
19723
+ width: iconSize,
19724
+ height: iconSize,
19725
+ transform: "translate(-50%, -50%)",
19726
+ padding: 0,
19727
+ border: selected ? "2px solid #38bdf8" : "0",
19728
+ borderRadius: 8,
19729
+ background: "transparent",
19730
+ cursor: editing ? "grab" : "pointer",
19731
+ pointerEvents: "auto"
19732
+ },
19733
+ onPointerDown: (event) => {
19734
+ event.stopPropagation();
19735
+ if (event.button !== 0) return;
19736
+ onSelectedSpotIdChange?.(spot.id);
19737
+ if (!editing) return;
19738
+ const pointLocal = localPoint(event);
19739
+ if (!pointLocal) return;
19740
+ const center = imagePointToContainer(spot, viewport);
19741
+ dragRef.current = {
19742
+ kind: "spot",
19743
+ pointerId: event.pointerId,
19744
+ spotId: spot.id,
19745
+ offsetX: pointLocal.x - center.x,
19746
+ offsetY: pointLocal.y - center.y
19747
+ };
19748
+ event.currentTarget.setPointerCapture(event.pointerId);
19749
+ },
19750
+ onClick: (event) => {
19751
+ event.stopPropagation();
19752
+ if (editing) return;
19753
+ onSelectScene?.(spot.sceneId, spot);
19754
+ },
19755
+ children: spot.icon ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19756
+ "img",
19757
+ {
19758
+ src: spot.icon,
19759
+ alt: "",
19760
+ draggable: false,
19761
+ style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" }
19762
+ }
19763
+ ) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(DefaultSpotIcon, { active, size: iconSize })
19764
+ },
19765
+ spot.id
19766
+ );
19767
+ })
19768
+ ]
19769
+ }
19770
+ )
19771
+ ]
19772
+ }
19773
+ );
19774
+ }
19775
+
19776
+ // src/map/pano-map-overlay.tsx
19777
+ var import_react66 = require("react");
19778
+ var import_jsx_runtime43 = require("react/jsx-runtime");
19779
+ function dockedBox(corner, offsetX, offsetY, width, height) {
19780
+ const horizontal = corner === "top-right" || corner === "bottom-right" ? { left: "auto", right: offsetX } : { left: offsetX, right: "auto" };
19781
+ const vertical = corner === "bottom-left" || corner === "bottom-right" ? { top: "auto", bottom: offsetY } : { top: offsetY, bottom: "auto" };
19782
+ return { ...horizontal, ...vertical, width, height };
19783
+ }
19784
+ function PanoMapDialogShell({
19785
+ open,
19786
+ onOpenChange,
19787
+ title,
19788
+ closeLabel,
19789
+ children
19790
+ }) {
19791
+ usePanoModal(open);
19792
+ const { size } = usePanoOverlay();
19793
+ const titleId = (0, import_react66.useId)();
19794
+ const panelRef = (0, import_react66.useRef)(null);
19795
+ const previouslyFocused = (0, import_react66.useRef)(null);
19796
+ (0, import_react66.useEffect)(() => {
19797
+ if (!open) return;
19798
+ const active = panelRef.current?.ownerDocument.activeElement;
19799
+ previouslyFocused.current = active instanceof HTMLElement ? active : null;
19800
+ const close = panelRef.current?.querySelector("[data-pano-map-dialog-close]");
19801
+ close?.focus({ preventScroll: true });
19802
+ const onKeyDown = (event) => {
19803
+ if (event.key === "Escape") {
19804
+ event.preventDefault();
19805
+ event.stopPropagation();
19806
+ onOpenChange(false);
19807
+ }
19808
+ };
19809
+ const doc = panelRef.current?.ownerDocument;
19810
+ doc?.addEventListener("keydown", onKeyDown, true);
19811
+ return () => {
19812
+ doc?.removeEventListener("keydown", onKeyDown, true);
19813
+ const previous = previouslyFocused.current;
19814
+ if (previous?.isConnected) previous.focus({ preventScroll: true });
19815
+ };
19816
+ }, [open, onOpenChange]);
19817
+ if (!open) return null;
19818
+ const width = Math.min(720, Math.max(0, size.width - 32));
19819
+ const height = Math.min(520, Math.max(0, size.height - 32));
19820
+ const left = Math.max(0, (size.width - width) / 2);
19821
+ const top = Math.max(0, (size.height - height) / 2);
19822
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
19823
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19824
+ "div",
19825
+ {
19826
+ className: "pano-map-dialog-backdrop",
19827
+ "aria-hidden": "true",
19828
+ style: {
19829
+ position: "absolute",
19830
+ inset: 0,
19831
+ pointerEvents: "auto"
19832
+ },
19833
+ onClick: () => onOpenChange(false)
19834
+ }
19835
+ ),
19836
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
19837
+ "div",
19838
+ {
19839
+ ref: panelRef,
19840
+ className: "pano-map-dialog",
19841
+ role: "dialog",
19842
+ "aria-modal": "true",
19843
+ "aria-labelledby": titleId,
19844
+ "data-pano-map-dialog": "",
19845
+ style: {
19846
+ position: "absolute",
19847
+ left,
19848
+ top,
19849
+ width,
19850
+ height,
19851
+ boxSizing: "border-box",
19852
+ display: "flex",
19853
+ flexDirection: "column",
19854
+ overflow: "hidden",
19855
+ pointerEvents: "auto",
19856
+ zIndex: 1
19857
+ },
19858
+ children: [
19859
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("header", { className: "pano-map-dialog-header", style: { flexShrink: 0 }, children: [
19860
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("h2", { id: titleId, className: "pano-map-dialog-title", children: title }),
19861
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19862
+ "button",
19863
+ {
19864
+ type: "button",
19865
+ className: "pano-map-dialog-close",
19866
+ "data-pano-map-dialog-close": "",
19867
+ "aria-label": closeLabel,
19868
+ onClick: () => onOpenChange(false),
19869
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("path", { d: "m4 4 8 8M12 4l-8 8" }) })
19870
+ }
19871
+ )
19872
+ ] }),
19873
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "pano-map-dialog-body", style: { minHeight: 0, flex: 1 }, children })
19874
+ ]
19875
+ }
19876
+ )
19877
+ ] });
19878
+ }
19879
+ function PanoMapOverlay({
19880
+ map,
19881
+ activeSceneId,
19882
+ onSelectScene,
19883
+ view,
19884
+ mode = "view",
19885
+ selectedSpotId,
19886
+ onSelectedSpotIdChange,
19887
+ onEditChange,
19888
+ placementSceneId,
19889
+ presentationOverride,
19890
+ openLabel = "Map",
19891
+ closeLabel = "Close",
19892
+ title = "Map",
19893
+ className,
19894
+ style,
19895
+ editorSurface = false
19896
+ }) {
19897
+ const { size } = usePanoOverlay();
19898
+ const [open, setOpen] = (0, import_react66.useState)(false);
19899
+ const [collapsed, setCollapsed] = (0, import_react66.useState)(false);
19900
+ const presentation = presentationOverride ?? map.presentation ?? {
19901
+ mode: "docked",
19902
+ corner: "bottom-left",
19903
+ width: 220,
19904
+ height: 160,
19905
+ offsetX: 16,
19906
+ offsetY: 16
19907
+ };
19908
+ const corner = presentation.corner ?? "bottom-left";
19909
+ const offsetX = presentation.offsetX ?? 16;
19910
+ const offsetY = presentation.offsetY ?? 16;
19911
+ const width = presentation.width ?? (presentation.mode === "dialog" ? 48 : 220);
19912
+ const height = presentation.height ?? (presentation.mode === "dialog" ? 48 : 160);
19913
+ const mapProps = {
19914
+ map,
19915
+ activeSceneId,
19916
+ view,
19917
+ mode,
19918
+ selectedSpotId,
19919
+ onSelectedSpotIdChange,
19920
+ onEditChange,
19921
+ placementSceneId,
19922
+ onSelectScene: (sceneId, spot) => {
19923
+ onSelectScene?.(sceneId, spot);
19924
+ if (presentation.mode === "dialog") setOpen(false);
19925
+ }
19926
+ };
19927
+ const editorBox = (0, import_react66.useMemo)(() => {
19928
+ const panelWidth2 = Math.min(420, Math.max(240, size.width * 0.45));
19929
+ const panelHeight2 = Math.min(320, Math.max(180, size.height * 0.45));
19930
+ return {
19931
+ left: 16,
19932
+ right: "auto",
19933
+ top: "auto",
19934
+ bottom: 16,
19935
+ width: panelWidth2,
19936
+ height: panelHeight2
19937
+ };
19938
+ }, [size.height, size.width]);
19939
+ if (editorSurface) {
19940
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19941
+ "div",
19942
+ {
19943
+ className: ["pano-map-overlay", "pano-map-overlay-editor", className].filter(Boolean).join(" "),
19944
+ style: {
19945
+ position: "absolute",
19946
+ pointerEvents: "auto",
19947
+ ...editorBox,
19948
+ ...style
19949
+ },
19950
+ "data-pano-map-overlay": "",
19951
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PanoMap, { ...mapProps, showCollapseControl: false })
19952
+ }
19953
+ );
19954
+ }
19955
+ if (presentation.mode === "dialog") {
19956
+ const launcherBox = dockedBox(corner, offsetX, offsetY, width, height);
19957
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: ["pano-map-overlay", className].filter(Boolean).join(" "), "data-pano-map-overlay": "", children: [
19958
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19959
+ "button",
19960
+ {
19961
+ type: "button",
19962
+ className: "pano-map-launcher",
19963
+ "aria-label": openLabel,
19964
+ style: {
19965
+ position: "absolute",
19966
+ pointerEvents: "auto",
19967
+ ...launcherBox,
19968
+ ...style
19969
+ },
19970
+ onClick: () => setOpen(true),
19971
+ children: openLabel
19972
+ }
19973
+ ),
19974
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19975
+ PanoMapDialogShell,
19976
+ {
19977
+ open,
19978
+ onOpenChange: setOpen,
19979
+ title,
19980
+ closeLabel,
19981
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { style: { height: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(PanoMap, { ...mapProps, showCollapseControl: false }) })
19982
+ }
19983
+ )
19984
+ ] });
19985
+ }
19986
+ const panelWidth = collapsed ? Math.max(44, Math.min(width, 56)) : width;
19987
+ const panelHeight = collapsed ? Math.max(44, Math.min(height, 56)) : height;
19988
+ const box = dockedBox(corner, offsetX, offsetY, panelWidth, panelHeight);
19989
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
19990
+ "div",
19991
+ {
19992
+ className: ["pano-map-overlay", className].filter(Boolean).join(" "),
19993
+ style: {
19994
+ position: "absolute",
19995
+ pointerEvents: "auto",
19996
+ ...box,
19997
+ ...style
19998
+ },
19999
+ "data-pano-map-overlay": "",
20000
+ "data-presentation": "docked",
20001
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20002
+ PanoMap,
20003
+ {
20004
+ ...mapProps,
20005
+ collapsed,
20006
+ onCollapsedChange: setCollapsed
20007
+ }
20008
+ )
20009
+ }
20010
+ );
20011
+ }
19048
20012
  // Annotate the CommonJS export names for ESM import in node:
19049
20013
  0 && (module.exports = {
19050
20014
  AudioHotspot,
@@ -19070,6 +20034,8 @@ function PanoVideo({
19070
20034
  PanoContextMenu,
19071
20035
  PanoEvents,
19072
20036
  PanoFilter,
20037
+ PanoMap,
20038
+ PanoMapOverlay,
19073
20039
  PanoOverlayPortalProvider,
19074
20040
  PanoVideo,
19075
20041
  PanoVideoControls,
@@ -19090,13 +20056,19 @@ function PanoVideo({
19090
20056
  WebVR,
19091
20057
  clampPanoPitch,
19092
20058
  composePanoContextMenuItems,
20059
+ containRect,
20060
+ containerPointToImage,
19093
20061
  createDefaultPanoContextMenuItems,
19094
20062
  createPanoContextMenuPresets,
19095
20063
  cycleSceneId,
20064
+ headingFromVector,
20065
+ horizontalFov,
20066
+ imagePointToContainer,
19096
20067
  normalizePanoPosition,
19097
20068
  normalizePanoYaw,
19098
20069
  panoPositionToVector3,
19099
20070
  resolvePanoContextMenuEntries,
20071
+ sectorRotation,
19100
20072
  unwrapPolygonVertices,
19101
20073
  usePanoEvents,
19102
20074
  usePanoModal,