@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.cjs CHANGED
@@ -2254,6 +2254,7 @@ __export(index_exports, {
2254
2254
  DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE: () => DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE,
2255
2255
  DEFAULT_HOTSPOT_TOOLTIP_OFFSET: () => DEFAULT_HOTSPOT_TOOLTIP_OFFSET,
2256
2256
  DEFAULT_PANO_CURSORS: () => DEFAULT_PANO_CURSORS,
2257
+ DEFAULT_PANO_SCENE_MENU_APPEARANCE: () => DEFAULT_PANO_SCENE_MENU_APPEARANCE,
2257
2258
  DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE: () => DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
2258
2259
  DEFAULT_PANO_VIDEO_CONTROLS_APPEARANCE: () => DEFAULT_PANO_VIDEO_CONTROLS_APPEARANCE,
2259
2260
  DEFAULT_PANO_VIDEO_PLAYBACK_RATES: () => DEFAULT_PANO_VIDEO_PLAYBACK_RATES,
@@ -2271,7 +2272,10 @@ __export(index_exports, {
2271
2272
  PanoContextMenu: () => PanoContextMenu,
2272
2273
  PanoEvents: () => PanoEvents,
2273
2274
  PanoFilter: () => PanoFilter,
2275
+ PanoMap: () => PanoMap,
2276
+ PanoMapOverlay: () => PanoMapOverlay,
2274
2277
  PanoOverlayPortalProvider: () => PanoOverlayPortalProvider,
2278
+ PanoSceneMenu: () => PanoSceneMenu,
2275
2279
  PanoVideo: () => PanoVideo,
2276
2280
  PanoVideoControls: () => PanoVideoControls,
2277
2281
  PanoViewer: () => PanoViewer,
@@ -2291,13 +2295,19 @@ __export(index_exports, {
2291
2295
  WebVR: () => WebVR,
2292
2296
  clampPanoPitch: () => clampPanoPitch,
2293
2297
  composePanoContextMenuItems: () => composePanoContextMenuItems,
2298
+ containRect: () => containRect,
2299
+ containerPointToImage: () => containerPointToImage,
2294
2300
  createDefaultPanoContextMenuItems: () => createDefaultPanoContextMenuItems,
2295
2301
  createPanoContextMenuPresets: () => createPanoContextMenuPresets,
2296
2302
  cycleSceneId: () => cycleSceneId,
2303
+ headingFromVector: () => headingFromVector,
2304
+ horizontalFov: () => horizontalFov,
2305
+ imagePointToContainer: () => imagePointToContainer,
2297
2306
  normalizePanoPosition: () => normalizePanoPosition,
2298
2307
  normalizePanoYaw: () => normalizePanoYaw,
2299
2308
  panoPositionToVector3: () => panoPositionToVector3,
2300
2309
  resolvePanoContextMenuEntries: () => resolvePanoContextMenuEntries,
2310
+ sectorRotation: () => sectorRotation,
2301
2311
  unwrapPolygonVertices: () => unwrapPolygonVertices,
2302
2312
  usePanoEvents: () => usePanoEvents,
2303
2313
  usePanoModal: () => usePanoModal,
@@ -9025,7 +9035,7 @@ var PanoViewer = (0, import_react29.forwardRef)(
9025
9035
  contextMenuOverlay,
9026
9036
  hotspotAccessibilityControls
9027
9037
  ] }),
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)(
9038
+ /* @__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
9039
  "div",
9030
9040
  {
9031
9041
  "data-pano-overlay": "",
@@ -9033,7 +9043,7 @@ var PanoViewer = (0, import_react29.forwardRef)(
9033
9043
  style: { position: "absolute", inset: 0, zIndex: 30, pointerEvents: "none" },
9034
9044
  children: renderOverlay?.(overlayContext)
9035
9045
  }
9036
- ) }) })
9046
+ ) }) }) })
9037
9047
  ]
9038
9048
  }
9039
9049
  );
@@ -18232,197 +18242,1097 @@ var WebVR = (0, import_react61.forwardRef)(function WebVR2({
18232
18242
  ] });
18233
18243
  });
18234
18244
 
18235
- // src/use-pano-viewer.ts
18245
+ // src/pano-scene-menu.tsx
18236
18246
  var import_react62 = require("react");
18237
- function createPanoViewerHandle(ref) {
18247
+
18248
+ // src/drag-motion.ts
18249
+ var DRAG_INERTIA_DAMPING = 8;
18250
+ var DRAG_FRICTION_STOP = 24;
18251
+ var DRAG_SPRING_STRENGTH = 28;
18252
+ var DRAG_RUBBER_CONSTANT = 0.55;
18253
+ var DRAG_SPRING_SETTLE_DISTANCE = 0.5;
18254
+ var DRAG_SPRING_SETTLE_VELOCITY = 12;
18255
+ function resolveDragMotion(motion) {
18256
+ if (motion === false) {
18257
+ return { inertia: false, bounce: false };
18258
+ }
18238
18259
  return {
18239
- getView: () => ref.current?.getView() ?? { yaw: 0, pitch: 0, fov: 75 },
18240
- setView: (view, options) => {
18241
- ref.current?.setView(view, options);
18242
- },
18243
- reset: () => {
18244
- ref.current?.reset();
18245
- },
18246
- lookTo: (position, options) => ref.current?.lookTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
18247
- moveTo: (position, options) => ref.current?.moveTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
18248
- zoomTo: (fov, options) => ref.current?.zoomTo(fov, options) ?? Promise.resolve({ status: "cancelled" }),
18249
- lookToHotspot: (id, options) => ref.current?.lookToHotspot(id, options) ?? Promise.resolve({ status: "not-found" }),
18250
- activateMedia: () => ref.current?.activateMedia() ?? Promise.resolve(),
18251
- enterFullscreen: () => ref.current?.enterFullscreen() ?? Promise.resolve(),
18252
- exitFullscreen: () => ref.current?.exitFullscreen() ?? Promise.resolve(),
18253
- toggleFullscreen: () => ref.current?.toggleFullscreen() ?? Promise.resolve(),
18254
- isFullscreen: () => ref.current?.isFullscreen() ?? false,
18255
- setScene: (id) => ref.current?.setScene(id) ?? false,
18256
- nextScene: () => ref.current?.nextScene() ?? false,
18257
- previousScene: () => ref.current?.previousScene() ?? false,
18258
- getActiveSceneId: () => ref.current?.getActiveSceneId() ?? null,
18259
- getSceneIds: () => ref.current?.getSceneIds() ?? [],
18260
- isSceneTransitioning: () => ref.current?.isSceneTransitioning() ?? false,
18261
- enterVR: () => ref.current?.enterVR() ?? Promise.resolve(false),
18262
- exitVR: () => ref.current?.exitVR() ?? Promise.resolve(),
18263
- toggleVR: () => ref.current?.toggleVR() ?? Promise.resolve(false),
18264
- isVRAvailable: () => ref.current?.isVRAvailable() ?? false,
18265
- isVREnabled: () => ref.current?.isVREnabled() ?? false,
18266
- getVRMode: () => ref.current?.getVRMode() ?? null,
18267
- requestVRPermission: () => ref.current?.requestVRPermission() ?? Promise.resolve(false),
18268
- getVideo: () => ref.current?.getVideo() ?? null,
18269
- subscribeVideo: (onStoreChange) => ref.current?.subscribeVideo(onStoreChange) ?? (() => {
18270
- }),
18271
- playVideo: () => ref.current?.playVideo() ?? Promise.resolve(),
18272
- pauseVideo: () => {
18273
- ref.current?.pauseVideo();
18274
- },
18275
- toggleVideo: () => {
18276
- ref.current?.toggleVideo();
18277
- },
18278
- seekVideo: (time) => {
18279
- ref.current?.seekVideo(time);
18280
- },
18281
- setVideoVolume: (volume) => {
18282
- ref.current?.setVideoVolume(volume);
18283
- },
18284
- setVideoMuted: (muted) => {
18285
- ref.current?.setVideoMuted(muted);
18286
- },
18287
- toggleVideoMuted: () => {
18288
- ref.current?.toggleVideoMuted();
18289
- },
18290
- getBackgroundAudio: () => ref.current?.getBackgroundAudio() ?? null,
18291
- subscribeBackgroundAudio: (onStoreChange) => ref.current?.subscribeBackgroundAudio(onStoreChange) ?? (() => {
18292
- }),
18293
- playBackgroundAudio: () => ref.current?.playBackgroundAudio() ?? Promise.resolve(),
18294
- pauseBackgroundAudio: () => {
18295
- ref.current?.pauseBackgroundAudio();
18296
- },
18297
- toggleBackgroundAudio: () => {
18298
- ref.current?.toggleBackgroundAudio();
18299
- },
18300
- setBackgroundAudioVolume: (volume) => {
18301
- ref.current?.setBackgroundAudioVolume(volume);
18302
- },
18303
- setBackgroundAudioMuted: (muted) => {
18304
- ref.current?.setBackgroundAudioMuted(muted);
18305
- },
18306
- toggleBackgroundAudioMuted: () => {
18307
- ref.current?.toggleBackgroundAudioMuted();
18308
- },
18309
- startAutoRotate: () => {
18310
- ref.current?.startAutoRotate();
18311
- },
18312
- stopAutoRotate: () => {
18313
- ref.current?.stopAutoRotate();
18314
- }
18260
+ inertia: motion?.inertia !== false,
18261
+ bounce: motion?.bounce !== false
18315
18262
  };
18316
18263
  }
18317
- function usePanoViewer() {
18318
- const ref = (0, import_react62.useRef)(null);
18319
- const handle = (0, import_react62.useMemo)(() => createPanoViewerHandle(ref), []);
18320
- return (0, import_react62.useMemo)(() => ({ ref, ...handle }), [handle]);
18264
+ function clampAxis(value, min, max) {
18265
+ if (max < min) return min;
18266
+ return Math.min(max, Math.max(min, value));
18267
+ }
18268
+ function constrainDragPosition(raw, min, max, dimension, bounce) {
18269
+ if (!(Number.isFinite(raw) && Number.isFinite(min) && Number.isFinite(max))) {
18270
+ return 0;
18271
+ }
18272
+ if (raw >= min && raw <= max) {
18273
+ return raw;
18274
+ }
18275
+ if (!bounce) {
18276
+ return clampAxis(raw, min, max);
18277
+ }
18278
+ const size = Math.max(1, Math.abs(dimension) || 1);
18279
+ if (raw < min) {
18280
+ const overshoot2 = min - raw;
18281
+ return min - rubberBandDistance(overshoot2, size);
18282
+ }
18283
+ const overshoot = raw - max;
18284
+ return max + rubberBandDistance(overshoot, size);
18285
+ }
18286
+ function rubberBandDistance(overshoot, dimension, constant = DRAG_RUBBER_CONSTANT) {
18287
+ if (!(overshoot > 0)) return 0;
18288
+ const size = Math.max(1, Math.abs(dimension) || 1);
18289
+ return size * overshoot / (size + overshoot * constant);
18290
+ }
18291
+ function velocityFromSamples(samples, now) {
18292
+ if (samples.length < 2) return 0;
18293
+ const windowMs = 100;
18294
+ let oldest = samples[0];
18295
+ const newest = samples[samples.length - 1];
18296
+ const referenceTime = now ?? newest.t;
18297
+ for (let i = samples.length - 2; i >= 0; i -= 1) {
18298
+ const sample = samples[i];
18299
+ if (referenceTime - sample.t > windowMs) break;
18300
+ oldest = sample;
18301
+ }
18302
+ const dt = (newest.t - oldest.t) / 1e3;
18303
+ if (!(dt >= 1e-3)) return 0;
18304
+ return (newest.position - oldest.position) / dt;
18305
+ }
18306
+ function integrateAxis(state, deltaSeconds, options) {
18307
+ const dt = Math.max(0, deltaSeconds);
18308
+ if (!(dt > 0)) {
18309
+ return { ...state, settled: isAxisSettled(state, options) };
18310
+ }
18311
+ const damping = options.damping ?? DRAG_INERTIA_DAMPING;
18312
+ const frictionStop = options.frictionStop ?? DRAG_FRICTION_STOP;
18313
+ const spring = options.spring ?? DRAG_SPRING_STRENGTH;
18314
+ const { min, max } = options;
18315
+ let { position, velocity } = state;
18316
+ const pastMin = position < min;
18317
+ const pastMax = position > max;
18318
+ if (pastMin || pastMax) {
18319
+ if (!options.bounce) {
18320
+ position = clampAxis(position, min, max);
18321
+ return { position, velocity: 0, settled: true };
18322
+ }
18323
+ const edge = pastMin ? min : max;
18324
+ if (pastMin && velocity < 0) velocity = 0;
18325
+ if (pastMax && velocity > 0) velocity = 0;
18326
+ const displacement = edge - position;
18327
+ velocity += displacement * spring * dt;
18328
+ const decay = Math.exp(-damping * dt);
18329
+ velocity *= decay;
18330
+ position += velocity * dt;
18331
+ if (pastMin && position > min) {
18332
+ position = min;
18333
+ velocity = 0;
18334
+ }
18335
+ if (pastMax && position < max) {
18336
+ position = max;
18337
+ velocity = 0;
18338
+ }
18339
+ const settled = isAxisSettled({ position, velocity }, options);
18340
+ if (settled) {
18341
+ return { position: clampAxis(position, min, max), velocity: 0, settled: true };
18342
+ }
18343
+ return { position, velocity, settled: false };
18344
+ }
18345
+ if (!options.inertia || Math.abs(velocity) < frictionStop) {
18346
+ return { position, velocity: 0, settled: true };
18347
+ }
18348
+ position += velocity * dt;
18349
+ velocity *= Math.exp(-damping * dt);
18350
+ if (position < min || position > max) {
18351
+ if (!options.bounce) {
18352
+ position = clampAxis(position, min, max);
18353
+ return { position, velocity: 0, settled: true };
18354
+ }
18355
+ if (Math.abs(velocity) < frictionStop) {
18356
+ velocity = 0;
18357
+ }
18358
+ return { position, velocity, settled: false };
18359
+ }
18360
+ if (Math.abs(velocity) < frictionStop) {
18361
+ return { position, velocity: 0, settled: true };
18362
+ }
18363
+ return { position, velocity, settled: false };
18364
+ }
18365
+ function isAxisSettled(state, options) {
18366
+ const frictionStop = options.frictionStop ?? DRAG_FRICTION_STOP;
18367
+ const inside = state.position >= options.min - DRAG_SPRING_SETTLE_DISTANCE && state.position <= options.max + DRAG_SPRING_SETTLE_DISTANCE;
18368
+ const slow = Math.abs(state.velocity) < (options.frictionStop ?? DRAG_SPRING_SETTLE_VELOCITY);
18369
+ if (!inside) return false;
18370
+ return slow || Math.abs(state.velocity) < frictionStop;
18371
+ }
18372
+ function pushSample(samples, sample, maxCount = 8) {
18373
+ const next = samples.length >= maxCount ? samples.slice(samples.length - maxCount + 1) : samples.slice();
18374
+ next.push(sample);
18375
+ return next;
18321
18376
  }
18322
18377
 
18323
- // src/video/pano-video.tsx
18324
- var import_fiber31 = require("@react-three/fiber");
18325
- var import_react63 = require("react");
18326
- var import_three40 = require("three");
18378
+ // src/pano-scene-menu.tsx
18327
18379
  var import_jsx_runtime41 = require("react/jsx-runtime");
18328
- var EMPTY_TRACKS = [];
18329
- var EMPTY_RATES = DEFAULT_PANO_VIDEO_PLAYBACK_RATES;
18330
- function createEmptySnapshot() {
18380
+ var SCENE_MENU_DRAG_THRESHOLD_PX = 6;
18381
+ var DEFAULT_PANO_SCENE_MENU_APPEARANCE = {
18382
+ background: "rgba(16, 16, 16, 0.72)",
18383
+ padding: "8px 10px",
18384
+ itemWidth: 128,
18385
+ itemHeight: 80,
18386
+ gap: 8,
18387
+ borderRadius: 8,
18388
+ selectionColor: "#ffffff",
18389
+ selectionWidth: 2,
18390
+ labelBackground: "rgba(0, 0, 0, 0.55)",
18391
+ labelColor: "#ffffff",
18392
+ labelAlign: "start",
18393
+ fontSize: 12,
18394
+ controlColor: "#ffffff",
18395
+ controlBackground: "rgba(255, 255, 255, 0.12)"
18396
+ };
18397
+ var DEFAULT_INSET = 12;
18398
+ function cssLength(value) {
18399
+ return typeof value === "number" ? `${value}px` : value;
18400
+ }
18401
+ function resolveAppearance2(appearance) {
18331
18402
  return {
18332
- ready: false,
18333
- playing: false,
18334
- blocked: false,
18335
- ended: false,
18336
- currentTime: 0,
18337
- duration: 0,
18338
- volume: 1,
18339
- volumeAdjustable: true,
18340
- muted: true,
18341
- playbackRate: 1,
18342
- variantId: "default",
18343
- variants: [],
18344
- trackId: null,
18345
- tracks: [],
18346
- captionText: "",
18347
- captionAppearance: DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
18348
- captionsEnabled: true,
18349
- playbackRates: EMPTY_RATES,
18350
- playbackState: "paused"
18403
+ ...DEFAULT_PANO_SCENE_MENU_APPEARANCE,
18404
+ ...appearance
18351
18405
  };
18352
18406
  }
18353
- function playbackStateFromVideo(video, blocked) {
18354
- if (blocked) {
18355
- return "blocked";
18356
- }
18357
- if (video.ended) {
18358
- return "ended";
18407
+ function resolveInset(inset) {
18408
+ if (inset === void 0) {
18409
+ const value = cssLength(DEFAULT_INSET);
18410
+ return { top: value, right: value, bottom: value, left: value };
18359
18411
  }
18360
- if (!video.paused) {
18361
- return "playing";
18412
+ if (typeof inset === "number" || typeof inset === "string") {
18413
+ const value = cssLength(inset);
18414
+ return { top: value, right: value, bottom: value, left: value };
18362
18415
  }
18363
- return "paused";
18416
+ return {
18417
+ top: cssLength(inset.top ?? DEFAULT_INSET),
18418
+ right: cssLength(inset.right ?? DEFAULT_INSET),
18419
+ bottom: cssLength(inset.bottom ?? DEFAULT_INSET),
18420
+ left: cssLength(inset.left ?? DEFAULT_INSET)
18421
+ };
18364
18422
  }
18365
- function collectCaptionText(track) {
18366
- if (!track?.activeCues || track.activeCues.length === 0) {
18367
- return "";
18423
+ function itemLabel(item) {
18424
+ return item.title?.trim() || item.id;
18425
+ }
18426
+ function isHorizontal(placement) {
18427
+ return placement === "top" || placement === "bottom";
18428
+ }
18429
+ var CHEVRON_PATHS = {
18430
+ // Keep glyphs optically centered in the 16×16 viewBox.
18431
+ up: "M3 11l5-6 5 6",
18432
+ down: "M3 5l5 6 5-6",
18433
+ left: "M11 3 5 8l6 5",
18434
+ right: "M5 3l6 5-6 5"
18435
+ };
18436
+ function arrowChevron(direction, orientation2) {
18437
+ if (direction === "prev") {
18438
+ return orientation2 === "horizontal" ? "left" : "up";
18368
18439
  }
18369
- const parts = [];
18370
- for (let index = 0; index < track.activeCues.length; index += 1) {
18371
- const cue = track.activeCues[index];
18372
- if (!cue) {
18373
- continue;
18374
- }
18375
- const text = cueText(cue);
18376
- if (text) {
18377
- parts.push(text);
18440
+ return orientation2 === "horizontal" ? "right" : "down";
18441
+ }
18442
+ function toggleChevron(placement, expanded) {
18443
+ switch (placement) {
18444
+ case "bottom":
18445
+ return expanded ? "down" : "up";
18446
+ case "top":
18447
+ return expanded ? "up" : "down";
18448
+ case "left":
18449
+ return expanded ? "left" : "right";
18450
+ case "right":
18451
+ return expanded ? "right" : "left";
18452
+ default: {
18453
+ const exhaustive = placement;
18454
+ void exhaustive;
18455
+ return "down";
18378
18456
  }
18379
18457
  }
18380
- return parts.join("\n");
18381
18458
  }
18382
- function findTextTrack(video, tracks, trackId) {
18383
- if (!trackId) {
18384
- return null;
18385
- }
18386
- const configured = tracks.find((track) => panoVideoTrackId(track) === trackId);
18387
- if (!configured) {
18388
- return null;
18459
+ function dockInset(placement, inset) {
18460
+ switch (placement) {
18461
+ case "top":
18462
+ return inset.top;
18463
+ case "bottom":
18464
+ return inset.bottom;
18465
+ case "left":
18466
+ return inset.left;
18467
+ case "right":
18468
+ return inset.right;
18469
+ default: {
18470
+ const exhaustive = placement;
18471
+ void exhaustive;
18472
+ return inset.bottom;
18473
+ }
18389
18474
  }
18390
- const list = video.textTracks;
18391
- for (let index = 0; index < list.length; index += 1) {
18392
- const textTrack = list[index];
18393
- if (!textTrack) {
18394
- continue;
18475
+ }
18476
+ function Chevron({ arrow }) {
18477
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
18478
+ "svg",
18479
+ {
18480
+ "aria-hidden": true,
18481
+ fill: "none",
18482
+ height: "16",
18483
+ stroke: "currentColor",
18484
+ strokeLinecap: "round",
18485
+ strokeLinejoin: "round",
18486
+ strokeWidth: "1.75",
18487
+ viewBox: "0 0 16 16",
18488
+ width: "16",
18489
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("path", { d: CHEVRON_PATHS[arrow] })
18395
18490
  }
18396
- if (textTrack.language === configured.srcLang || textTrack.label === configured.label) {
18397
- return textTrack;
18491
+ );
18492
+ }
18493
+ function ControlButton({
18494
+ className,
18495
+ disabled,
18496
+ label,
18497
+ onClick,
18498
+ children
18499
+ }) {
18500
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
18501
+ "button",
18502
+ {
18503
+ "aria-label": label,
18504
+ className: cn("pano-scene-menu-control", className),
18505
+ disabled,
18506
+ onClick,
18507
+ onPointerDown: (event) => {
18508
+ event.stopPropagation();
18509
+ },
18510
+ title: label,
18511
+ type: "button",
18512
+ children
18513
+ }
18514
+ );
18515
+ }
18516
+ function horizontalAlignStyle(align, inset, widthValue) {
18517
+ const left = `calc(${inset.left} + var(--pano-safe-left))`;
18518
+ const right = `calc(${inset.right} + var(--pano-safe-right))`;
18519
+ if (!widthValue) {
18520
+ return { left, right, width: void 0, marginInline: void 0 };
18521
+ }
18522
+ switch (align) {
18523
+ case "start":
18524
+ return { left, right: "auto", width: widthValue, marginInline: void 0 };
18525
+ case "end":
18526
+ return { left: "auto", right, width: widthValue, marginInline: void 0 };
18527
+ case "center":
18528
+ return { left, right, width: widthValue, marginInline: "auto" };
18529
+ default: {
18530
+ const exhaustive = align;
18531
+ void exhaustive;
18532
+ return { left, right, width: widthValue, marginInline: "auto" };
18398
18533
  }
18399
18534
  }
18400
- return list.length > 0 ? list[0] : null;
18401
18535
  }
18402
- function applyEquirectangularLayout(texture, anisotropy) {
18403
- texture.colorSpace = import_three40.SRGBColorSpace;
18404
- texture.wrapS = import_three40.RepeatWrapping;
18405
- texture.repeat.x = -1;
18406
- texture.offset.x = 1;
18407
- texture.anisotropy = anisotropy;
18408
- texture.needsUpdate = true;
18536
+ function verticalAlignStyle(align, inset, heightValue) {
18537
+ const top = `calc(${inset.top} + var(--pano-safe-top))`;
18538
+ const bottom = `calc(${inset.bottom} + var(--pano-safe-bottom))`;
18539
+ if (!heightValue) {
18540
+ return { top, bottom, height: void 0, marginBlock: void 0 };
18541
+ }
18542
+ switch (align) {
18543
+ case "start":
18544
+ return { top, bottom: "auto", height: heightValue, marginBlock: void 0 };
18545
+ case "end":
18546
+ return { top: "auto", bottom, height: heightValue, marginBlock: void 0 };
18547
+ case "center":
18548
+ return { top, bottom, height: heightValue, marginBlock: "auto" };
18549
+ default: {
18550
+ const exhaustive = align;
18551
+ void exhaustive;
18552
+ return { top, bottom, height: heightValue, marginBlock: "auto" };
18553
+ }
18554
+ }
18409
18555
  }
18410
- function usePosterTexture2(poster, onError) {
18411
- const gl = (0, import_fiber31.useThree)((state) => state.gl);
18412
- const [texture, setTexture] = (0, import_react63.useState)(null);
18413
- const onErrorRef = (0, import_react63.useRef)(onError);
18414
- onErrorRef.current = onError;
18415
- (0, import_react63.useEffect)(() => {
18416
- let active = true;
18417
- setTexture(null);
18418
- if (!poster) {
18419
- return () => {
18420
- active = false;
18556
+ function placementBoxStyle(placement, inset, width, height, align) {
18557
+ const widthValue = width === void 0 ? void 0 : cssLength(width);
18558
+ const heightValue = height === void 0 ? void 0 : cssLength(height);
18559
+ const maxWidth = `calc(100% - ${inset.left} - ${inset.right} - var(--pano-safe-left) - var(--pano-safe-right))`;
18560
+ const maxHeight = `calc(100% - ${inset.top} - ${inset.bottom} - var(--pano-safe-top) - var(--pano-safe-bottom))`;
18561
+ switch (placement) {
18562
+ case "top":
18563
+ return {
18564
+ ...horizontalAlignStyle(align, inset, widthValue),
18565
+ top: `calc(${inset.top} + var(--pano-safe-top))`,
18566
+ bottom: "auto",
18567
+ maxWidth,
18568
+ height: heightValue,
18569
+ maxHeight
18570
+ };
18571
+ case "bottom":
18572
+ return {
18573
+ ...horizontalAlignStyle(align, inset, widthValue),
18574
+ top: "auto",
18575
+ bottom: `calc(${inset.bottom} + var(--pano-safe-bottom))`,
18576
+ maxWidth,
18577
+ height: heightValue,
18578
+ maxHeight
18579
+ };
18580
+ case "left":
18581
+ return {
18582
+ ...verticalAlignStyle(align, inset, heightValue),
18583
+ right: "auto",
18584
+ left: `calc(${inset.left} + var(--pano-safe-left))`,
18585
+ width: widthValue,
18586
+ maxWidth,
18587
+ maxHeight
18588
+ };
18589
+ case "right":
18590
+ return {
18591
+ ...verticalAlignStyle(align, inset, heightValue),
18592
+ right: `calc(${inset.right} + var(--pano-safe-right))`,
18593
+ left: "auto",
18594
+ width: widthValue,
18595
+ maxWidth,
18596
+ maxHeight
18421
18597
  };
18598
+ default: {
18599
+ const exhaustive = placement;
18600
+ void exhaustive;
18601
+ return {};
18422
18602
  }
18423
- const loader = new import_three40.TextureLoader();
18424
- loader.load(
18425
- poster,
18603
+ }
18604
+ }
18605
+ function PanoSceneMenu({
18606
+ items,
18607
+ activeId = null,
18608
+ onSelect,
18609
+ placement = "bottom",
18610
+ align = "center",
18611
+ inset,
18612
+ width,
18613
+ height,
18614
+ collapsible = false,
18615
+ defaultExpanded = true,
18616
+ expanded: expandedProp,
18617
+ onExpandedChange,
18618
+ showArrows = false,
18619
+ loop = false,
18620
+ dragMotion: dragMotionProp,
18621
+ appearance,
18622
+ className,
18623
+ style,
18624
+ label = "Scenes",
18625
+ previousLabel = "Previous scene",
18626
+ nextLabel = "Next scene",
18627
+ expandLabel = "Show scenes",
18628
+ collapseLabel = "Hide scenes"
18629
+ }) {
18630
+ const listId = (0, import_react62.useId)();
18631
+ const listRef = (0, import_react62.useRef)(null);
18632
+ const trackRef = (0, import_react62.useRef)(null);
18633
+ const itemRefs = (0, import_react62.useRef)(/* @__PURE__ */ new Map());
18634
+ const resolved = resolveAppearance2(appearance);
18635
+ const resolvedInset = resolveInset(inset);
18636
+ const horizontal = isHorizontal(placement);
18637
+ const customDrag = dragMotionProp !== false;
18638
+ const dragMotion = resolveDragMotion(
18639
+ dragMotionProp === false ? false : dragMotionProp
18640
+ );
18641
+ const isExpandedControlled = expandedProp !== void 0;
18642
+ const [internalExpanded, setInternalExpanded] = (0, import_react62.useState)(defaultExpanded);
18643
+ const expanded = collapsible ? isExpandedControlled ? expandedProp : internalExpanded : true;
18644
+ const [offset, setOffset] = (0, import_react62.useState)(0);
18645
+ const [viewportSize, setViewportSize] = (0, import_react62.useState)(0);
18646
+ const [contentSize, setContentSize] = (0, import_react62.useState)(0);
18647
+ const offsetRef = (0, import_react62.useRef)(0);
18648
+ const dragRef = (0, import_react62.useRef)(null);
18649
+ const animRef = (0, import_react62.useRef)(null);
18650
+ const maxOffset = Math.max(0, contentSize - viewportSize);
18651
+ offsetRef.current = offset;
18652
+ const activeIndex = (0, import_react62.useMemo)(() => {
18653
+ if (activeId == null) {
18654
+ return -1;
18655
+ }
18656
+ return items.findIndex((item) => item.id === activeId);
18657
+ }, [activeId, items]);
18658
+ const cancelScrollAnimation = (0, import_react62.useCallback)(() => {
18659
+ const anim = animRef.current;
18660
+ if (anim?.raf != null) {
18661
+ cancelAnimationFrame(anim.raf);
18662
+ }
18663
+ animRef.current = null;
18664
+ }, []);
18665
+ const clampOffset = (0, import_react62.useCallback)(
18666
+ (value) => Math.min(maxOffset, Math.max(0, value)),
18667
+ [maxOffset]
18668
+ );
18669
+ const startScrollAnimation = (0, import_react62.useCallback)(
18670
+ (velocity) => {
18671
+ cancelScrollAnimation();
18672
+ const current = offsetRef.current;
18673
+ const needsSpring = dragMotion.bounce && (current < 0 || current > maxOffset);
18674
+ const nextVelocity = dragMotion.inertia ? velocity : 0;
18675
+ if (!needsSpring && Math.abs(nextVelocity) < 1) {
18676
+ setOffset(clampOffset(current));
18677
+ return;
18678
+ }
18679
+ if (!dragMotion.inertia && !dragMotion.bounce) {
18680
+ setOffset(clampOffset(current));
18681
+ return;
18682
+ }
18683
+ const anim = {
18684
+ raf: null,
18685
+ lastTime: performance.now(),
18686
+ position: current,
18687
+ velocity: nextVelocity
18688
+ };
18689
+ animRef.current = anim;
18690
+ const tick = (now) => {
18691
+ if (animRef.current !== anim) return;
18692
+ const dt = Math.min(0.064, Math.max(0, (now - anim.lastTime) / 1e3));
18693
+ anim.lastTime = now;
18694
+ const next = integrateAxis(
18695
+ { position: anim.position, velocity: anim.velocity },
18696
+ dt,
18697
+ {
18698
+ min: 0,
18699
+ max: maxOffset,
18700
+ inertia: dragMotion.inertia,
18701
+ bounce: dragMotion.bounce
18702
+ }
18703
+ );
18704
+ anim.position = next.position;
18705
+ anim.velocity = next.velocity;
18706
+ setOffset(next.position);
18707
+ if (next.settled) {
18708
+ setOffset(clampOffset(next.position));
18709
+ animRef.current = null;
18710
+ return;
18711
+ }
18712
+ anim.raf = requestAnimationFrame(tick);
18713
+ };
18714
+ anim.raf = requestAnimationFrame(tick);
18715
+ },
18716
+ [
18717
+ cancelScrollAnimation,
18718
+ clampOffset,
18719
+ dragMotion.bounce,
18720
+ dragMotion.inertia,
18721
+ maxOffset
18722
+ ]
18723
+ );
18724
+ (0, import_react62.useEffect)(() => () => cancelScrollAnimation(), [cancelScrollAnimation]);
18725
+ (0, import_react62.useLayoutEffect)(() => {
18726
+ if (!customDrag || !expanded) return;
18727
+ const list = listRef.current;
18728
+ const track = trackRef.current;
18729
+ if (!list || !track) return;
18730
+ const measure = () => {
18731
+ const nextViewport = horizontal ? list.clientWidth : list.clientHeight;
18732
+ const nextContent = horizontal ? track.scrollWidth : track.scrollHeight;
18733
+ setViewportSize(
18734
+ (previous) => previous === nextViewport ? previous : nextViewport
18735
+ );
18736
+ setContentSize(
18737
+ (previous) => previous === nextContent ? previous : nextContent
18738
+ );
18739
+ };
18740
+ measure();
18741
+ const observer = new ResizeObserver(measure);
18742
+ observer.observe(list);
18743
+ observer.observe(track);
18744
+ return () => observer.disconnect();
18745
+ }, [customDrag, expanded, horizontal, items]);
18746
+ (0, import_react62.useEffect)(() => {
18747
+ if (!customDrag) return;
18748
+ if (dragRef.current || animRef.current) return;
18749
+ setOffset((previous) => Math.min(maxOffset, Math.max(0, previous)));
18750
+ }, [customDrag, maxOffset]);
18751
+ (0, import_react62.useEffect)(() => {
18752
+ if (!expanded || activeId == null) {
18753
+ return;
18754
+ }
18755
+ const node = itemRefs.current.get(activeId);
18756
+ if (!node) return;
18757
+ if (!customDrag) {
18758
+ node.scrollIntoView({
18759
+ behavior: "smooth",
18760
+ block: "nearest",
18761
+ inline: "nearest"
18762
+ });
18763
+ return;
18764
+ }
18765
+ const list = listRef.current;
18766
+ if (!list) return;
18767
+ const listRect = list.getBoundingClientRect();
18768
+ const itemRect = node.getBoundingClientRect();
18769
+ if (horizontal) {
18770
+ const itemStart2 = itemRect.left - listRect.left + offsetRef.current;
18771
+ const itemEnd2 = itemStart2 + itemRect.width;
18772
+ let next2 = offsetRef.current;
18773
+ if (itemStart2 < next2) next2 = itemStart2;
18774
+ else if (itemEnd2 > next2 + listRect.width) next2 = itemEnd2 - listRect.width;
18775
+ cancelScrollAnimation();
18776
+ setOffset(clampOffset(next2));
18777
+ return;
18778
+ }
18779
+ const itemStart = itemRect.top - listRect.top + offsetRef.current;
18780
+ const itemEnd = itemStart + itemRect.height;
18781
+ let next = offsetRef.current;
18782
+ if (itemStart < next) next = itemStart;
18783
+ else if (itemEnd > next + listRect.height) next = itemEnd - listRect.height;
18784
+ cancelScrollAnimation();
18785
+ setOffset(clampOffset(next));
18786
+ }, [
18787
+ activeId,
18788
+ cancelScrollAnimation,
18789
+ clampOffset,
18790
+ customDrag,
18791
+ expanded,
18792
+ horizontal
18793
+ ]);
18794
+ const rootStyle = (0, import_react62.useMemo)(() => {
18795
+ const vars = {
18796
+ "--pano-scene-menu-background": resolved.background,
18797
+ "--pano-scene-menu-padding": cssLength(resolved.padding),
18798
+ "--pano-scene-menu-item-width": cssLength(resolved.itemWidth),
18799
+ "--pano-scene-menu-item-height": cssLength(resolved.itemHeight),
18800
+ "--pano-scene-menu-gap": cssLength(resolved.gap),
18801
+ "--pano-scene-menu-radius": cssLength(resolved.borderRadius),
18802
+ "--pano-scene-menu-selection-color": resolved.selectionColor,
18803
+ "--pano-scene-menu-selection-width": cssLength(resolved.selectionWidth),
18804
+ "--pano-scene-menu-label-background": resolved.labelBackground,
18805
+ "--pano-scene-menu-label-color": resolved.labelColor,
18806
+ "--pano-scene-menu-label-align": resolved.labelAlign,
18807
+ "--pano-scene-menu-font-size": cssLength(resolved.fontSize),
18808
+ "--pano-scene-menu-control-color": resolved.controlColor,
18809
+ "--pano-scene-menu-control-background": resolved.controlBackground,
18810
+ "--pano-scene-menu-dock-inset": dockInset(placement, resolvedInset)
18811
+ };
18812
+ return {
18813
+ ...vars,
18814
+ ...placementBoxStyle(placement, resolvedInset, width, height, align),
18815
+ ...style
18816
+ };
18817
+ }, [align, height, placement, resolved, resolvedInset, style, width]);
18818
+ const setExpanded = (next) => {
18819
+ if (!collapsible) {
18820
+ return;
18821
+ }
18822
+ if (!isExpandedControlled) {
18823
+ setInternalExpanded(next);
18824
+ }
18825
+ onExpandedChange?.(next);
18826
+ };
18827
+ const selectAt = (index) => {
18828
+ const item = items[index];
18829
+ if (!item) {
18830
+ return;
18831
+ }
18832
+ onSelect?.(item.id);
18833
+ itemRefs.current.get(item.id)?.focus({ preventScroll: true });
18834
+ };
18835
+ const selectRelative = (delta) => {
18836
+ if (items.length === 0) {
18837
+ return;
18838
+ }
18839
+ const current = activeIndex >= 0 ? activeIndex : 0;
18840
+ if (loop) {
18841
+ const next = ((current + delta) % items.length + items.length) % items.length;
18842
+ selectAt(next);
18843
+ return;
18844
+ }
18845
+ selectAt(Math.max(0, Math.min(items.length - 1, current + delta)));
18846
+ };
18847
+ const onKeyDown = (event) => {
18848
+ if (items.length === 0 || !expanded) {
18849
+ return;
18850
+ }
18851
+ const current = activeIndex >= 0 ? activeIndex : Math.max(
18852
+ 0,
18853
+ items.findIndex(
18854
+ (item) => itemRefs.current.get(item.id) === event.target
18855
+ )
18856
+ );
18857
+ const backward = horizontal ? event.key === "ArrowLeft" : event.key === "ArrowUp";
18858
+ const forward = horizontal ? event.key === "ArrowRight" : event.key === "ArrowDown";
18859
+ if (backward) {
18860
+ event.preventDefault();
18861
+ if (loop) {
18862
+ selectAt((current - 1 + items.length) % items.length);
18863
+ } else {
18864
+ selectAt(Math.max(0, current - 1));
18865
+ }
18866
+ return;
18867
+ }
18868
+ if (forward) {
18869
+ event.preventDefault();
18870
+ if (loop) {
18871
+ selectAt((current + 1) % items.length);
18872
+ } else {
18873
+ selectAt(Math.min(items.length - 1, current + 1));
18874
+ }
18875
+ return;
18876
+ }
18877
+ switch (event.key) {
18878
+ case "Home": {
18879
+ event.preventDefault();
18880
+ selectAt(0);
18881
+ break;
18882
+ }
18883
+ case "End": {
18884
+ event.preventDefault();
18885
+ selectAt(items.length - 1);
18886
+ break;
18887
+ }
18888
+ default:
18889
+ break;
18890
+ }
18891
+ };
18892
+ const onWheel = (event) => {
18893
+ const list = listRef.current;
18894
+ if (!list) {
18895
+ return;
18896
+ }
18897
+ if (!customDrag) {
18898
+ if (horizontal) {
18899
+ const canScroll2 = list.scrollWidth > list.clientWidth + 1;
18900
+ if (!canScroll2 || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
18901
+ return;
18902
+ }
18903
+ event.preventDefault();
18904
+ event.stopPropagation();
18905
+ list.scrollLeft += event.deltaY;
18906
+ return;
18907
+ }
18908
+ const canScroll = list.scrollHeight > list.clientHeight + 1;
18909
+ if (!canScroll || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) {
18910
+ return;
18911
+ }
18912
+ event.preventDefault();
18913
+ event.stopPropagation();
18914
+ list.scrollTop += event.deltaX;
18915
+ return;
18916
+ }
18917
+ if (horizontal) {
18918
+ if (maxOffset <= 0 || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
18919
+ return;
18920
+ }
18921
+ event.preventDefault();
18922
+ event.stopPropagation();
18923
+ cancelScrollAnimation();
18924
+ setOffset((previous) => clampOffset(previous + event.deltaY));
18925
+ return;
18926
+ }
18927
+ if (maxOffset <= 0 || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) {
18928
+ return;
18929
+ }
18930
+ event.preventDefault();
18931
+ event.stopPropagation();
18932
+ cancelScrollAnimation();
18933
+ setOffset((previous) => clampOffset(previous + event.deltaX));
18934
+ };
18935
+ const axisPoint = (event) => horizontal ? event.clientX : event.clientY;
18936
+ const endSuppressClickRef = (0, import_react62.useRef)(false);
18937
+ const beginListDrag = (event, captureTarget) => {
18938
+ if (!customDrag || event.button !== 0) return;
18939
+ cancelScrollAnimation();
18940
+ const now = performance.now();
18941
+ const origin = axisPoint(event);
18942
+ dragRef.current = {
18943
+ pointerId: event.pointerId,
18944
+ origin,
18945
+ startOffset: offsetRef.current,
18946
+ samples: [{ t: now, position: offsetRef.current }],
18947
+ moved: false,
18948
+ suppressClick: false
18949
+ };
18950
+ try {
18951
+ captureTarget.setPointerCapture(event.pointerId);
18952
+ } catch {
18953
+ }
18954
+ };
18955
+ const onListPointerMove = (event) => {
18956
+ const drag = dragRef.current;
18957
+ if (!drag || drag.pointerId !== event.pointerId) return;
18958
+ const point = axisPoint(event);
18959
+ const delta = point - drag.origin;
18960
+ if (!drag.moved && Math.abs(delta) >= SCENE_MENU_DRAG_THRESHOLD_PX) {
18961
+ drag.moved = true;
18962
+ drag.suppressClick = true;
18963
+ }
18964
+ if (!drag.moved) return;
18965
+ const raw = drag.startOffset - delta;
18966
+ const now = performance.now();
18967
+ drag.samples = pushSample(drag.samples, { t: now, position: raw });
18968
+ setOffset(
18969
+ constrainDragPosition(
18970
+ raw,
18971
+ 0,
18972
+ maxOffset,
18973
+ Math.max(1, viewportSize),
18974
+ dragMotion.bounce
18975
+ )
18976
+ );
18977
+ event.preventDefault();
18978
+ };
18979
+ const finishPointerWithClickGuard = (event) => {
18980
+ const drag = dragRef.current;
18981
+ if (!drag || drag.pointerId !== event.pointerId) return;
18982
+ const suppress = drag.suppressClick;
18983
+ dragRef.current = null;
18984
+ try {
18985
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
18986
+ event.currentTarget.releasePointerCapture(event.pointerId);
18987
+ }
18988
+ } catch {
18989
+ }
18990
+ if (suppress) {
18991
+ endSuppressClickRef.current = true;
18992
+ queueMicrotask(() => {
18993
+ endSuppressClickRef.current = false;
18994
+ });
18995
+ }
18996
+ if (!drag.moved) return;
18997
+ if (event.type === "pointercancel") {
18998
+ setOffset(clampOffset(offsetRef.current));
18999
+ return;
19000
+ }
19001
+ startScrollAnimation(velocityFromSamples(drag.samples));
19002
+ };
19003
+ const onItemClick = (event, id) => {
19004
+ if (endSuppressClickRef.current) {
19005
+ event.preventDefault();
19006
+ event.stopPropagation();
19007
+ return;
19008
+ }
19009
+ onSelect?.(id);
19010
+ };
19011
+ const canStep = items.length > 1;
19012
+ const canGoPrevious = canStep && (loop || activeIndex !== 0);
19013
+ const canGoNext = canStep && (loop || activeIndex !== items.length - 1);
19014
+ const orientation2 = horizontal ? "horizontal" : "vertical";
19015
+ const trackStyle = customDrag ? {
19016
+ transform: horizontal ? `translate3d(${-offset}px, 0, 0)` : `translate3d(0, ${-offset}px, 0)`,
19017
+ willChange: "transform"
19018
+ } : void 0;
19019
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
19020
+ "div",
19021
+ {
19022
+ "aria-label": label,
19023
+ className: cn("pano-scene-menu", className),
19024
+ "data-align": align,
19025
+ "data-collapsible": collapsible ? "true" : void 0,
19026
+ "data-drag-motion": customDrag ? "custom" : "native",
19027
+ "data-expanded": expanded ? "true" : "false",
19028
+ "data-orientation": orientation2,
19029
+ "data-placement": placement,
19030
+ role: "group",
19031
+ style: rootStyle,
19032
+ children: [
19033
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
19034
+ "div",
19035
+ {
19036
+ "aria-hidden": expanded ? void 0 : true,
19037
+ className: "pano-scene-menu-body",
19038
+ ...!expanded ? { inert: true } : null,
19039
+ children: [
19040
+ showArrows ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19041
+ ControlButton,
19042
+ {
19043
+ className: "pano-scene-menu-arrow",
19044
+ disabled: !canGoPrevious || !expanded,
19045
+ label: previousLabel,
19046
+ onClick: () => selectRelative(-1),
19047
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Chevron, { arrow: arrowChevron("prev", orientation2) })
19048
+ }
19049
+ ) : null,
19050
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19051
+ "div",
19052
+ {
19053
+ "aria-label": label,
19054
+ className: "pano-scene-menu-list",
19055
+ id: listId,
19056
+ onKeyDown,
19057
+ onPointerCancel: customDrag ? finishPointerWithClickGuard : void 0,
19058
+ onPointerDown: customDrag ? (event) => beginListDrag(event, event.currentTarget) : void 0,
19059
+ onPointerMove: customDrag ? onListPointerMove : void 0,
19060
+ onPointerUp: customDrag ? finishPointerWithClickGuard : void 0,
19061
+ onWheel,
19062
+ ref: listRef,
19063
+ role: "radiogroup",
19064
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19065
+ "div",
19066
+ {
19067
+ className: "pano-scene-menu-track",
19068
+ ref: trackRef,
19069
+ style: trackStyle,
19070
+ children: items.map((item) => {
19071
+ const name = itemLabel(item);
19072
+ const selected = item.id === activeId;
19073
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19074
+ "button",
19075
+ {
19076
+ "aria-checked": selected,
19077
+ className: "pano-scene-menu-item",
19078
+ "data-active": selected ? "true" : void 0,
19079
+ onClick: (event) => onItemClick(event, item.id),
19080
+ onPointerDown: (event) => {
19081
+ event.stopPropagation();
19082
+ const list = listRef.current;
19083
+ if (customDrag && list) {
19084
+ beginListDrag(event, list);
19085
+ }
19086
+ },
19087
+ ref: (node) => {
19088
+ if (node) {
19089
+ itemRefs.current.set(item.id, node);
19090
+ } else {
19091
+ itemRefs.current.delete(item.id);
19092
+ }
19093
+ },
19094
+ role: "radio",
19095
+ tabIndex: selected || activeId == null && item === items[0] ? 0 : -1,
19096
+ title: name,
19097
+ type: "button",
19098
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "pano-scene-menu-thumb", children: [
19099
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19100
+ "img",
19101
+ {
19102
+ alt: "",
19103
+ decoding: "async",
19104
+ draggable: false,
19105
+ src: item.thumbnailUrl
19106
+ }
19107
+ ),
19108
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "pano-scene-menu-label", children: name })
19109
+ ] })
19110
+ },
19111
+ item.id
19112
+ );
19113
+ })
19114
+ }
19115
+ )
19116
+ }
19117
+ ),
19118
+ showArrows ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19119
+ ControlButton,
19120
+ {
19121
+ className: "pano-scene-menu-arrow",
19122
+ disabled: !canGoNext || !expanded,
19123
+ label: nextLabel,
19124
+ onClick: () => selectRelative(1),
19125
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Chevron, { arrow: arrowChevron("next", orientation2) })
19126
+ }
19127
+ ) : null
19128
+ ]
19129
+ }
19130
+ ),
19131
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19132
+ ControlButton,
19133
+ {
19134
+ className: "pano-scene-menu-toggle",
19135
+ label: expanded ? collapseLabel : expandLabel,
19136
+ onClick: () => setExpanded(!expanded),
19137
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Chevron, { arrow: toggleChevron(placement, expanded) })
19138
+ }
19139
+ ) : null
19140
+ ]
19141
+ }
19142
+ );
19143
+ }
19144
+
19145
+ // src/use-pano-viewer.ts
19146
+ var import_react63 = require("react");
19147
+ function createPanoViewerHandle(ref) {
19148
+ return {
19149
+ getView: () => ref.current?.getView() ?? { yaw: 0, pitch: 0, fov: 75 },
19150
+ setView: (view, options) => {
19151
+ ref.current?.setView(view, options);
19152
+ },
19153
+ reset: () => {
19154
+ ref.current?.reset();
19155
+ },
19156
+ lookTo: (position, options) => ref.current?.lookTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
19157
+ moveTo: (position, options) => ref.current?.moveTo(position, options) ?? Promise.resolve({ status: "cancelled" }),
19158
+ zoomTo: (fov, options) => ref.current?.zoomTo(fov, options) ?? Promise.resolve({ status: "cancelled" }),
19159
+ lookToHotspot: (id, options) => ref.current?.lookToHotspot(id, options) ?? Promise.resolve({ status: "not-found" }),
19160
+ activateMedia: () => ref.current?.activateMedia() ?? Promise.resolve(),
19161
+ enterFullscreen: () => ref.current?.enterFullscreen() ?? Promise.resolve(),
19162
+ exitFullscreen: () => ref.current?.exitFullscreen() ?? Promise.resolve(),
19163
+ toggleFullscreen: () => ref.current?.toggleFullscreen() ?? Promise.resolve(),
19164
+ isFullscreen: () => ref.current?.isFullscreen() ?? false,
19165
+ setScene: (id) => ref.current?.setScene(id) ?? false,
19166
+ nextScene: () => ref.current?.nextScene() ?? false,
19167
+ previousScene: () => ref.current?.previousScene() ?? false,
19168
+ getActiveSceneId: () => ref.current?.getActiveSceneId() ?? null,
19169
+ getSceneIds: () => ref.current?.getSceneIds() ?? [],
19170
+ isSceneTransitioning: () => ref.current?.isSceneTransitioning() ?? false,
19171
+ enterVR: () => ref.current?.enterVR() ?? Promise.resolve(false),
19172
+ exitVR: () => ref.current?.exitVR() ?? Promise.resolve(),
19173
+ toggleVR: () => ref.current?.toggleVR() ?? Promise.resolve(false),
19174
+ isVRAvailable: () => ref.current?.isVRAvailable() ?? false,
19175
+ isVREnabled: () => ref.current?.isVREnabled() ?? false,
19176
+ getVRMode: () => ref.current?.getVRMode() ?? null,
19177
+ requestVRPermission: () => ref.current?.requestVRPermission() ?? Promise.resolve(false),
19178
+ getVideo: () => ref.current?.getVideo() ?? null,
19179
+ subscribeVideo: (onStoreChange) => ref.current?.subscribeVideo(onStoreChange) ?? (() => {
19180
+ }),
19181
+ playVideo: () => ref.current?.playVideo() ?? Promise.resolve(),
19182
+ pauseVideo: () => {
19183
+ ref.current?.pauseVideo();
19184
+ },
19185
+ toggleVideo: () => {
19186
+ ref.current?.toggleVideo();
19187
+ },
19188
+ seekVideo: (time) => {
19189
+ ref.current?.seekVideo(time);
19190
+ },
19191
+ setVideoVolume: (volume) => {
19192
+ ref.current?.setVideoVolume(volume);
19193
+ },
19194
+ setVideoMuted: (muted) => {
19195
+ ref.current?.setVideoMuted(muted);
19196
+ },
19197
+ toggleVideoMuted: () => {
19198
+ ref.current?.toggleVideoMuted();
19199
+ },
19200
+ getBackgroundAudio: () => ref.current?.getBackgroundAudio() ?? null,
19201
+ subscribeBackgroundAudio: (onStoreChange) => ref.current?.subscribeBackgroundAudio(onStoreChange) ?? (() => {
19202
+ }),
19203
+ playBackgroundAudio: () => ref.current?.playBackgroundAudio() ?? Promise.resolve(),
19204
+ pauseBackgroundAudio: () => {
19205
+ ref.current?.pauseBackgroundAudio();
19206
+ },
19207
+ toggleBackgroundAudio: () => {
19208
+ ref.current?.toggleBackgroundAudio();
19209
+ },
19210
+ setBackgroundAudioVolume: (volume) => {
19211
+ ref.current?.setBackgroundAudioVolume(volume);
19212
+ },
19213
+ setBackgroundAudioMuted: (muted) => {
19214
+ ref.current?.setBackgroundAudioMuted(muted);
19215
+ },
19216
+ toggleBackgroundAudioMuted: () => {
19217
+ ref.current?.toggleBackgroundAudioMuted();
19218
+ },
19219
+ startAutoRotate: () => {
19220
+ ref.current?.startAutoRotate();
19221
+ },
19222
+ stopAutoRotate: () => {
19223
+ ref.current?.stopAutoRotate();
19224
+ }
19225
+ };
19226
+ }
19227
+ function usePanoViewer() {
19228
+ const ref = (0, import_react63.useRef)(null);
19229
+ const handle = (0, import_react63.useMemo)(() => createPanoViewerHandle(ref), []);
19230
+ return (0, import_react63.useMemo)(() => ({ ref, ...handle }), [handle]);
19231
+ }
19232
+
19233
+ // src/video/pano-video.tsx
19234
+ var import_fiber31 = require("@react-three/fiber");
19235
+ var import_react64 = require("react");
19236
+ var import_three40 = require("three");
19237
+ var import_jsx_runtime42 = require("react/jsx-runtime");
19238
+ var EMPTY_TRACKS = [];
19239
+ var EMPTY_RATES = DEFAULT_PANO_VIDEO_PLAYBACK_RATES;
19240
+ function createEmptySnapshot() {
19241
+ return {
19242
+ ready: false,
19243
+ playing: false,
19244
+ blocked: false,
19245
+ ended: false,
19246
+ currentTime: 0,
19247
+ duration: 0,
19248
+ volume: 1,
19249
+ volumeAdjustable: true,
19250
+ muted: true,
19251
+ playbackRate: 1,
19252
+ variantId: "default",
19253
+ variants: [],
19254
+ trackId: null,
19255
+ tracks: [],
19256
+ captionText: "",
19257
+ captionAppearance: DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
19258
+ captionsEnabled: true,
19259
+ playbackRates: EMPTY_RATES,
19260
+ playbackState: "paused"
19261
+ };
19262
+ }
19263
+ function playbackStateFromVideo(video, blocked) {
19264
+ if (blocked) {
19265
+ return "blocked";
19266
+ }
19267
+ if (video.ended) {
19268
+ return "ended";
19269
+ }
19270
+ if (!video.paused) {
19271
+ return "playing";
19272
+ }
19273
+ return "paused";
19274
+ }
19275
+ function collectCaptionText(track) {
19276
+ if (!track?.activeCues || track.activeCues.length === 0) {
19277
+ return "";
19278
+ }
19279
+ const parts = [];
19280
+ for (let index = 0; index < track.activeCues.length; index += 1) {
19281
+ const cue = track.activeCues[index];
19282
+ if (!cue) {
19283
+ continue;
19284
+ }
19285
+ const text = cueText(cue);
19286
+ if (text) {
19287
+ parts.push(text);
19288
+ }
19289
+ }
19290
+ return parts.join("\n");
19291
+ }
19292
+ function findTextTrack(video, tracks, trackId) {
19293
+ if (!trackId) {
19294
+ return null;
19295
+ }
19296
+ const configured = tracks.find((track) => panoVideoTrackId(track) === trackId);
19297
+ if (!configured) {
19298
+ return null;
19299
+ }
19300
+ const list = video.textTracks;
19301
+ for (let index = 0; index < list.length; index += 1) {
19302
+ const textTrack = list[index];
19303
+ if (!textTrack) {
19304
+ continue;
19305
+ }
19306
+ if (textTrack.language === configured.srcLang || textTrack.label === configured.label) {
19307
+ return textTrack;
19308
+ }
19309
+ }
19310
+ return list.length > 0 ? list[0] : null;
19311
+ }
19312
+ function applyEquirectangularLayout(texture, anisotropy) {
19313
+ texture.colorSpace = import_three40.SRGBColorSpace;
19314
+ texture.wrapS = import_three40.RepeatWrapping;
19315
+ texture.repeat.x = -1;
19316
+ texture.offset.x = 1;
19317
+ texture.anisotropy = anisotropy;
19318
+ texture.needsUpdate = true;
19319
+ }
19320
+ function usePosterTexture2(poster, onError) {
19321
+ const gl = (0, import_fiber31.useThree)((state) => state.gl);
19322
+ const [texture, setTexture] = (0, import_react64.useState)(null);
19323
+ const onErrorRef = (0, import_react64.useRef)(onError);
19324
+ onErrorRef.current = onError;
19325
+ (0, import_react64.useEffect)(() => {
19326
+ let active = true;
19327
+ setTexture(null);
19328
+ if (!poster) {
19329
+ return () => {
19330
+ active = false;
19331
+ };
19332
+ }
19333
+ const loader = new import_three40.TextureLoader();
19334
+ loader.load(
19335
+ poster,
18426
19336
  (loadedTexture) => {
18427
19337
  if (!active) {
18428
19338
  loadedTexture.dispose();
@@ -18449,7 +19359,7 @@ function usePosterTexture2(poster, onError) {
18449
19359
  active = false;
18450
19360
  };
18451
19361
  }, [gl, poster]);
18452
- (0, import_react63.useEffect)(
19362
+ (0, import_react64.useEffect)(
18453
19363
  () => () => {
18454
19364
  texture?.dispose();
18455
19365
  },
@@ -18508,37 +19418,37 @@ function PanoVideo({
18508
19418
  }) {
18509
19419
  usePanoMediaActivationIntent(autoPlay);
18510
19420
  const gl = (0, import_fiber31.useThree)((state) => state.gl);
18511
- const host = (0, import_react63.useContext)(PanoVideoHostContext);
18512
- const variants = (0, import_react63.useMemo)(
19421
+ const host = (0, import_react64.useContext)(PanoVideoHostContext);
19422
+ const variants = (0, import_react64.useMemo)(
18513
19423
  () => resolvePanoVideoVariants({ poster, src, variants: variantsProp }),
18514
19424
  [poster, src, variantsProp]
18515
19425
  );
18516
19426
  const tracks = tracksProp ?? EMPTY_TRACKS;
18517
19427
  const captionsEnabled = captions !== false;
18518
- const captionAppearance = (0, import_react63.useMemo)(
19428
+ const captionAppearance = (0, import_react64.useMemo)(
18519
19429
  () => resolvePanoVideoCaptionAppearance(
18520
19430
  typeof captions === "object" ? captions : void 0
18521
19431
  ),
18522
19432
  [captions]
18523
19433
  );
18524
- const [variantId, setVariantId] = (0, import_react63.useState)(
19434
+ const [variantId, setVariantId] = (0, import_react64.useState)(
18525
19435
  () => resolvePanoVideoVariantId(variants, defaultVariantId)
18526
19436
  );
18527
- const [trackId, setTrackId] = (0, import_react63.useState)(
19437
+ const [trackId, setTrackId] = (0, import_react64.useState)(
18528
19438
  () => captionsEnabled ? defaultPanoVideoTrackId(tracks) : null
18529
19439
  );
18530
- const [ready, setReady] = (0, import_react63.useState)(false);
18531
- const autoPlayRef = (0, import_react63.useRef)(autoPlay);
19440
+ const [ready, setReady] = (0, import_react64.useState)(false);
19441
+ const autoPlayRef = (0, import_react64.useRef)(autoPlay);
18532
19442
  autoPlayRef.current = autoPlay;
18533
- const blockedRef = (0, import_react63.useRef)(false);
18534
- const captionAppearanceOverrideRef = (0, import_react63.useRef)(
19443
+ const blockedRef = (0, import_react64.useRef)(false);
19444
+ const captionAppearanceOverrideRef = (0, import_react64.useRef)(
18535
19445
  {}
18536
19446
  );
18537
- const mediaCapabilitiesRef = (0, import_react63.useRef)({
19447
+ const mediaCapabilitiesRef = (0, import_react64.useRef)({
18538
19448
  volumeAdjustable: true,
18539
19449
  playbackRates: [...DEFAULT_PANO_VIDEO_PLAYBACK_RATES]
18540
19450
  });
18541
- const storeRef = (0, import_react63.useRef)(null);
19451
+ const storeRef = (0, import_react64.useRef)(null);
18542
19452
  if (!storeRef.current) {
18543
19453
  storeRef.current = {
18544
19454
  snapshot: {
@@ -18548,7 +19458,7 @@ function PanoVideo({
18548
19458
  listeners: /* @__PURE__ */ new Set()
18549
19459
  };
18550
19460
  }
18551
- const actionsRef = (0, import_react63.useRef)({
19461
+ const actionsRef = (0, import_react64.useRef)({
18552
19462
  play: async () => {
18553
19463
  },
18554
19464
  pause: () => {
@@ -18568,19 +19478,19 @@ function PanoVideo({
18568
19478
  setCaptionAppearance: (_appearance) => {
18569
19479
  }
18570
19480
  });
18571
- const onLoadRef = (0, import_react63.useRef)(onLoad);
18572
- const onErrorRef = (0, import_react63.useRef)(onError);
18573
- const onPlaybackStateChangeRef = (0, import_react63.useRef)(onPlaybackStateChange);
18574
- const onEndedRef = (0, import_react63.useRef)(onEnded);
18575
- const onVariantChangeRef = (0, import_react63.useRef)(onVariantChange);
18576
- const onTrackChangeRef = (0, import_react63.useRef)(onTrackChange);
19481
+ const onLoadRef = (0, import_react64.useRef)(onLoad);
19482
+ const onErrorRef = (0, import_react64.useRef)(onError);
19483
+ const onPlaybackStateChangeRef = (0, import_react64.useRef)(onPlaybackStateChange);
19484
+ const onEndedRef = (0, import_react64.useRef)(onEnded);
19485
+ const onVariantChangeRef = (0, import_react64.useRef)(onVariantChange);
19486
+ const onTrackChangeRef = (0, import_react64.useRef)(onTrackChange);
18577
19487
  onLoadRef.current = onLoad;
18578
19488
  onErrorRef.current = onError;
18579
19489
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
18580
19490
  onEndedRef.current = onEnded;
18581
19491
  onVariantChangeRef.current = onVariantChange;
18582
19492
  onTrackChangeRef.current = onTrackChange;
18583
- const resource = (0, import_react63.useMemo)(() => {
19493
+ const resource = (0, import_react64.useMemo)(() => {
18584
19494
  if (typeof document === "undefined") {
18585
19495
  return null;
18586
19496
  }
@@ -18595,7 +19505,7 @@ function PanoVideo({
18595
19505
  texture.generateMipmaps = false;
18596
19506
  return { video, texture };
18597
19507
  }, []);
18598
- (0, import_react63.useEffect)(
19508
+ (0, import_react64.useEffect)(
18599
19509
  () => () => {
18600
19510
  if (!resource) {
18601
19511
  return;
@@ -18610,13 +19520,13 @@ function PanoVideo({
18610
19520
  },
18611
19521
  [resource]
18612
19522
  );
18613
- (0, import_react63.useEffect)(() => {
19523
+ (0, import_react64.useEffect)(() => {
18614
19524
  if (!resource) {
18615
19525
  return;
18616
19526
  }
18617
19527
  resource.texture.anisotropy = gl.capabilities.getMaxAnisotropy();
18618
19528
  }, [gl, resource]);
18619
- (0, import_react63.useLayoutEffect)(() => {
19529
+ (0, import_react64.useLayoutEffect)(() => {
18620
19530
  if (!resource) {
18621
19531
  return;
18622
19532
  }
@@ -18632,7 +19542,7 @@ function PanoVideo({
18632
19542
  activeVariant?.poster ?? poster,
18633
19543
  onError
18634
19544
  );
18635
- const emitSnapshot = (0, import_react63.useCallback)(
19545
+ const emitSnapshot = (0, import_react64.useCallback)(
18636
19546
  (patch) => {
18637
19547
  const store = storeRef.current;
18638
19548
  const video = resource?.video;
@@ -18684,7 +19594,7 @@ function PanoVideo({
18684
19594
  captionAppearance
18685
19595
  ]
18686
19596
  );
18687
- const controller = (0, import_react63.useMemo)(
19597
+ const controller = (0, import_react64.useMemo)(
18688
19598
  () => ({
18689
19599
  subscribe: (onStoreChange) => {
18690
19600
  const store = storeRef.current;
@@ -18736,7 +19646,7 @@ function PanoVideo({
18736
19646
  }),
18737
19647
  []
18738
19648
  );
18739
- (0, import_react63.useLayoutEffect)(() => {
19649
+ (0, import_react64.useLayoutEffect)(() => {
18740
19650
  if (!host || variants.length === 0) {
18741
19651
  return;
18742
19652
  }
@@ -18749,7 +19659,7 @@ function PanoVideo({
18749
19659
  notifyPanoVideoHost(host);
18750
19660
  };
18751
19661
  }, [captionsEnabled, controller, controls, host, variants.length]);
18752
- (0, import_react63.useLayoutEffect)(() => {
19662
+ (0, import_react64.useLayoutEffect)(() => {
18753
19663
  if (!resource) {
18754
19664
  return;
18755
19665
  }
@@ -18762,10 +19672,10 @@ function PanoVideo({
18762
19672
  };
18763
19673
  emitSnapshot();
18764
19674
  }, [emitSnapshot, playbackRates, resource]);
18765
- (0, import_react63.useEffect)(() => {
19675
+ (0, import_react64.useEffect)(() => {
18766
19676
  emitSnapshot();
18767
19677
  }, [emitSnapshot]);
18768
- (0, import_react63.useEffect)(() => {
19678
+ (0, import_react64.useEffect)(() => {
18769
19679
  if (!resource || !activeVariant || !mediaKey) {
18770
19680
  return;
18771
19681
  }
@@ -18822,7 +19732,7 @@ function PanoVideo({
18822
19732
  video.removeEventListener("error", handleError);
18823
19733
  };
18824
19734
  }, [mediaKey, resource]);
18825
- (0, import_react63.useEffect)(() => {
19735
+ (0, import_react64.useEffect)(() => {
18826
19736
  if (!resource) {
18827
19737
  return;
18828
19738
  }
@@ -18914,137 +19824,1292 @@ function PanoVideo({
18914
19824
  }
18915
19825
  };
18916
19826
  }, [emitSnapshot, resource]);
18917
- (0, import_react63.useLayoutEffect)(() => {
19827
+ (0, import_react64.useLayoutEffect)(() => {
19828
+ if (!resource) {
19829
+ return;
19830
+ }
19831
+ const { video } = resource;
19832
+ video.loop = loop;
19833
+ video.muted = muted;
19834
+ if (mediaCapabilitiesRef.current.volumeAdjustable) {
19835
+ video.volume = clampVolume(volume);
19836
+ }
19837
+ applyInlineVideoPlayback(video, playsInline);
19838
+ video.preload = preload;
19839
+ }, [loop, muted, playsInline, preload, resource, volume]);
19840
+ (0, import_react64.useEffect)(() => {
19841
+ if (!resource) {
19842
+ return;
19843
+ }
19844
+ const { video } = resource;
19845
+ let active = true;
19846
+ const handleTimeUpdate = () => {
19847
+ if (active) {
19848
+ emitSnapshot();
19849
+ }
19850
+ };
19851
+ const handlePlay = () => {
19852
+ blockedRef.current = false;
19853
+ onPlaybackStateChangeRef.current?.("playing");
19854
+ emitSnapshot();
19855
+ };
19856
+ const handlePause = () => {
19857
+ onPlaybackStateChangeRef.current?.(
19858
+ video.ended ? "ended" : "paused"
19859
+ );
19860
+ emitSnapshot();
19861
+ };
19862
+ const handleEnded = () => {
19863
+ onPlaybackStateChangeRef.current?.("ended");
19864
+ onEndedRef.current?.();
19865
+ emitSnapshot();
19866
+ };
19867
+ const handleVolume = () => {
19868
+ emitSnapshot();
19869
+ };
19870
+ video.addEventListener("timeupdate", handleTimeUpdate);
19871
+ video.addEventListener("durationchange", handleTimeUpdate);
19872
+ video.addEventListener("progress", handleTimeUpdate);
19873
+ video.addEventListener("play", handlePlay);
19874
+ video.addEventListener("pause", handlePause);
19875
+ video.addEventListener("ended", handleEnded);
19876
+ video.addEventListener("volumechange", handleVolume);
19877
+ video.addEventListener("ratechange", handleVolume);
19878
+ return () => {
19879
+ active = false;
19880
+ video.removeEventListener("timeupdate", handleTimeUpdate);
19881
+ video.removeEventListener("durationchange", handleTimeUpdate);
19882
+ video.removeEventListener("progress", handleTimeUpdate);
19883
+ video.removeEventListener("play", handlePlay);
19884
+ video.removeEventListener("pause", handlePause);
19885
+ video.removeEventListener("ended", handleEnded);
19886
+ video.removeEventListener("volumechange", handleVolume);
19887
+ video.removeEventListener("ratechange", handleVolume);
19888
+ };
19889
+ }, [emitSnapshot, resource]);
19890
+ (0, import_react64.useEffect)(() => {
18918
19891
  if (!resource) {
18919
19892
  return;
18920
19893
  }
18921
- const { video } = resource;
18922
- video.loop = loop;
18923
- video.muted = muted;
18924
- if (mediaCapabilitiesRef.current.volumeAdjustable) {
18925
- video.volume = clampVolume(volume);
18926
- }
18927
- applyInlineVideoPlayback(video, playsInline);
18928
- video.preload = preload;
18929
- }, [loop, muted, playsInline, preload, resource, volume]);
18930
- (0, import_react63.useEffect)(() => {
18931
- if (!resource) {
19894
+ const { video } = resource;
19895
+ const applyModes = () => {
19896
+ const selected2 = findTextTrack(video, tracks, trackId);
19897
+ const list = video.textTracks;
19898
+ for (let index = 0; index < list.length; index += 1) {
19899
+ const textTrack = list[index];
19900
+ if (!textTrack) {
19901
+ continue;
19902
+ }
19903
+ textTrack.mode = textTrack === selected2 ? "hidden" : "disabled";
19904
+ }
19905
+ emitSnapshot({ captionText: collectCaptionText(selected2) });
19906
+ };
19907
+ applyModes();
19908
+ const selected = findTextTrack(video, tracks, trackId);
19909
+ const handleCueChange = () => {
19910
+ emitSnapshot({
19911
+ captionText: collectCaptionText(
19912
+ findTextTrack(video, tracks, trackId)
19913
+ )
19914
+ });
19915
+ };
19916
+ selected?.addEventListener("cuechange", handleCueChange);
19917
+ video.textTracks.addEventListener("addtrack", applyModes);
19918
+ video.textTracks.addEventListener("change", applyModes);
19919
+ return () => {
19920
+ selected?.removeEventListener("cuechange", handleCueChange);
19921
+ video.textTracks.removeEventListener("addtrack", applyModes);
19922
+ video.textTracks.removeEventListener("change", applyModes);
19923
+ };
19924
+ }, [emitSnapshot, resource, trackId, tracks]);
19925
+ (0, import_react64.useEffect)(() => {
19926
+ if (!captionsEnabled) {
19927
+ setTrackId(null);
19928
+ return;
19929
+ }
19930
+ setTrackId(defaultPanoVideoTrackId(tracks));
19931
+ }, [captionsEnabled, tracks]);
19932
+ const activeTexture = ready && resource ? resource.texture : posterTexture;
19933
+ if (variants.length === 0) {
19934
+ return null;
19935
+ }
19936
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
19937
+ activeTexture ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
19938
+ "mesh",
19939
+ {
19940
+ rotation: [0, import_three40.MathUtils.degToRad(90 + yawOffset2), 0],
19941
+ visible,
19942
+ children: [
19943
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
19944
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
19945
+ PanoBasicMaterial,
19946
+ {
19947
+ map: activeTexture,
19948
+ side: import_three40.BackSide,
19949
+ toneMapped: false
19950
+ }
19951
+ )
19952
+ ]
19953
+ }
19954
+ ) : null,
19955
+ captionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
19956
+ ] });
19957
+ }
19958
+
19959
+ // src/map/pano-map.tsx
19960
+ var import_react66 = require("react");
19961
+
19962
+ // src/map/geometry.ts
19963
+ function sectorRotation(heading, yaw) {
19964
+ return heading + yaw;
19965
+ }
19966
+ function horizontalFov(verticalFov, aspect) {
19967
+ if (!(aspect > 0) || !(verticalFov > 0) || verticalFov >= 180) return verticalFov;
19968
+ const verticalRad = verticalFov * Math.PI / 180;
19969
+ const horizontalRad = 2 * Math.atan(Math.tan(verticalRad / 2) * aspect);
19970
+ return horizontalRad * 180 / Math.PI;
19971
+ }
19972
+ function containRect(containerWidth, containerHeight, imageWidth, imageHeight) {
19973
+ if (!(containerWidth > 0) || !(containerHeight > 0) || !(imageWidth > 0) || !(imageHeight > 0)) {
19974
+ return { left: 0, top: 0, width: 0, height: 0 };
19975
+ }
19976
+ const scale = Math.min(
19977
+ containerWidth / imageWidth,
19978
+ containerHeight / imageHeight
19979
+ );
19980
+ const width = imageWidth * scale;
19981
+ const height = imageHeight * scale;
19982
+ return {
19983
+ left: (containerWidth - width) / 2,
19984
+ top: (containerHeight - height) / 2,
19985
+ width,
19986
+ height
19987
+ };
19988
+ }
19989
+ function imagePointToContainer(point, viewport) {
19990
+ const { fit, scale, panX, panY } = viewport;
19991
+ return {
19992
+ x: fit.left + panX + point.x * fit.width * scale,
19993
+ y: fit.top + panY + point.y * fit.height * scale
19994
+ };
19995
+ }
19996
+ function containerPointToImage(point, viewport) {
19997
+ const { fit, scale, panX, panY } = viewport;
19998
+ const width = fit.width * scale;
19999
+ const height = fit.height * scale;
20000
+ if (!(width > 0) || !(height > 0)) return null;
20001
+ return {
20002
+ x: (point.x - fit.left - panX) / width,
20003
+ y: (point.y - fit.top - panY) / height
20004
+ };
20005
+ }
20006
+ function clamp012(value) {
20007
+ return Math.min(1, Math.max(0, value));
20008
+ }
20009
+ function headingFromVector(dx, dy) {
20010
+ if (dx === 0 && dy === 0) return 0;
20011
+ const radians = Math.atan2(dx, -dy);
20012
+ return radians * 180 / Math.PI;
20013
+ }
20014
+ function normalizeDegrees(value) {
20015
+ const wrapped = (value % 360 + 360) % 360;
20016
+ return wrapped > 180 ? wrapped - 360 : wrapped;
20017
+ }
20018
+
20019
+ // src/map/use-map-view.ts
20020
+ var import_react65 = require("react");
20021
+ var DEFAULT_VIEW2 = { yaw: 0, pitch: 0, fov: 75 };
20022
+ function useMapView(controlled) {
20023
+ const eventBus = (0, import_react65.useContext)(PanoEventBusContext);
20024
+ const [view, setView] = (0, import_react65.useState)(controlled ?? DEFAULT_VIEW2);
20025
+ const controlledRef = (0, import_react65.useRef)(controlled);
20026
+ controlledRef.current = controlled;
20027
+ (0, import_react65.useEffect)(() => {
20028
+ if (controlled !== void 0) {
20029
+ setView(controlled);
20030
+ return;
20031
+ }
20032
+ if (!eventBus) return;
20033
+ return eventBus.subscribe("viewchange", (next) => {
20034
+ if (controlledRef.current !== void 0) return;
20035
+ setView(next);
20036
+ });
20037
+ }, [controlled, eventBus]);
20038
+ return controlled ?? view;
20039
+ }
20040
+
20041
+ // src/map/pano-map.tsx
20042
+ var import_jsx_runtime43 = require("react/jsx-runtime");
20043
+ var DEFAULT_ICON_SIZE2 = 28;
20044
+ var DEFAULT_SECTOR_COLOR = "#ffffff";
20045
+ var DEFAULT_SECTOR_OPACITY = 0.55;
20046
+ var MIN_SCALE = 1;
20047
+ var DEFAULT_MAX_SCALE = 8;
20048
+ var ZOOM_STEP = 1.25;
20049
+ function defaultGradientId(prefix) {
20050
+ return `${prefix}-sector-gradient`;
20051
+ }
20052
+ function SectorShape({
20053
+ sector,
20054
+ angle,
20055
+ rotation,
20056
+ gradientId,
20057
+ interactive,
20058
+ onHeadingPointerDown,
20059
+ transformRef
20060
+ }) {
20061
+ const half = angle / 2;
20062
+ const start = (-half - 90) * Math.PI / 180;
20063
+ const end = (half - 90) * Math.PI / 180;
20064
+ const radius = sector.radius;
20065
+ const largeArc = angle > 180 ? 1 : 0;
20066
+ const x1 = Math.cos(start) * radius;
20067
+ const y1 = Math.sin(start) * radius;
20068
+ const x2 = Math.cos(end) * radius;
20069
+ const y2 = Math.sin(end) * radius;
20070
+ const path = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
20071
+ const opacity = sector.opacity ?? DEFAULT_SECTOR_OPACITY;
20072
+ const color = sector.color ?? DEFAULT_SECTOR_COLOR;
20073
+ const gradient = sector.gradient;
20074
+ const strokeWidth = sector.strokeWidth ?? 0;
20075
+ const strokeColor = sector.strokeColor ?? color;
20076
+ const strokeOpacity = sector.strokeOpacity ?? 1;
20077
+ const gradientStrength = gradient && gradient.stops.length > 0 ? Math.max(
20078
+ ...gradient.stops.map((stop) => stop.opacity ?? 1),
20079
+ Number.EPSILON
20080
+ ) : 1;
20081
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("g", { ref: transformRef, transform: `rotate(${rotation})`, style: { pointerEvents: "none" }, children: [
20082
+ gradient ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("defs", { children: gradient.type === "radial" ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("radialGradient", { id: gradientId, cx: "0", cy: "0", r: radius, gradientUnits: "userSpaceOnUse", children: gradient.stops.map((stop, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20083
+ "stop",
20084
+ {
20085
+ offset: `${stop.offset * 100}%`,
20086
+ stopColor: stop.color,
20087
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
20088
+ },
20089
+ `${stop.offset}:${index}`
20090
+ )) }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20091
+ "linearGradient",
20092
+ {
20093
+ id: gradientId,
20094
+ x1: "0",
20095
+ y1: "0",
20096
+ x2: "0",
20097
+ y2: -radius,
20098
+ gradientUnits: "userSpaceOnUse",
20099
+ children: gradient.stops.map((stop, index) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20100
+ "stop",
20101
+ {
20102
+ offset: `${stop.offset * 100}%`,
20103
+ stopColor: stop.color,
20104
+ stopOpacity: (stop.opacity ?? 1) / gradientStrength
20105
+ },
20106
+ `${stop.offset}:${index}`
20107
+ ))
20108
+ }
20109
+ ) }) : null,
20110
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20111
+ "path",
20112
+ {
20113
+ d: path,
20114
+ fill: gradient ? `url(#${gradientId})` : color,
20115
+ fillOpacity: opacity,
20116
+ stroke: strokeWidth > 0 ? strokeColor : "none",
20117
+ strokeWidth: strokeWidth > 0 ? strokeWidth : void 0,
20118
+ strokeOpacity: strokeWidth > 0 ? strokeOpacity : void 0,
20119
+ vectorEffect: "non-scaling-stroke",
20120
+ style: {
20121
+ pointerEvents: interactive ? "auto" : "none",
20122
+ cursor: interactive ? "grab" : void 0
20123
+ },
20124
+ onPointerDown: interactive ? onHeadingPointerDown : void 0
20125
+ }
20126
+ )
20127
+ ] });
20128
+ }
20129
+ function DefaultSpotIcon({ active, size }) {
20130
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
20131
+ "svg",
20132
+ {
20133
+ width: size,
20134
+ height: size,
20135
+ viewBox: "0 0 32 32",
20136
+ "aria-hidden": "true",
20137
+ style: { display: "block" },
20138
+ children: [
20139
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20140
+ "rect",
20141
+ {
20142
+ x: "3",
20143
+ y: "7",
20144
+ width: "26",
20145
+ height: "18",
20146
+ rx: "5",
20147
+ fill: active ? "rgba(15,23,42,0.92)" : "rgba(15,23,42,0.72)",
20148
+ stroke: "rgba(255,255,255,0.92)",
20149
+ strokeWidth: "2"
20150
+ }
20151
+ ),
20152
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("circle", { cx: "16", cy: "16", r: "5", fill: "none", stroke: "rgba(255,255,255,0.92)", strokeWidth: "2" }),
20153
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("circle", { cx: "16", cy: "16", r: "2", fill: active ? "#ef4444" : "rgba(255,255,255,0.85)" })
20154
+ ]
20155
+ }
20156
+ );
20157
+ }
20158
+ function PanoMap({
20159
+ map,
20160
+ activeSceneId,
20161
+ onSelectScene,
20162
+ view: controlledView,
20163
+ mode = "view",
20164
+ selectedSpotId = null,
20165
+ onSelectedSpotIdChange,
20166
+ onEditChange,
20167
+ placementSceneId,
20168
+ className,
20169
+ style,
20170
+ showZoomControls,
20171
+ showCollapseControl,
20172
+ collapsed: controlledCollapsed,
20173
+ onCollapsedChange,
20174
+ renderChrome,
20175
+ initialScale
20176
+ }) {
20177
+ const reactId = (0, import_react66.useId)().replace(/:/g, "");
20178
+ const view = useMapView(controlledView);
20179
+ const rootRef = (0, import_react66.useRef)(null);
20180
+ const containerRef = (0, import_react66.useRef)(null);
20181
+ const sectorNodeRef = (0, import_react66.useRef)(null);
20182
+ const yawRef = (0, import_react66.useRef)(view.yaw);
20183
+ const [size, setSize] = (0, import_react66.useState)({ width: 0, height: 0 });
20184
+ const [imageSize2, setImageSize] = (0, import_react66.useState)({ width: 0, height: 0 });
20185
+ const [scale, setScale] = (0, import_react66.useState)(1);
20186
+ const [pan, setPan] = (0, import_react66.useState)({ x: 0, y: 0 });
20187
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = (0, import_react66.useState)(false);
20188
+ const dragRef = (0, import_react66.useRef)(null);
20189
+ const panAnimRef = (0, import_react66.useRef)(null);
20190
+ const panRef = (0, import_react66.useRef)(pan);
20191
+ const scaleRef = (0, import_react66.useRef)(scale);
20192
+ const initialViewKeyRef = (0, import_react66.useRef)(null);
20193
+ const spots = map.spots;
20194
+ const iconSize = map.iconSize ?? DEFAULT_ICON_SIZE2;
20195
+ const interaction = map.interaction ?? "fixed";
20196
+ const gestureZoom = interaction === "zoom";
20197
+ const dragMotion = resolveDragMotion(map.dragMotion);
20198
+ const maxScale = Math.max(MIN_SCALE, map.maxScale ?? DEFAULT_MAX_SCALE);
20199
+ const resolvedInitialScale = Math.min(
20200
+ maxScale,
20201
+ Math.max(
20202
+ MIN_SCALE,
20203
+ Number.isFinite(initialScale) ? initialScale : MIN_SCALE
20204
+ )
20205
+ );
20206
+ const zoomControls = gestureZoom && (showZoomControls ?? map.controls?.zoom === true);
20207
+ const collapseControl = showCollapseControl ?? map.controls?.collapse === true;
20208
+ const collapsed = controlledCollapsed ?? uncontrolledCollapsed;
20209
+ const editing = mode === "edit";
20210
+ const fit = (0, import_react66.useMemo)(
20211
+ () => containRect(size.width, size.height, imageSize2.width, imageSize2.height),
20212
+ [size, imageSize2]
20213
+ );
20214
+ const viewport = (0, import_react66.useMemo)(
20215
+ () => ({ fit, scale, panX: pan.x, panY: pan.y }),
20216
+ [fit, scale, pan]
20217
+ );
20218
+ (0, import_react66.useLayoutEffect)(() => {
20219
+ if (collapsed) return;
20220
+ const node = containerRef.current;
20221
+ if (!node) return;
20222
+ const measure = () => {
20223
+ const width = node.clientWidth;
20224
+ const height = node.clientHeight;
20225
+ if (!(width > 0) || !(height > 0)) return;
20226
+ setSize(
20227
+ (previous) => previous.width === width && previous.height === height ? previous : { width, height }
20228
+ );
20229
+ };
20230
+ measure();
20231
+ const observer = new ResizeObserver(measure);
20232
+ observer.observe(node);
20233
+ return () => observer.disconnect();
20234
+ }, [collapsed]);
20235
+ (0, import_react66.useEffect)(() => {
20236
+ yawRef.current = view.yaw;
20237
+ const node = sectorNodeRef.current;
20238
+ if (!node) return;
20239
+ const active = spots.find((spot) => spot.sceneId === activeSceneId);
20240
+ if (!active || !map.sector) return;
20241
+ node.setAttribute(
20242
+ "transform",
20243
+ `rotate(${sectorRotation(active.heading, view.yaw)})`
20244
+ );
20245
+ }, [view.yaw, activeSceneId, spots, map.sector]);
20246
+ const setCollapsed = (next) => {
20247
+ onCollapsedChange?.(next);
20248
+ if (controlledCollapsed === void 0) setUncontrolledCollapsed(next);
20249
+ };
20250
+ panRef.current = pan;
20251
+ scaleRef.current = scale;
20252
+ const panBoundsFor = (0, import_react66.useCallback)(
20253
+ (nextScale) => {
20254
+ const width = fit.width * nextScale;
20255
+ const height = fit.height * nextScale;
20256
+ if (!(fit.width > 0) || !(fit.height > 0)) {
20257
+ return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
20258
+ }
20259
+ return {
20260
+ minX: Math.min(0, size.width - (fit.left + width)),
20261
+ maxX: Math.max(0, -fit.left),
20262
+ minY: Math.min(0, size.height - (fit.top + height)),
20263
+ maxY: Math.max(0, -fit.top)
20264
+ };
20265
+ },
20266
+ [fit, size]
20267
+ );
20268
+ const clampPan = (0, import_react66.useCallback)(
20269
+ (nextScale, nextPan) => {
20270
+ const bounds = panBoundsFor(nextScale);
20271
+ return {
20272
+ x: Math.min(bounds.maxX, Math.max(bounds.minX, nextPan.x)),
20273
+ y: Math.min(bounds.maxY, Math.max(bounds.minY, nextPan.y))
20274
+ };
20275
+ },
20276
+ [panBoundsFor]
20277
+ );
20278
+ const cancelPanAnimation = (0, import_react66.useCallback)(() => {
20279
+ const anim = panAnimRef.current;
20280
+ if (anim?.raf != null) {
20281
+ cancelAnimationFrame(anim.raf);
20282
+ }
20283
+ panAnimRef.current = null;
20284
+ }, []);
20285
+ const startPanAnimation = (0, import_react66.useCallback)(
20286
+ (velocity) => {
20287
+ cancelPanAnimation();
20288
+ const current = panRef.current;
20289
+ const currentScale = scaleRef.current;
20290
+ const bounds = panBoundsFor(currentScale);
20291
+ const needsSpring = dragMotion.bounce && (current.x < bounds.minX || current.x > bounds.maxX || current.y < bounds.minY || current.y > bounds.maxY);
20292
+ const vx = dragMotion.inertia ? velocity.x : 0;
20293
+ const vy = dragMotion.inertia ? velocity.y : 0;
20294
+ if (!needsSpring && Math.abs(vx) < 1 && Math.abs(vy) < 1) {
20295
+ setPan(clampPan(currentScale, current));
20296
+ return;
20297
+ }
20298
+ if (!dragMotion.inertia && !dragMotion.bounce) {
20299
+ setPan(clampPan(currentScale, current));
20300
+ return;
20301
+ }
20302
+ const anim = {
20303
+ raf: null,
20304
+ lastTime: performance.now(),
20305
+ x: { position: current.x, velocity: vx },
20306
+ y: { position: current.y, velocity: vy }
20307
+ };
20308
+ panAnimRef.current = anim;
20309
+ const tick = (now) => {
20310
+ if (panAnimRef.current !== anim) return;
20311
+ const dt = Math.min(0.064, Math.max(0, (now - anim.lastTime) / 1e3));
20312
+ anim.lastTime = now;
20313
+ const liveBounds = panBoundsFor(scaleRef.current);
20314
+ const nextX = integrateAxis(anim.x, dt, {
20315
+ min: liveBounds.minX,
20316
+ max: liveBounds.maxX,
20317
+ inertia: dragMotion.inertia,
20318
+ bounce: dragMotion.bounce
20319
+ });
20320
+ const nextY = integrateAxis(anim.y, dt, {
20321
+ min: liveBounds.minY,
20322
+ max: liveBounds.maxY,
20323
+ inertia: dragMotion.inertia,
20324
+ bounce: dragMotion.bounce
20325
+ });
20326
+ anim.x = { position: nextX.position, velocity: nextX.velocity };
20327
+ anim.y = { position: nextY.position, velocity: nextY.velocity };
20328
+ setPan({ x: nextX.position, y: nextY.position });
20329
+ if (nextX.settled && nextY.settled) {
20330
+ setPan(
20331
+ clampPan(scaleRef.current, {
20332
+ x: nextX.position,
20333
+ y: nextY.position
20334
+ })
20335
+ );
20336
+ panAnimRef.current = null;
20337
+ return;
20338
+ }
20339
+ anim.raf = requestAnimationFrame(tick);
20340
+ };
20341
+ anim.raf = requestAnimationFrame(tick);
20342
+ },
20343
+ [cancelPanAnimation, clampPan, dragMotion.bounce, dragMotion.inertia, panBoundsFor]
20344
+ );
20345
+ (0, import_react66.useEffect)(() => () => cancelPanAnimation(), [cancelPanAnimation]);
20346
+ (0, import_react66.useEffect)(() => {
20347
+ if (collapsed) cancelPanAnimation();
20348
+ }, [cancelPanAnimation, collapsed]);
20349
+ (0, import_react66.useEffect)(() => {
20350
+ if (!gestureZoom) {
20351
+ initialViewKeyRef.current = null;
20352
+ setScale(1);
20353
+ setPan({ x: 0, y: 0 });
20354
+ return;
20355
+ }
20356
+ setScale((previous) => {
20357
+ const next = Math.min(previous, maxScale);
20358
+ if (next !== previous) {
20359
+ setPan((currentPan) => clampPan(next, currentPan));
20360
+ }
20361
+ return next;
20362
+ });
20363
+ }, [clampPan, gestureZoom, maxScale]);
20364
+ (0, import_react66.useEffect)(() => {
20365
+ if (!gestureZoom) return;
20366
+ if (!(fit.width > 0) || !(fit.height > 0)) return;
20367
+ if (!(size.width > 0) || !(size.height > 0)) return;
20368
+ const key = `${interaction}:${initialScale ?? MIN_SCALE}`;
20369
+ if (initialViewKeyRef.current === key) return;
20370
+ initialViewKeyRef.current = key;
20371
+ const pivot = { x: size.width / 2, y: size.height / 2 };
20372
+ const nextPan = {
20373
+ x: pivot.x - (pivot.x - fit.left) * resolvedInitialScale - fit.left,
20374
+ y: pivot.y - (pivot.y - fit.top) * resolvedInitialScale - fit.top
20375
+ };
20376
+ setScale(resolvedInitialScale);
20377
+ setPan(clampPan(resolvedInitialScale, nextPan));
20378
+ }, [
20379
+ clampPan,
20380
+ fit.height,
20381
+ fit.left,
20382
+ fit.top,
20383
+ fit.width,
20384
+ gestureZoom,
20385
+ initialScale,
20386
+ interaction,
20387
+ resolvedInitialScale,
20388
+ size.height,
20389
+ size.width
20390
+ ]);
20391
+ const applyZoom = (0, import_react66.useCallback)(
20392
+ (nextScale, origin) => {
20393
+ if (!gestureZoom) return;
20394
+ cancelPanAnimation();
20395
+ const clamped = Math.min(maxScale, Math.max(MIN_SCALE, nextScale));
20396
+ const pivot = origin ?? { x: size.width / 2, y: size.height / 2 };
20397
+ const ratio = clamped / scale;
20398
+ const nextPan = {
20399
+ x: pivot.x - (pivot.x - (fit.left + pan.x)) * ratio - fit.left,
20400
+ y: pivot.y - (pivot.y - (fit.top + pan.y)) * ratio - fit.top
20401
+ };
20402
+ setScale(clamped);
20403
+ setPan(clampPan(clamped, nextPan));
20404
+ },
20405
+ [
20406
+ cancelPanAnimation,
20407
+ clampPan,
20408
+ fit.left,
20409
+ fit.top,
20410
+ gestureZoom,
20411
+ maxScale,
20412
+ pan.x,
20413
+ pan.y,
20414
+ scale,
20415
+ size.height,
20416
+ size.width
20417
+ ]
20418
+ );
20419
+ (0, import_react66.useEffect)(() => {
20420
+ const root = rootRef.current;
20421
+ if (!root) return;
20422
+ const onWheel = (event) => {
20423
+ event.preventDefault();
20424
+ event.stopPropagation();
20425
+ if (!gestureZoom || collapsed) return;
20426
+ const viewport2 = containerRef.current;
20427
+ if (!viewport2) return;
20428
+ const rect = viewport2.getBoundingClientRect();
20429
+ const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
20430
+ applyZoom(scale * factor, {
20431
+ x: event.clientX - rect.left,
20432
+ y: event.clientY - rect.top
20433
+ });
20434
+ };
20435
+ root.addEventListener("wheel", onWheel, { passive: false });
20436
+ return () => root.removeEventListener("wheel", onWheel);
20437
+ }, [applyZoom, collapsed, gestureZoom, scale]);
20438
+ const updateSpots = (next, selected) => {
20439
+ onEditChange?.({
20440
+ spots: next,
20441
+ ...selected === void 0 ? {} : { selectedSpotId: selected }
20442
+ });
20443
+ };
20444
+ const localPoint = (event) => {
20445
+ const rect = containerRef.current?.getBoundingClientRect();
20446
+ if (!rect) return null;
20447
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
20448
+ };
20449
+ const onPointerDownSurface = (event) => {
20450
+ if (collapsed) return;
20451
+ if (event.button !== 0) return;
20452
+ const point = localPoint(event);
20453
+ if (!point) return;
20454
+ if (editing && placementSceneId) {
20455
+ const image = containerPointToImage(point, viewport);
20456
+ if (!image) return;
20457
+ const spot = {
20458
+ id: crypto.randomUUID(),
20459
+ sceneId: placementSceneId,
20460
+ x: clamp012(image.x),
20461
+ y: clamp012(image.y),
20462
+ heading: normalizeDegrees(-view.yaw)
20463
+ };
20464
+ updateSpots([...spots, spot], spot.id);
20465
+ onSelectedSpotIdChange?.(spot.id);
20466
+ event.preventDefault();
20467
+ event.stopPropagation();
20468
+ return;
20469
+ }
20470
+ if (gestureZoom && scale > 1) {
20471
+ cancelPanAnimation();
20472
+ const now = performance.now();
20473
+ dragRef.current = {
20474
+ kind: "pan",
20475
+ pointerId: event.pointerId,
20476
+ originX: point.x,
20477
+ originY: point.y,
20478
+ panX: pan.x,
20479
+ panY: pan.y,
20480
+ samplesX: [{ t: now, position: pan.x }],
20481
+ samplesY: [{ t: now, position: pan.y }]
20482
+ };
20483
+ try {
20484
+ event.currentTarget.setPointerCapture(event.pointerId);
20485
+ } catch {
20486
+ }
20487
+ event.preventDefault();
20488
+ event.stopPropagation();
20489
+ }
20490
+ };
20491
+ const onPointerMoveSurface = (event) => {
20492
+ const drag = dragRef.current;
20493
+ if (!drag || drag.pointerId !== event.pointerId) return;
20494
+ const point = localPoint(event);
20495
+ if (!point) return;
20496
+ if (drag.kind === "pan") {
20497
+ const rawX = drag.panX + (point.x - drag.originX);
20498
+ const rawY = drag.panY + (point.y - drag.originY);
20499
+ const bounds = panBoundsFor(scale);
20500
+ const now = performance.now();
20501
+ drag.samplesX = pushSample(drag.samplesX, { t: now, position: rawX });
20502
+ drag.samplesY = pushSample(drag.samplesY, { t: now, position: rawY });
20503
+ setPan({
20504
+ x: constrainDragPosition(
20505
+ rawX,
20506
+ bounds.minX,
20507
+ bounds.maxX,
20508
+ size.width,
20509
+ dragMotion.bounce
20510
+ ),
20511
+ y: constrainDragPosition(
20512
+ rawY,
20513
+ bounds.minY,
20514
+ bounds.maxY,
20515
+ size.height,
20516
+ dragMotion.bounce
20517
+ )
20518
+ });
20519
+ event.preventDefault();
20520
+ event.stopPropagation();
20521
+ return;
20522
+ }
20523
+ if (drag.kind === "spot") {
20524
+ const image = containerPointToImage(
20525
+ { x: point.x - drag.offsetX, y: point.y - drag.offsetY },
20526
+ viewport
20527
+ );
20528
+ if (!image) return;
20529
+ updateSpots(
20530
+ spots.map(
20531
+ (spot2) => spot2.id === drag.spotId ? { ...spot2, x: clamp012(image.x), y: clamp012(image.y) } : spot2
20532
+ )
20533
+ );
20534
+ event.preventDefault();
20535
+ event.stopPropagation();
18932
20536
  return;
18933
20537
  }
18934
- const { video } = resource;
18935
- let active = true;
18936
- const handleTimeUpdate = () => {
18937
- if (active) {
18938
- emitSnapshot();
18939
- }
18940
- };
18941
- const handlePlay = () => {
18942
- blockedRef.current = false;
18943
- onPlaybackStateChangeRef.current?.("playing");
18944
- emitSnapshot();
18945
- };
18946
- const handlePause = () => {
18947
- onPlaybackStateChangeRef.current?.(
18948
- video.ended ? "ended" : "paused"
20538
+ const spot = spots.find((entry) => entry.id === drag.spotId);
20539
+ if (!spot) return;
20540
+ const center = imagePointToContainer(spot, viewport);
20541
+ const heading = normalizeDegrees(
20542
+ headingFromVector(point.x - center.x, point.y - center.y) - view.yaw
20543
+ );
20544
+ updateSpots(
20545
+ spots.map(
20546
+ (entry) => entry.id === drag.spotId ? { ...entry, heading } : entry
20547
+ )
20548
+ );
20549
+ if (sectorNodeRef.current && spot.sceneId === activeSceneId) {
20550
+ sectorNodeRef.current.setAttribute(
20551
+ "transform",
20552
+ `rotate(${sectorRotation(heading, view.yaw)})`
18949
20553
  );
18950
- emitSnapshot();
18951
- };
18952
- const handleEnded = () => {
18953
- onPlaybackStateChangeRef.current?.("ended");
18954
- onEndedRef.current?.();
18955
- emitSnapshot();
18956
- };
18957
- const handleVolume = () => {
18958
- emitSnapshot();
18959
- };
18960
- video.addEventListener("timeupdate", handleTimeUpdate);
18961
- video.addEventListener("durationchange", handleTimeUpdate);
18962
- video.addEventListener("progress", handleTimeUpdate);
18963
- video.addEventListener("play", handlePlay);
18964
- video.addEventListener("pause", handlePause);
18965
- video.addEventListener("ended", handleEnded);
18966
- video.addEventListener("volumechange", handleVolume);
18967
- video.addEventListener("ratechange", handleVolume);
18968
- return () => {
18969
- active = false;
18970
- video.removeEventListener("timeupdate", handleTimeUpdate);
18971
- video.removeEventListener("durationchange", handleTimeUpdate);
18972
- video.removeEventListener("progress", handleTimeUpdate);
18973
- video.removeEventListener("play", handlePlay);
18974
- video.removeEventListener("pause", handlePause);
18975
- video.removeEventListener("ended", handleEnded);
18976
- video.removeEventListener("volumechange", handleVolume);
18977
- video.removeEventListener("ratechange", handleVolume);
18978
- };
18979
- }, [emitSnapshot, resource]);
18980
- (0, import_react63.useEffect)(() => {
18981
- if (!resource) {
20554
+ }
20555
+ event.preventDefault();
20556
+ event.stopPropagation();
20557
+ };
20558
+ const endDrag = (event) => {
20559
+ const drag = dragRef.current;
20560
+ if (drag?.pointerId !== event.pointerId) return;
20561
+ dragRef.current = null;
20562
+ try {
20563
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
20564
+ event.currentTarget.releasePointerCapture(event.pointerId);
20565
+ }
20566
+ } catch {
20567
+ }
20568
+ if (drag.kind !== "pan") return;
20569
+ if (event.type === "pointercancel") {
20570
+ setPan(clampPan(scale, panRef.current));
18982
20571
  return;
18983
20572
  }
18984
- const { video } = resource;
18985
- const applyModes = () => {
18986
- const selected2 = findTextTrack(video, tracks, trackId);
18987
- const list = video.textTracks;
18988
- for (let index = 0; index < list.length; index += 1) {
18989
- const textTrack = list[index];
18990
- if (!textTrack) {
18991
- continue;
18992
- }
18993
- textTrack.mode = textTrack === selected2 ? "hidden" : "disabled";
20573
+ startPanAnimation({
20574
+ x: velocityFromSamples(drag.samplesX),
20575
+ y: velocityFromSamples(drag.samplesY)
20576
+ });
20577
+ };
20578
+ const sectorAngle = (() => {
20579
+ if (!map.sector) return 0;
20580
+ if (!map.sector.followFov) return map.sector.angle;
20581
+ const aspect = size.width > 0 && size.height > 0 ? size.width / size.height : 1;
20582
+ return horizontalFov(view.fov, aspect);
20583
+ })();
20584
+ const activeSpot = spots.find((spot) => spot.sceneId === activeSceneId);
20585
+ const selectedSpot = spots.find((spot) => spot.id === selectedSpotId);
20586
+ const controls = /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "pano-map-controls", style: { pointerEvents: "auto" }, children: [
20587
+ collapseControl ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20588
+ "button",
20589
+ {
20590
+ type: "button",
20591
+ className: "pano-map-control",
20592
+ "aria-label": collapsed ? "Expand map" : "Collapse map",
20593
+ "aria-pressed": !collapsed,
20594
+ style: {
20595
+ background: "rgba(15, 23, 42, 0.92)",
20596
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20597
+ color: "#f8fafc"
20598
+ },
20599
+ onClick: () => setCollapsed(!collapsed),
20600
+ children: collapsed ? "\u25A3" : "\u25A2"
18994
20601
  }
18995
- emitSnapshot({ captionText: collectCaptionText(selected2) });
18996
- };
18997
- applyModes();
18998
- const selected = findTextTrack(video, tracks, trackId);
18999
- const handleCueChange = () => {
19000
- emitSnapshot({
19001
- captionText: collectCaptionText(
19002
- findTextTrack(video, tracks, trackId)
20602
+ ) : null,
20603
+ zoomControls && !collapsed ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(import_jsx_runtime43.Fragment, { children: [
20604
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20605
+ "button",
20606
+ {
20607
+ type: "button",
20608
+ className: "pano-map-control",
20609
+ "aria-label": "Zoom in",
20610
+ style: {
20611
+ background: "rgba(15, 23, 42, 0.92)",
20612
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20613
+ color: "#f8fafc"
20614
+ },
20615
+ onClick: () => applyZoom(scale * ZOOM_STEP),
20616
+ children: "+"
20617
+ }
20618
+ ),
20619
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20620
+ "button",
20621
+ {
20622
+ type: "button",
20623
+ className: "pano-map-control",
20624
+ "aria-label": "Zoom out",
20625
+ style: {
20626
+ background: "rgba(15, 23, 42, 0.92)",
20627
+ border: "1px solid rgba(255, 255, 255, 0.28)",
20628
+ color: "#f8fafc"
20629
+ },
20630
+ onClick: () => applyZoom(scale / ZOOM_STEP),
20631
+ children: "\u2212"
20632
+ }
20633
+ )
20634
+ ] }) : null
20635
+ ] });
20636
+ const syncImageSize = (image) => {
20637
+ if (!(image.naturalWidth > 0) || !(image.naturalHeight > 0)) return;
20638
+ setImageSize(
20639
+ (previous) => previous.width === image.naturalWidth && previous.height === image.naturalHeight ? previous : { width: image.naturalWidth, height: image.naturalHeight }
20640
+ );
20641
+ };
20642
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
20643
+ "div",
20644
+ {
20645
+ ref: rootRef,
20646
+ className: ["pano-map", collapsed ? "pano-map-collapsed" : "", className].filter(Boolean).join(" "),
20647
+ style: {
20648
+ position: "relative",
20649
+ overflow: "hidden",
20650
+ width: "100%",
20651
+ height: "100%",
20652
+ pointerEvents: "auto",
20653
+ touchAction: gestureZoom && !collapsed ? "none" : "auto",
20654
+ ...style
20655
+ },
20656
+ "data-pano-map": "",
20657
+ "data-mode": mode,
20658
+ "data-collapsed": collapsed ? "true" : void 0,
20659
+ children: [
20660
+ renderChrome ? renderChrome(controls) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "pano-map-chrome", children: controls }),
20661
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
20662
+ "div",
20663
+ {
20664
+ ref: containerRef,
20665
+ className: "pano-map-viewport",
20666
+ hidden: collapsed,
20667
+ style: {
20668
+ position: "absolute",
20669
+ inset: 0,
20670
+ overflow: "hidden",
20671
+ visibility: collapsed ? "hidden" : "visible"
20672
+ },
20673
+ onPointerDown: onPointerDownSurface,
20674
+ onPointerMove: onPointerMoveSurface,
20675
+ onPointerUp: endDrag,
20676
+ onPointerCancel: endDrag,
20677
+ children: [
20678
+ fit.width > 0 && fit.height > 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20679
+ "div",
20680
+ {
20681
+ className: "pano-map-image-layer",
20682
+ style: {
20683
+ position: "absolute",
20684
+ left: fit.left + pan.x,
20685
+ top: fit.top + pan.y,
20686
+ width: fit.width * scale,
20687
+ height: fit.height * scale,
20688
+ transformOrigin: "0 0"
20689
+ },
20690
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20691
+ "img",
20692
+ {
20693
+ src: map.image,
20694
+ alt: "",
20695
+ draggable: false,
20696
+ ref: (node) => {
20697
+ if (node) syncImageSize(node);
20698
+ },
20699
+ onLoad: (event) => {
20700
+ syncImageSize(event.currentTarget);
20701
+ },
20702
+ style: {
20703
+ width: "100%",
20704
+ height: "100%",
20705
+ display: "block",
20706
+ userSelect: "none",
20707
+ pointerEvents: "none"
20708
+ }
20709
+ }
20710
+ )
20711
+ }
20712
+ ) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20713
+ "img",
20714
+ {
20715
+ src: map.image,
20716
+ alt: "",
20717
+ hidden: true,
20718
+ ref: (node) => {
20719
+ if (node) syncImageSize(node);
20720
+ },
20721
+ onLoad: (event) => {
20722
+ syncImageSize(event.currentTarget);
20723
+ }
20724
+ }
20725
+ ),
20726
+ activeSpot && map.sector ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20727
+ "svg",
20728
+ {
20729
+ className: "pano-map-sector-layer",
20730
+ width: size.width,
20731
+ height: size.height,
20732
+ style: {
20733
+ position: "absolute",
20734
+ inset: 0,
20735
+ overflow: "visible",
20736
+ pointerEvents: "none"
20737
+ },
20738
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20739
+ "g",
20740
+ {
20741
+ transform: `translate(${imagePointToContainer(activeSpot, viewport).x} ${imagePointToContainer(activeSpot, viewport).y})`,
20742
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20743
+ SectorShape,
20744
+ {
20745
+ sector: map.sector,
20746
+ angle: sectorAngle,
20747
+ rotation: sectorRotation(activeSpot.heading, view.yaw),
20748
+ gradientId: defaultGradientId(reactId),
20749
+ interactive: editing,
20750
+ onHeadingPointerDown: (event) => {
20751
+ event.stopPropagation();
20752
+ if (event.button !== 0) return;
20753
+ onSelectedSpotIdChange?.(activeSpot.id);
20754
+ dragRef.current = {
20755
+ kind: "heading",
20756
+ pointerId: event.pointerId,
20757
+ spotId: activeSpot.id
20758
+ };
20759
+ event.currentTarget.setPointerCapture(event.pointerId);
20760
+ },
20761
+ transformRef: (node) => {
20762
+ sectorNodeRef.current = node;
20763
+ }
20764
+ }
20765
+ )
20766
+ }
20767
+ )
20768
+ }
20769
+ ) : null,
20770
+ spots.map((spot) => {
20771
+ const point = imagePointToContainer(spot, viewport);
20772
+ const active = spot.sceneId === activeSceneId;
20773
+ const selected = spot.id === selectedSpotId;
20774
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20775
+ "button",
20776
+ {
20777
+ type: "button",
20778
+ className: "pano-map-spot",
20779
+ "aria-label": spot.sceneId,
20780
+ "aria-current": active ? "true" : void 0,
20781
+ "data-active": active ? "true" : void 0,
20782
+ "data-selected": selected ? "true" : void 0,
20783
+ style: {
20784
+ position: "absolute",
20785
+ left: point.x,
20786
+ top: point.y,
20787
+ width: iconSize,
20788
+ height: iconSize,
20789
+ transform: "translate(-50%, -50%)",
20790
+ padding: 0,
20791
+ border: selected ? "2px solid #38bdf8" : "0",
20792
+ borderRadius: 8,
20793
+ background: "transparent",
20794
+ cursor: editing ? "grab" : "pointer",
20795
+ pointerEvents: "auto"
20796
+ },
20797
+ onPointerDown: (event) => {
20798
+ event.stopPropagation();
20799
+ if (event.button !== 0) return;
20800
+ onSelectedSpotIdChange?.(spot.id);
20801
+ if (!editing) return;
20802
+ const pointLocal = localPoint(event);
20803
+ if (!pointLocal) return;
20804
+ const center = imagePointToContainer(spot, viewport);
20805
+ dragRef.current = {
20806
+ kind: "spot",
20807
+ pointerId: event.pointerId,
20808
+ spotId: spot.id,
20809
+ offsetX: pointLocal.x - center.x,
20810
+ offsetY: pointLocal.y - center.y
20811
+ };
20812
+ event.currentTarget.setPointerCapture(event.pointerId);
20813
+ },
20814
+ onClick: (event) => {
20815
+ event.stopPropagation();
20816
+ if (editing) return;
20817
+ onSelectScene?.(spot.sceneId, spot);
20818
+ },
20819
+ children: spot.icon ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
20820
+ "img",
20821
+ {
20822
+ src: spot.icon,
20823
+ alt: "",
20824
+ draggable: false,
20825
+ style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" }
20826
+ }
20827
+ ) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(DefaultSpotIcon, { active, size: iconSize })
20828
+ },
20829
+ spot.id
20830
+ );
20831
+ })
20832
+ ]
20833
+ }
19003
20834
  )
19004
- });
20835
+ ]
20836
+ }
20837
+ );
20838
+ }
20839
+
20840
+ // src/map/pano-map-overlay.tsx
20841
+ var import_react67 = require("react");
20842
+ var import_jsx_runtime44 = require("react/jsx-runtime");
20843
+ var DEFAULT_DIALOG_WIDTH = 720;
20844
+ var DEFAULT_DIALOG_HEIGHT = 520;
20845
+ function dockedBox(corner, offsetX, offsetY, width, height) {
20846
+ const horizontal = corner === "top-right" || corner === "bottom-right" ? { left: "auto", right: offsetX } : { left: offsetX, right: "auto" };
20847
+ const vertical = corner === "bottom-left" || corner === "bottom-right" ? { top: "auto", bottom: offsetY } : { top: offsetY, bottom: "auto" };
20848
+ return { ...horizontal, ...vertical, width, height };
20849
+ }
20850
+ function PanoMapDialogShell({
20851
+ open,
20852
+ onOpenChange,
20853
+ title,
20854
+ closeLabel,
20855
+ dialogWidth,
20856
+ dialogHeight,
20857
+ children
20858
+ }) {
20859
+ usePanoModal(open);
20860
+ const { size } = usePanoOverlay();
20861
+ const titleId = (0, import_react67.useId)();
20862
+ const panelRef = (0, import_react67.useRef)(null);
20863
+ const previouslyFocused = (0, import_react67.useRef)(null);
20864
+ (0, import_react67.useEffect)(() => {
20865
+ if (!open) return;
20866
+ const active = panelRef.current?.ownerDocument.activeElement;
20867
+ previouslyFocused.current = active instanceof HTMLElement ? active : null;
20868
+ const close = panelRef.current?.querySelector("[data-pano-map-dialog-close]");
20869
+ close?.focus({ preventScroll: true });
20870
+ const onKeyDown = (event) => {
20871
+ if (event.key === "Escape") {
20872
+ event.preventDefault();
20873
+ event.stopPropagation();
20874
+ onOpenChange(false);
20875
+ }
19005
20876
  };
19006
- selected?.addEventListener("cuechange", handleCueChange);
19007
- video.textTracks.addEventListener("addtrack", applyModes);
19008
- video.textTracks.addEventListener("change", applyModes);
20877
+ const doc = panelRef.current?.ownerDocument;
20878
+ doc?.addEventListener("keydown", onKeyDown, true);
19009
20879
  return () => {
19010
- selected?.removeEventListener("cuechange", handleCueChange);
19011
- video.textTracks.removeEventListener("addtrack", applyModes);
19012
- video.textTracks.removeEventListener("change", applyModes);
20880
+ doc?.removeEventListener("keydown", onKeyDown, true);
20881
+ const previous = previouslyFocused.current;
20882
+ if (previous?.isConnected) previous.focus({ preventScroll: true });
19013
20883
  };
19014
- }, [emitSnapshot, resource, trackId, tracks]);
19015
- (0, import_react63.useEffect)(() => {
19016
- if (!captionsEnabled) {
19017
- setTrackId(null);
19018
- return;
19019
- }
19020
- setTrackId(defaultPanoVideoTrackId(tracks));
19021
- }, [captionsEnabled, tracks]);
19022
- const activeTexture = ready && resource ? resource.texture : posterTexture;
19023
- if (variants.length === 0) {
19024
- return null;
19025
- }
19026
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
19027
- activeTexture ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
19028
- "mesh",
20884
+ }, [open, onOpenChange]);
20885
+ if (!open) return null;
20886
+ const preferredWidth = Number.isFinite(dialogWidth) && dialogWidth > 0 ? dialogWidth : DEFAULT_DIALOG_WIDTH;
20887
+ const preferredHeight = Number.isFinite(dialogHeight) && dialogHeight > 0 ? dialogHeight : DEFAULT_DIALOG_HEIGHT;
20888
+ const width = Math.min(preferredWidth, Math.max(0, size.width - 32));
20889
+ const height = Math.min(preferredHeight, Math.max(0, size.height - 32));
20890
+ const left = Math.max(0, (size.width - width) / 2);
20891
+ const top = Math.max(0, (size.height - height) / 2);
20892
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
20893
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
20894
+ "div",
19029
20895
  {
19030
- rotation: [0, import_three40.MathUtils.degToRad(90 + yawOffset2), 0],
19031
- visible,
20896
+ className: "pano-map-dialog-backdrop",
20897
+ "aria-hidden": "true",
20898
+ style: {
20899
+ position: "absolute",
20900
+ inset: 0,
20901
+ pointerEvents: "auto"
20902
+ },
20903
+ onClick: () => onOpenChange(false)
20904
+ }
20905
+ ),
20906
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
20907
+ "div",
20908
+ {
20909
+ ref: panelRef,
20910
+ className: "pano-map-dialog",
20911
+ role: "dialog",
20912
+ "aria-modal": "true",
20913
+ "aria-labelledby": titleId,
20914
+ "data-pano-map-dialog": "",
20915
+ style: {
20916
+ position: "absolute",
20917
+ left,
20918
+ top,
20919
+ width,
20920
+ height,
20921
+ boxSizing: "border-box",
20922
+ display: "flex",
20923
+ flexDirection: "column",
20924
+ overflow: "hidden",
20925
+ pointerEvents: "auto",
20926
+ zIndex: 1
20927
+ },
19032
20928
  children: [
19033
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
19034
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
19035
- PanoBasicMaterial,
19036
- {
19037
- map: activeTexture,
19038
- side: import_three40.BackSide,
19039
- toneMapped: false
19040
- }
19041
- )
20929
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("header", { className: "pano-map-dialog-header", style: { flexShrink: 0 }, children: [
20930
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("h2", { id: titleId, className: "pano-map-dialog-title", children: title }),
20931
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
20932
+ "button",
20933
+ {
20934
+ type: "button",
20935
+ className: "pano-map-dialog-close",
20936
+ "data-pano-map-dialog-close": "",
20937
+ "aria-label": closeLabel,
20938
+ onClick: () => onOpenChange(false),
20939
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.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_runtime44.jsx)("path", { d: "m4 4 8 8M12 4l-8 8" }) })
20940
+ }
20941
+ )
20942
+ ] }),
20943
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "pano-map-dialog-body", style: { minHeight: 0, flex: 1 }, children })
19042
20944
  ]
19043
20945
  }
19044
- ) : null,
19045
- captionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(PanoVideoCaptions, { appearance: typeof captions === "object" ? captions : void 0 }) : null
20946
+ )
19046
20947
  ] });
19047
20948
  }
20949
+ function PanoMapOverlay({
20950
+ map,
20951
+ activeSceneId,
20952
+ onSelectScene,
20953
+ view,
20954
+ mode = "view",
20955
+ selectedSpotId,
20956
+ onSelectedSpotIdChange,
20957
+ onEditChange,
20958
+ placementSceneId,
20959
+ presentationOverride,
20960
+ presentation: presentationModeProp,
20961
+ corner: cornerProp,
20962
+ width: widthProp,
20963
+ height: heightProp,
20964
+ offsetX: offsetXProp,
20965
+ offsetY: offsetYProp,
20966
+ dialogWidth = DEFAULT_DIALOG_WIDTH,
20967
+ dialogHeight = DEFAULT_DIALOG_HEIGHT,
20968
+ interaction,
20969
+ maxScale,
20970
+ initialScale,
20971
+ showZoomControls,
20972
+ showCollapseControl,
20973
+ iconSize,
20974
+ openLabel = "Map",
20975
+ closeLabel = "Close",
20976
+ title = "Map",
20977
+ className,
20978
+ style,
20979
+ editorSurface = false
20980
+ }) {
20981
+ const { size } = usePanoOverlay();
20982
+ const [open, setOpen] = (0, import_react67.useState)(false);
20983
+ const [collapsed, setCollapsed] = (0, import_react67.useState)(false);
20984
+ const basePresentation = presentationOverride ?? map.presentation ?? {
20985
+ mode: "docked",
20986
+ corner: "bottom-left",
20987
+ width: 220,
20988
+ height: 160,
20989
+ offsetX: 16,
20990
+ offsetY: 16
20991
+ };
20992
+ const presentationMode = presentationModeProp ?? basePresentation.mode;
20993
+ const corner = cornerProp ?? basePresentation.corner ?? "bottom-left";
20994
+ const offsetX = offsetXProp ?? basePresentation.offsetX ?? 16;
20995
+ const offsetY = offsetYProp ?? basePresentation.offsetY ?? 16;
20996
+ const width = widthProp ?? basePresentation.width ?? (presentationMode === "dialog" ? 48 : 220);
20997
+ const height = heightProp ?? basePresentation.height ?? (presentationMode === "dialog" ? 48 : 160);
20998
+ const resolvedMap = (0, import_react67.useMemo)(() => {
20999
+ if (interaction === void 0 && maxScale === void 0 && iconSize === void 0) {
21000
+ return map;
21001
+ }
21002
+ return {
21003
+ ...map,
21004
+ ...interaction === void 0 ? {} : { interaction },
21005
+ ...maxScale === void 0 ? {} : { maxScale },
21006
+ ...iconSize === void 0 ? {} : { iconSize }
21007
+ };
21008
+ }, [iconSize, interaction, map, maxScale]);
21009
+ const mapProps = {
21010
+ map: resolvedMap,
21011
+ activeSceneId,
21012
+ view,
21013
+ mode,
21014
+ selectedSpotId,
21015
+ onSelectedSpotIdChange,
21016
+ onEditChange,
21017
+ placementSceneId,
21018
+ initialScale,
21019
+ showZoomControls,
21020
+ showCollapseControl,
21021
+ onSelectScene: (sceneId, spot) => {
21022
+ onSelectScene?.(sceneId, spot);
21023
+ if (presentationMode === "dialog") setOpen(false);
21024
+ }
21025
+ };
21026
+ const editorBox = (0, import_react67.useMemo)(() => {
21027
+ const panelWidth2 = widthProp !== void 0 && Number.isFinite(widthProp) && widthProp > 0 ? widthProp : Math.min(420, Math.max(240, size.width * 0.45));
21028
+ const panelHeight2 = heightProp !== void 0 && Number.isFinite(heightProp) && heightProp > 0 ? heightProp : Math.min(320, Math.max(180, size.height * 0.45));
21029
+ return {
21030
+ left: 16,
21031
+ right: "auto",
21032
+ top: "auto",
21033
+ bottom: 16,
21034
+ width: panelWidth2,
21035
+ height: panelHeight2
21036
+ };
21037
+ }, [heightProp, size.height, size.width, widthProp]);
21038
+ if (editorSurface) {
21039
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21040
+ "div",
21041
+ {
21042
+ className: ["pano-map-overlay", "pano-map-overlay-editor", className].filter(Boolean).join(" "),
21043
+ style: {
21044
+ position: "absolute",
21045
+ pointerEvents: "auto",
21046
+ ...editorBox,
21047
+ ...style
21048
+ },
21049
+ "data-pano-map-overlay": "",
21050
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(PanoMap, { ...mapProps, showCollapseControl: false })
21051
+ }
21052
+ );
21053
+ }
21054
+ if (presentationMode === "dialog") {
21055
+ const launcherBox = dockedBox(corner, offsetX, offsetY, width, height);
21056
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: ["pano-map-overlay", className].filter(Boolean).join(" "), "data-pano-map-overlay": "", children: [
21057
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21058
+ "button",
21059
+ {
21060
+ type: "button",
21061
+ className: "pano-map-launcher",
21062
+ "aria-label": openLabel,
21063
+ style: {
21064
+ position: "absolute",
21065
+ pointerEvents: "auto",
21066
+ ...launcherBox,
21067
+ ...style
21068
+ },
21069
+ onClick: () => setOpen(true),
21070
+ children: openLabel
21071
+ }
21072
+ ),
21073
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21074
+ PanoMapDialogShell,
21075
+ {
21076
+ open,
21077
+ onOpenChange: setOpen,
21078
+ title,
21079
+ closeLabel,
21080
+ dialogWidth,
21081
+ dialogHeight,
21082
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { style: { height: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(PanoMap, { ...mapProps, showCollapseControl: false }) })
21083
+ }
21084
+ )
21085
+ ] });
21086
+ }
21087
+ const panelWidth = collapsed ? Math.max(44, Math.min(width, 56)) : width;
21088
+ const panelHeight = collapsed ? Math.max(44, Math.min(height, 56)) : height;
21089
+ const box = dockedBox(corner, offsetX, offsetY, panelWidth, panelHeight);
21090
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21091
+ "div",
21092
+ {
21093
+ className: ["pano-map-overlay", className].filter(Boolean).join(" "),
21094
+ style: {
21095
+ position: "absolute",
21096
+ pointerEvents: "auto",
21097
+ ...box,
21098
+ ...style
21099
+ },
21100
+ "data-pano-map-overlay": "",
21101
+ "data-presentation": "docked",
21102
+ children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
21103
+ PanoMap,
21104
+ {
21105
+ ...mapProps,
21106
+ collapsed,
21107
+ onCollapsedChange: setCollapsed
21108
+ }
21109
+ )
21110
+ }
21111
+ );
21112
+ }
19048
21113
  // Annotate the CommonJS export names for ESM import in node:
19049
21114
  0 && (module.exports = {
19050
21115
  AudioHotspot,
@@ -19053,6 +21118,7 @@ function PanoVideo({
19053
21118
  DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE,
19054
21119
  DEFAULT_HOTSPOT_TOOLTIP_OFFSET,
19055
21120
  DEFAULT_PANO_CURSORS,
21121
+ DEFAULT_PANO_SCENE_MENU_APPEARANCE,
19056
21122
  DEFAULT_PANO_VIDEO_CAPTION_APPEARANCE,
19057
21123
  DEFAULT_PANO_VIDEO_CONTROLS_APPEARANCE,
19058
21124
  DEFAULT_PANO_VIDEO_PLAYBACK_RATES,
@@ -19070,7 +21136,10 @@ function PanoVideo({
19070
21136
  PanoContextMenu,
19071
21137
  PanoEvents,
19072
21138
  PanoFilter,
21139
+ PanoMap,
21140
+ PanoMapOverlay,
19073
21141
  PanoOverlayPortalProvider,
21142
+ PanoSceneMenu,
19074
21143
  PanoVideo,
19075
21144
  PanoVideoControls,
19076
21145
  PanoViewer,
@@ -19090,13 +21159,19 @@ function PanoVideo({
19090
21159
  WebVR,
19091
21160
  clampPanoPitch,
19092
21161
  composePanoContextMenuItems,
21162
+ containRect,
21163
+ containerPointToImage,
19093
21164
  createDefaultPanoContextMenuItems,
19094
21165
  createPanoContextMenuPresets,
19095
21166
  cycleSceneId,
21167
+ headingFromVector,
21168
+ horizontalFov,
21169
+ imagePointToContainer,
19096
21170
  normalizePanoPosition,
19097
21171
  normalizePanoYaw,
19098
21172
  panoPositionToVector3,
19099
21173
  resolvePanoContextMenuEntries,
21174
+ sectorRotation,
19100
21175
  unwrapPolygonVertices,
19101
21176
  usePanoEvents,
19102
21177
  usePanoModal,