@ericchen1990/pano-view 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4,30 +4,70 @@
4
4
  import { Canvas } from "@react-three/fiber";
5
5
  import {
6
6
  forwardRef as forwardRef2,
7
- useEffect as useEffect4,
7
+ useCallback as useCallback3,
8
+ useEffect as useEffect10,
8
9
  useImperativeHandle as useImperativeHandle2,
9
- useMemo as useMemo2,
10
- useRef as useRef4,
11
- useState as useState2
10
+ useMemo as useMemo4,
11
+ useRef as useRef9,
12
+ useState as useState3
12
13
  } from "react";
13
14
 
14
- // src/panorama-controls.tsx
15
+ // src/auto-rotate.tsx
16
+ import { useFrame as useFrame2 } from "@react-three/fiber";
17
+ import { useContext, useEffect as useEffect2, useRef as useRef2 } from "react";
18
+
19
+ // src/pano-event-bus.ts
20
+ import { createContext } from "react";
21
+ function createPanoEventBus() {
22
+ const listeners = /* @__PURE__ */ new Map();
23
+ return {
24
+ subscribe(type, listener) {
25
+ let set = listeners.get(type);
26
+ if (!set) {
27
+ set = /* @__PURE__ */ new Set();
28
+ listeners.set(type, set);
29
+ }
30
+ set.add(listener);
31
+ return () => {
32
+ set?.delete(listener);
33
+ if (set && set.size === 0) {
34
+ listeners.delete(type);
35
+ }
36
+ };
37
+ },
38
+ emit(type, payload) {
39
+ const set = listeners.get(type);
40
+ if (!set || set.size === 0) {
41
+ return;
42
+ }
43
+ for (const listener of [...set]) {
44
+ listener(payload);
45
+ }
46
+ }
47
+ };
48
+ }
49
+ var PanoEventBusContext = createContext(null);
50
+
51
+ // src/panorama-view-runtime.tsx
15
52
  import { useFrame, useThree } from "@react-three/fiber";
16
53
  import {
54
+ createContext as createContext2,
17
55
  forwardRef,
18
56
  useEffect,
19
57
  useImperativeHandle,
20
58
  useRef
21
59
  } from "react";
22
60
  import { Euler, MathUtils, PerspectiveCamera } from "three";
23
- var MAX_PITCH = 89.9;
61
+ var MAX_PITCH = 90;
62
+ var BOUNCE_OVERSHOOT_PITCH = 12;
63
+ var BOUNCE_OVERSHOOT_FOV_RATIO = 0.15;
24
64
  var INERTIA_ROTATION_DAMPING = 8;
25
65
  var INERTIA_ZOOM_DAMPING = 12;
26
66
  var DEFAULT_ROTATE_DAMPING = 14;
27
67
  var DEFAULT_ZOOM_DAMPING = 16;
28
- var MIN_ROTATION_VELOCITY = 0.01;
29
- var MIN_ZOOM_VELOCITY = 0.01;
68
+ var DEFAULT_FRICTION_STOP = 0.01;
30
69
  var VIEW_SETTLE_EPSILON = 1e-3;
70
+ var PanoramaViewContext = createContext2(null);
31
71
  function clamp(value, min, max) {
32
72
  return Math.max(min, Math.min(max, value));
33
73
  }
@@ -43,54 +83,104 @@ function dampingFactor(damping, deltaSeconds) {
43
83
  function resolveDamping(value, fallback) {
44
84
  return Number.isFinite(value) && value >= 0 ? value : fallback;
45
85
  }
46
- function pointerDistance(a, b) {
47
- return Math.hypot(a.x - b.x, a.y - b.y);
86
+ function resolveFrictionStop(value) {
87
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_FRICTION_STOP;
48
88
  }
49
89
  function viewsEqual(a, b) {
50
90
  return a.yaw === b.yaw && a.pitch === b.pitch && a.fov === b.fov;
51
91
  }
52
- var PanoramaControls = forwardRef(function PanoramaControls2({ enabled, initialView, minFov, maxFov, options, onViewChange }, ref) {
53
- const { camera, gl } = useThree();
92
+ var PanoramaViewRuntime = forwardRef(function PanoramaViewRuntime2({ initialView, minFov, maxFov, options, eventBus }, ref) {
93
+ const { camera } = useThree();
54
94
  const viewRef = useRef({ ...initialView });
55
95
  const targetViewRef = useRef({ ...initialView });
56
96
  const initialViewRef = useRef({ ...initialView });
57
- const pointersRef = useRef(/* @__PURE__ */ new Map());
58
- const pinchDistanceRef = useRef(null);
59
97
  const yawVelocityRef = useRef(0);
60
98
  const pitchVelocityRef = useRef(0);
61
99
  const zoomVelocityRef = useRef(0);
62
100
  const dirtyRef = useRef(true);
101
+ const settledRef = useRef(false);
63
102
  const interactingRef = useRef(false);
103
+ const mouseActiveRef = useRef(false);
104
+ const touchActiveRef = useRef(false);
64
105
  const interactionLockCountRef = useRef(0);
106
+ const keyboardActiveRef = useRef(false);
107
+ const lastVelocitySampleAtRef = useRef(null);
65
108
  const optionsRef = useRef(options);
66
- const onViewChangeRef = useRef(onViewChange);
109
+ const eventBusRef = useRef(eventBus);
67
110
  const lastEmittedViewRef = useRef(null);
68
111
  const eulerRef = useRef(new Euler(0, 0, 0, "YXZ"));
112
+ const minFovRef = useRef(minFov);
113
+ const maxFovRef = useRef(maxFov);
69
114
  optionsRef.current = options;
70
- onViewChangeRef.current = onViewChange;
71
- const constrainView = (view, current = targetViewRef.current) => {
115
+ eventBusRef.current = eventBus;
116
+ minFovRef.current = minFov;
117
+ maxFovRef.current = maxFov;
118
+ const hasInertia = () => yawVelocityRef.current !== 0 || pitchVelocityRef.current !== 0 || zoomVelocityRef.current !== 0;
119
+ const isUserInteracting = () => mouseActiveRef.current || touchActiveRef.current || keyboardActiveRef.current || interactionLockCountRef.current > 0 || hasInertia();
120
+ const syncInteracting = () => {
121
+ interactingRef.current = mouseActiveRef.current || touchActiveRef.current;
122
+ };
123
+ const hardConstrainView = (view, current = targetViewRef.current) => {
72
124
  return {
73
125
  yaw: normalizeYaw(view.yaw ?? current.yaw),
74
126
  pitch: clamp(view.pitch ?? current.pitch, -MAX_PITCH, MAX_PITCH),
75
- fov: clamp(view.fov ?? current.fov, minFov, maxFov)
127
+ fov: clamp(
128
+ view.fov ?? current.fov,
129
+ minFovRef.current,
130
+ maxFovRef.current
131
+ )
132
+ };
133
+ };
134
+ const softConstrainView = (view, current = targetViewRef.current) => {
135
+ const bouncing = optionsRef.current.bouncingLimits === true;
136
+ if (!bouncing || !interactingRef.current) {
137
+ return hardConstrainView(view, current);
138
+ }
139
+ const fovRange = Math.max(maxFovRef.current - minFovRef.current, 1);
140
+ const fovOvershoot = fovRange * BOUNCE_OVERSHOOT_FOV_RATIO;
141
+ return {
142
+ yaw: normalizeYaw(view.yaw ?? current.yaw),
143
+ pitch: clamp(
144
+ view.pitch ?? current.pitch,
145
+ -MAX_PITCH - BOUNCE_OVERSHOOT_PITCH,
146
+ MAX_PITCH + BOUNCE_OVERSHOOT_PITCH
147
+ ),
148
+ fov: clamp(
149
+ view.fov ?? current.fov,
150
+ minFovRef.current - fovOvershoot,
151
+ maxFovRef.current + fovOvershoot
152
+ )
76
153
  };
77
154
  };
78
155
  const stopVelocity = () => {
79
156
  yawVelocityRef.current = 0;
80
157
  pitchVelocityRef.current = 0;
81
158
  zoomVelocityRef.current = 0;
159
+ lastVelocitySampleAtRef.current = null;
160
+ };
161
+ const markDirty = () => {
162
+ dirtyRef.current = true;
163
+ settledRef.current = false;
164
+ };
165
+ const snapTargetToHardLimits = () => {
166
+ const next = hardConstrainView(targetViewRef.current);
167
+ if (next.yaw !== targetViewRef.current.yaw || next.pitch !== targetViewRef.current.pitch || next.fov !== targetViewRef.current.fov) {
168
+ targetViewRef.current = next;
169
+ markDirty();
170
+ }
82
171
  };
83
172
  const acquireInteractionLock = () => {
84
173
  interactionLockCountRef.current += 1;
85
174
  stopVelocity();
86
- interactingRef.current = false;
87
- pinchDistanceRef.current = null;
88
- for (const pointerId of pointersRef.current.keys()) {
89
- if (gl.domElement.hasPointerCapture?.(pointerId)) {
90
- gl.domElement.releasePointerCapture?.(pointerId);
91
- }
175
+ if (mouseActiveRef.current) {
176
+ mouseActiveRef.current = false;
177
+ eventBusRef.current.emit("viewinteractionend", { source: "mouse" });
92
178
  }
93
- pointersRef.current.clear();
179
+ if (touchActiveRef.current) {
180
+ touchActiveRef.current = false;
181
+ eventBusRef.current.emit("viewinteractionend", { source: "touch" });
182
+ }
183
+ syncInteracting();
94
184
  let released = false;
95
185
  return () => {
96
186
  if (released) {
@@ -104,233 +194,145 @@ var PanoramaControls = forwardRef(function PanoramaControls2({ enabled, initialV
104
194
  };
105
195
  };
106
196
  const setView = (view, setOptions) => {
107
- const nextView = constrainView(view);
197
+ const nextView = hardConstrainView(view);
108
198
  targetViewRef.current = nextView;
109
199
  viewRef.current = nextView;
110
200
  if (setOptions?.immediate !== false) {
111
201
  stopVelocity();
112
202
  }
113
- dirtyRef.current = true;
203
+ markDirty();
204
+ };
205
+ const applyViewDelta = (delta, applyOptions) => {
206
+ if (interactionLockCountRef.current > 0) {
207
+ return false;
208
+ }
209
+ const current = targetViewRef.current;
210
+ const nextYaw = delta.yaw !== void 0 && Number.isFinite(delta.yaw) ? current.yaw + delta.yaw : current.yaw;
211
+ const nextPitch = delta.pitch !== void 0 && Number.isFinite(delta.pitch) ? current.pitch + delta.pitch : current.pitch;
212
+ const nextFov = delta.fov !== void 0 && Number.isFinite(delta.fov) ? current.fov + delta.fov : current.fov;
213
+ targetViewRef.current = softConstrainView({
214
+ yaw: nextYaw,
215
+ pitch: nextPitch,
216
+ fov: nextFov
217
+ });
218
+ markDirty();
219
+ if (applyOptions?.recordVelocity) {
220
+ const now = performance.now();
221
+ const previousAt = lastVelocitySampleAtRef.current;
222
+ const elapsed = previousAt === null ? 1 / 60 : Math.max((now - previousAt) / 1e3, 1 / 120);
223
+ lastVelocitySampleAtRef.current = now;
224
+ if (delta.yaw !== void 0 && Number.isFinite(delta.yaw)) {
225
+ yawVelocityRef.current = delta.yaw / elapsed;
226
+ }
227
+ if (delta.pitch !== void 0 && Number.isFinite(delta.pitch)) {
228
+ pitchVelocityRef.current = delta.pitch / elapsed;
229
+ }
230
+ if (delta.fov !== void 0 && Number.isFinite(delta.fov)) {
231
+ zoomVelocityRef.current = clamp(delta.fov / elapsed, -180, 180);
232
+ }
233
+ }
234
+ return true;
114
235
  };
115
236
  useImperativeHandle(
116
237
  ref,
117
238
  () => ({
118
239
  getView: () => ({ ...viewRef.current }),
240
+ getTargetView: () => ({ ...targetViewRef.current }),
119
241
  setView,
120
242
  reset: () => setView(initialViewRef.current),
243
+ applyViewDelta,
244
+ setKeyboardActive: (active) => {
245
+ const wasActive = keyboardActiveRef.current;
246
+ keyboardActiveRef.current = active;
247
+ if (active && !wasActive) {
248
+ eventBusRef.current.emit("viewinteractionstart", {
249
+ source: "keyboard"
250
+ });
251
+ } else if (!active && wasActive) {
252
+ eventBusRef.current.emit("viewinteractionend", {
253
+ source: "keyboard"
254
+ });
255
+ }
256
+ },
257
+ setPointerActive: (source, active) => {
258
+ const wasActive = source === "mouse" ? mouseActiveRef.current : touchActiveRef.current;
259
+ if (source === "mouse") {
260
+ mouseActiveRef.current = active;
261
+ } else {
262
+ touchActiveRef.current = active;
263
+ }
264
+ syncInteracting();
265
+ if (active && !wasActive) {
266
+ eventBusRef.current.emit("viewinteractionstart", { source });
267
+ } else if (!active && wasActive) {
268
+ eventBusRef.current.emit("viewinteractionend", { source });
269
+ }
270
+ if (active) {
271
+ stopVelocity();
272
+ } else if (!mouseActiveRef.current && !touchActiveRef.current) {
273
+ lastVelocitySampleAtRef.current = null;
274
+ snapTargetToHardLimits();
275
+ }
276
+ },
277
+ isInteractionLocked: () => interactionLockCountRef.current > 0,
278
+ isUserInteracting,
121
279
  acquireInteractionLock,
122
280
  applyAutoRotation: (yawDelta) => {
123
281
  const inertiaFinished = yawVelocityRef.current === 0 && pitchVelocityRef.current === 0 && zoomVelocityRef.current === 0;
124
- if (!Number.isFinite(yawDelta) || interactionLockCountRef.current > 0 || interactingRef.current || !inertiaFinished) {
282
+ if (!Number.isFinite(yawDelta) || interactionLockCountRef.current > 0 || interactingRef.current || keyboardActiveRef.current || !inertiaFinished) {
125
283
  return false;
126
284
  }
127
285
  targetViewRef.current.yaw += yawDelta;
128
- dirtyRef.current = true;
286
+ markDirty();
129
287
  return true;
130
288
  }
131
289
  }),
132
- [gl, maxFov, minFov]
290
+ []
133
291
  );
134
292
  useEffect(() => {
135
293
  initialViewRef.current = { ...initialView };
136
294
  setView(initialView);
137
295
  }, [initialView.fov, initialView.pitch, initialView.yaw]);
138
- useEffect(() => {
139
- const element = gl.domElement;
140
- if (!enabled) {
141
- pointersRef.current.clear();
142
- interactingRef.current = false;
143
- return;
144
- }
145
- const updatePointer = (event) => {
146
- const previous = pointersRef.current.get(event.pointerId);
147
- const now = performance.now();
148
- const next = { x: event.clientX, y: event.clientY, at: now };
149
- pointersRef.current.set(event.pointerId, next);
150
- return { previous, next };
151
- };
152
- const onPointerDown = (event) => {
153
- if (interactionLockCountRef.current > 0) {
154
- return;
155
- }
156
- event.preventDefault();
157
- interactingRef.current = true;
158
- stopVelocity();
159
- pointersRef.current.set(event.pointerId, {
160
- x: event.clientX,
161
- y: event.clientY,
162
- at: performance.now()
163
- });
164
- element.setPointerCapture?.(event.pointerId);
165
- if (pointersRef.current.size === 2) {
166
- const [first, second] = Array.from(pointersRef.current.values());
167
- pinchDistanceRef.current = pointerDistance(first, second);
168
- }
169
- };
170
- const onPointerMove = (event) => {
171
- if (interactionLockCountRef.current > 0) {
172
- return;
173
- }
174
- if (!pointersRef.current.has(event.pointerId)) {
175
- return;
176
- }
177
- event.preventDefault();
178
- const { previous, next } = updatePointer(event);
179
- if (!previous) {
180
- return;
181
- }
182
- const pointers = Array.from(pointersRef.current.values());
183
- if (pointers.length >= 2) {
184
- const distance = pointerDistance(pointers[0], pointers[1]);
185
- const previousDistance = pinchDistanceRef.current;
186
- pinchDistanceRef.current = distance;
187
- if (previousDistance && distance > 0) {
188
- const currentFov = targetViewRef.current.fov;
189
- const nextFov = clamp(
190
- currentFov * (previousDistance / distance),
191
- minFov,
192
- maxFov
193
- );
194
- const elapsed2 = Math.max((next.at - previous.at) / 1e3, 1 / 120);
195
- zoomVelocityRef.current = (nextFov - currentFov) / elapsed2;
196
- targetViewRef.current = {
197
- ...targetViewRef.current,
198
- fov: nextFov
199
- };
200
- dirtyRef.current = true;
201
- }
202
- return;
203
- }
204
- const rect = element.getBoundingClientRect();
205
- const rotateSpeed = optionsRef.current.rotateSpeed ?? 0.35;
206
- const deltaYaw = -(next.x - previous.x) * 360 * rotateSpeed / Math.max(rect.width, 1);
207
- const deltaPitch = (next.y - previous.y) * 180 * rotateSpeed / Math.max(rect.height, 1);
208
- const elapsed = Math.max((next.at - previous.at) / 1e3, 1 / 120);
209
- targetViewRef.current = constrainView({
210
- yaw: targetViewRef.current.yaw + deltaYaw,
211
- pitch: targetViewRef.current.pitch + deltaPitch
212
- });
213
- yawVelocityRef.current = deltaYaw / elapsed;
214
- pitchVelocityRef.current = deltaPitch / elapsed;
215
- dirtyRef.current = true;
216
- };
217
- const releasePointer = (event) => {
218
- pointersRef.current.delete(event.pointerId);
219
- if (element.hasPointerCapture?.(event.pointerId)) {
220
- element.releasePointerCapture?.(event.pointerId);
221
- }
222
- if (pointersRef.current.size < 2) {
223
- pinchDistanceRef.current = null;
224
- }
225
- interactingRef.current = pointersRef.current.size > 0;
226
- };
227
- const onWheel = (event) => {
228
- if (interactionLockCountRef.current > 0) {
229
- event.preventDefault();
230
- return;
231
- }
232
- event.preventDefault();
233
- const zoomSpeed = optionsRef.current.zoomSpeed ?? 0.08;
234
- const delta = event.deltaY * zoomSpeed;
235
- const nextFov = clamp(
236
- targetViewRef.current.fov + delta,
237
- minFov,
238
- maxFov
239
- );
240
- zoomVelocityRef.current = clamp(
241
- zoomVelocityRef.current + delta * 12,
242
- -180,
243
- 180
244
- );
245
- targetViewRef.current = {
246
- ...targetViewRef.current,
247
- fov: nextFov
248
- };
249
- dirtyRef.current = true;
250
- };
251
- const onKeyDown = (event) => {
252
- if (interactionLockCountRef.current > 0) {
253
- return;
254
- }
255
- if (optionsRef.current.keyboard === false) {
256
- return;
257
- }
258
- const step = event.shiftKey ? 10 : 3;
259
- let next = null;
260
- if (event.key === "ArrowLeft") {
261
- next = { yaw: targetViewRef.current.yaw - step };
262
- } else if (event.key === "ArrowRight") {
263
- next = { yaw: targetViewRef.current.yaw + step };
264
- } else if (event.key === "ArrowUp") {
265
- next = { pitch: targetViewRef.current.pitch + step };
266
- } else if (event.key === "ArrowDown") {
267
- next = { pitch: targetViewRef.current.pitch - step };
268
- } else if (event.key === "+" || event.key === "=") {
269
- next = { fov: targetViewRef.current.fov - step };
270
- } else if (event.key === "-" || event.key === "_") {
271
- next = { fov: targetViewRef.current.fov + step };
272
- } else if (event.key === "0") {
273
- setView(initialViewRef.current);
274
- event.preventDefault();
275
- return;
276
- }
277
- if (next) {
278
- event.preventDefault();
279
- targetViewRef.current = constrainView(next);
280
- dirtyRef.current = true;
281
- }
282
- };
283
- element.addEventListener("pointerdown", onPointerDown);
284
- element.addEventListener("pointermove", onPointerMove);
285
- element.addEventListener("pointerup", releasePointer);
286
- element.addEventListener("pointercancel", releasePointer);
287
- element.addEventListener("wheel", onWheel, { passive: false });
288
- element.addEventListener("keydown", onKeyDown);
289
- return () => {
290
- element.removeEventListener("pointerdown", onPointerDown);
291
- element.removeEventListener("pointermove", onPointerMove);
292
- element.removeEventListener("pointerup", releasePointer);
293
- element.removeEventListener("pointercancel", releasePointer);
294
- element.removeEventListener("wheel", onWheel);
295
- element.removeEventListener("keydown", onKeyDown);
296
- pointersRef.current.clear();
297
- };
298
- }, [enabled, gl, maxFov, minFov]);
299
296
  useFrame((_, deltaSeconds) => {
300
297
  if (!(camera instanceof PerspectiveCamera)) {
301
298
  return;
302
299
  }
300
+ const frictionStop = resolveFrictionStop(optionsRef.current.frictionStop);
303
301
  const inertiaEnabled = optionsRef.current.inertia !== false;
304
302
  if (!interactingRef.current && inertiaEnabled) {
305
303
  const rotationDamping = Math.exp(
306
304
  -INERTIA_ROTATION_DAMPING * deltaSeconds
307
305
  );
308
306
  const zoomDamping2 = Math.exp(-INERTIA_ZOOM_DAMPING * deltaSeconds);
309
- if (Math.abs(yawVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
307
+ if (Math.abs(yawVelocityRef.current) >= frictionStop) {
310
308
  targetViewRef.current.yaw += yawVelocityRef.current * deltaSeconds;
311
309
  yawVelocityRef.current *= rotationDamping;
312
- dirtyRef.current = true;
310
+ markDirty();
313
311
  } else {
314
312
  yawVelocityRef.current = 0;
315
313
  }
316
- if (Math.abs(pitchVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
314
+ if (Math.abs(pitchVelocityRef.current) >= frictionStop) {
317
315
  targetViewRef.current.pitch += pitchVelocityRef.current * deltaSeconds;
318
316
  pitchVelocityRef.current *= rotationDamping;
319
- dirtyRef.current = true;
317
+ markDirty();
320
318
  } else {
321
319
  pitchVelocityRef.current = 0;
322
320
  }
323
- if (Math.abs(zoomVelocityRef.current) >= MIN_ZOOM_VELOCITY) {
321
+ if (Math.abs(zoomVelocityRef.current) >= frictionStop) {
324
322
  targetViewRef.current.fov += zoomVelocityRef.current * deltaSeconds;
325
323
  zoomVelocityRef.current *= zoomDamping2;
326
- dirtyRef.current = true;
324
+ markDirty();
327
325
  } else {
328
326
  zoomVelocityRef.current = 0;
329
327
  }
330
328
  } else if (!inertiaEnabled) {
331
329
  stopVelocity();
332
330
  }
333
- targetViewRef.current = constrainView(targetViewRef.current);
331
+ if (!interactingRef.current) {
332
+ targetViewRef.current = hardConstrainView(targetViewRef.current);
333
+ } else {
334
+ targetViewRef.current = softConstrainView(targetViewRef.current);
335
+ }
334
336
  if (!dirtyRef.current) {
335
337
  return;
336
338
  }
@@ -367,7 +369,12 @@ var PanoramaControls = forwardRef(function PanoramaControls2({ enabled, initialV
367
369
  const lastEmitted = lastEmittedViewRef.current;
368
370
  if (!lastEmitted || !viewsEqual(lastEmitted, nextView)) {
369
371
  lastEmittedViewRef.current = nextView;
370
- onViewChangeRef.current?.(nextView);
372
+ eventBusRef.current.emit("viewchange", nextView);
373
+ }
374
+ const wasSettled = settledRef.current;
375
+ settledRef.current = hasSettled;
376
+ if (hasSettled && !wasSettled) {
377
+ eventBusRef.current.emit("viewsettled", nextView);
371
378
  }
372
379
  dirtyRef.current = !hasSettled;
373
380
  });
@@ -375,17 +382,9 @@ var PanoramaControls = forwardRef(function PanoramaControls2({ enabled, initialV
375
382
  });
376
383
 
377
384
  // src/auto-rotate.tsx
378
- import { useFrame as useFrame2 } from "@react-three/fiber";
379
- import {
380
- createContext,
381
- useContext,
382
- useEffect as useEffect2,
383
- useRef as useRef2
384
- } from "react";
385
385
  var DEFAULT_AUTO_ROTATE_SPEED = 18;
386
386
  var DEFAULT_AUTO_ROTATE_ACCELERATION = 18;
387
387
  var DEFAULT_AUTO_ROTATE_START_DELAY = 2;
388
- var PanoramaControlsContext = createContext(null);
389
388
  function resolveStartDelay(value) {
390
389
  return Number.isFinite(value) ? Math.max(value, 0) : 0;
391
390
  }
@@ -398,19 +397,31 @@ function AutoRotate({
398
397
  acceleration = DEFAULT_AUTO_ROTATE_ACCELERATION,
399
398
  startDelay = DEFAULT_AUTO_ROTATE_START_DELAY
400
399
  }) {
401
- const controlsRef = useContext(PanoramaControlsContext);
400
+ const controlsRef = useContext(PanoramaViewContext);
401
+ const eventBus = useContext(PanoEventBusContext);
402
402
  const elapsedRef = useRef2(0);
403
403
  const rotationSpeedRef = useRef2(0);
404
+ const rotatingRef = useRef2(false);
405
+ const yawAccumulatedRef = useRef2(0);
404
406
  if (!controlsRef) {
405
407
  throw new Error("<AutoRotate> must be rendered inside <PanoView>.");
406
408
  }
407
409
  useEffect2(() => {
408
410
  elapsedRef.current = 0;
409
411
  rotationSpeedRef.current = 0;
410
- }, [enabled, startDelay]);
412
+ yawAccumulatedRef.current = 0;
413
+ if (rotatingRef.current) {
414
+ rotatingRef.current = false;
415
+ eventBus?.emit("autorotatestop", void 0);
416
+ }
417
+ }, [enabled, eventBus, startDelay]);
411
418
  useFrame2((_, deltaSeconds) => {
412
419
  if (!enabled || !Number.isFinite(speed) || speed === 0) {
413
420
  rotationSpeedRef.current = 0;
421
+ if (rotatingRef.current) {
422
+ rotatingRef.current = false;
423
+ eventBus?.emit("autorotatestop", void 0);
424
+ }
414
425
  return;
415
426
  }
416
427
  const delaySeconds = resolveStartDelay(startDelay) / 1e3;
@@ -429,8 +440,21 @@ function AutoRotate({
429
440
  const yawDelta = Math.sign(speed) * averageSpeed * deltaSeconds;
430
441
  if (!controlsRef.current?.applyAutoRotation(yawDelta)) {
431
442
  rotationSpeedRef.current = 0;
443
+ if (rotatingRef.current) {
444
+ rotatingRef.current = false;
445
+ eventBus?.emit("autorotatestop", void 0);
446
+ }
432
447
  return;
433
448
  }
449
+ if (!rotatingRef.current) {
450
+ rotatingRef.current = true;
451
+ eventBus?.emit("autorotatestart", void 0);
452
+ }
453
+ yawAccumulatedRef.current += Math.abs(yawDelta);
454
+ while (yawAccumulatedRef.current >= 360) {
455
+ yawAccumulatedRef.current -= 360;
456
+ eventBus?.emit("autorotateoneround", void 0);
457
+ }
434
458
  rotationSpeedRef.current = nextSpeed;
435
459
  });
436
460
  return null;
@@ -438,7 +462,7 @@ function AutoRotate({
438
462
 
439
463
  // src/hotspot/accessibility.tsx
440
464
  import {
441
- createContext as createContext2,
465
+ createContext as createContext3,
442
466
  useCallback,
443
467
  useContext as useContext2,
444
468
  useEffect as useEffect3,
@@ -447,7 +471,7 @@ import {
447
471
  useState
448
472
  } from "react";
449
473
  import { jsx } from "react/jsx-runtime";
450
- var HotspotAccessibilityContext = createContext2(null);
474
+ var HotspotAccessibilityContext = createContext3(null);
451
475
  var VISUALLY_HIDDEN_STYLE = {
452
476
  border: 0,
453
477
  clip: "rect(0 0 0 0)",
@@ -552,131 +576,1649 @@ function useHotspotAccessibilityLayer() {
552
576
  return { controls, registry };
553
577
  }
554
578
 
555
- // src/panorama-event-surface.tsx
556
- import { BackSide } from "three";
557
-
558
- // src/hotspot/coordinates.ts
559
- import { MathUtils as MathUtils2, Vector3 } from "three";
560
-
561
- // src/hotspot/types.ts
562
- var MAX_HOTSPOT_PITCH = 89.9;
563
-
564
- // src/hotspot/coordinates.ts
565
- function normalizePanoYaw(yaw) {
566
- if (!Number.isFinite(yaw)) {
567
- return 0;
579
+ // src/keyboard-controls.tsx
580
+ import { useFrame as useFrame3, useThree as useThree2 } from "@react-three/fiber";
581
+ import { useContext as useContext3, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4 } from "react";
582
+ var DEFAULT_ROTATE_SPEED = 60;
583
+ var DEFAULT_ZOOM_SPEED = 30;
584
+ var DEFAULT_SHIFT_MULTIPLIER = 3;
585
+ var DEFAULT_KEYS = {
586
+ left: ["ArrowLeft"],
587
+ right: ["ArrowRight"],
588
+ up: ["ArrowUp"],
589
+ down: ["ArrowDown"],
590
+ zoomIn: ["+", "="],
591
+ zoomOut: ["-", "_"],
592
+ previousScene: ["[", "PageUp"],
593
+ nextScene: ["]", "PageDown"],
594
+ reset: ["0"]
595
+ };
596
+ var CONTINUOUS_ACTIONS = /* @__PURE__ */ new Set([
597
+ "left",
598
+ "right",
599
+ "up",
600
+ "down",
601
+ "zoomIn",
602
+ "zoomOut"
603
+ ]);
604
+ function normalizeBinding(value) {
605
+ if (value === void 0) {
606
+ return [];
568
607
  }
569
- return MathUtils2.euclideanModulo(yaw + 180, 360) - 180;
608
+ return (typeof value === "string" ? [value] : [...value]).filter(
609
+ (entry) => entry.length > 0
610
+ );
570
611
  }
571
- function clampPanoPitch(pitch) {
572
- if (!Number.isFinite(pitch)) {
573
- return 0;
612
+ function resolveKeyMap(overrides) {
613
+ const map = /* @__PURE__ */ new Map();
614
+ for (const action of Object.keys(DEFAULT_KEYS)) {
615
+ const bindings = overrides && action in overrides ? normalizeBinding(overrides[action]) : [...DEFAULT_KEYS[action]];
616
+ for (const binding of bindings) {
617
+ map.set(binding.toLowerCase(), action);
618
+ }
574
619
  }
575
- return MathUtils2.clamp(pitch, -MAX_HOTSPOT_PITCH, MAX_HOTSPOT_PITCH);
620
+ return map;
576
621
  }
577
- function normalizePanoPosition(position) {
578
- return {
579
- yaw: normalizePanoYaw(position.yaw),
580
- pitch: clampPanoPitch(position.pitch)
581
- };
622
+ function matchAction(event, keyMap) {
623
+ return keyMap.get(event.key.toLowerCase()) ?? keyMap.get(event.code.toLowerCase()) ?? null;
582
624
  }
583
- function panoPositionToVector3(position, radius = 1) {
584
- const normalized = normalizePanoPosition(position);
585
- const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
586
- const yaw = MathUtils2.degToRad(normalized.yaw);
587
- const pitch = MathUtils2.degToRad(normalized.pitch);
588
- const cosPitch = Math.cos(pitch);
589
- return new Vector3(
590
- cosPitch * Math.sin(yaw) * safeRadius,
591
- Math.sin(pitch) * safeRadius,
592
- -cosPitch * Math.cos(yaw) * safeRadius
593
- );
625
+ function resolveSpeed(value, fallback) {
626
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
594
627
  }
595
- function vector3ToPanoPosition(vector) {
596
- const length = Math.hypot(vector.x, vector.y, vector.z);
597
- if (!Number.isFinite(length) || length === 0) {
598
- return { yaw: 0, pitch: 0 };
628
+ function cycleSceneId(scenes, activeSceneId, step) {
629
+ if (scenes.length === 0) {
630
+ return null;
599
631
  }
600
- return normalizePanoPosition({
601
- yaw: MathUtils2.radToDeg(Math.atan2(vector.x, -vector.z)),
602
- pitch: MathUtils2.radToDeg(
603
- Math.asin(MathUtils2.clamp(vector.y / length, -1, 1))
604
- )
605
- });
606
- }
607
-
608
- // src/panorama-radius.ts
609
- var DEFAULT_PANORAMA_RADIUS = 1e3;
610
- var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
611
- var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
612
-
613
- // src/panorama-event-surface.tsx
614
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
615
- var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
616
- function makePanoramaPointerEvent(event) {
617
- const nativeEvent = event.nativeEvent;
618
- return {
619
- position: vector3ToPanoPosition(event.ray.direction),
620
- nativeEvent,
621
- pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
622
- button: nativeEvent.button,
623
- buttons: nativeEvent.buttons,
624
- clientX: nativeEvent.clientX,
625
- clientY: nativeEvent.clientY
626
- };
627
- }
628
- function emitPanoramaEvent(event, callback) {
629
- if (!callback) {
630
- return;
632
+ const index = scenes.findIndex((scene) => scene.id === activeSceneId);
633
+ if (index < 0) {
634
+ return null;
631
635
  }
632
- event.stopPropagation();
633
- callback(makePanoramaPointerEvent(event));
636
+ const nextIndex = (index + step + scenes.length) % scenes.length;
637
+ return scenes[nextIndex]?.id ?? null;
634
638
  }
635
- function PanoramaEventSurface({
636
- onClick,
637
- onDoubleClick,
638
- onPointerMove
639
+ function KeyboardControls({
640
+ enabled = true,
641
+ keys,
642
+ rotateSpeed = DEFAULT_ROTATE_SPEED,
643
+ zoomSpeed = DEFAULT_ZOOM_SPEED,
644
+ shiftMultiplier = DEFAULT_SHIFT_MULTIPLIER,
645
+ invert = false,
646
+ fovSpeed,
647
+ onPreviousScene,
648
+ onNextScene
639
649
  }) {
640
- if (!onClick && !onDoubleClick && !onPointerMove) {
641
- return null;
650
+ const controlsRef = useContext3(PanoramaViewContext);
651
+ const { gl } = useThree2();
652
+ const pressedRef = useRef4(/* @__PURE__ */ new Set());
653
+ const shiftHeldRef = useRef4(false);
654
+ const invertRef = useRef4(invert);
655
+ const fovSpeedRef = useRef4(fovSpeed);
656
+ const onPreviousSceneRef = useRef4(onPreviousScene);
657
+ const onNextSceneRef = useRef4(onNextScene);
658
+ const keyMap = useMemo2(() => resolveKeyMap(keys), [keys]);
659
+ if (!controlsRef) {
660
+ throw new Error("<KeyboardControls> must be rendered inside <PanoView>.");
642
661
  }
643
- return /* @__PURE__ */ jsxs(
644
- "mesh",
645
- {
646
- renderOrder: -200,
647
- onClick: (event) => emitPanoramaEvent(event, onClick),
648
- onDoubleClick: (event) => emitPanoramaEvent(event, onDoubleClick),
649
- onPointerMove: (event) => emitPanoramaEvent(event, onPointerMove),
650
- children: [
651
- /* @__PURE__ */ jsx2("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
652
- /* @__PURE__ */ jsx2(
653
- "meshBasicMaterial",
654
- {
655
- colorWrite: false,
656
- depthTest: false,
657
- depthWrite: false,
658
- opacity: 0,
659
- side: BackSide,
660
- transparent: true
661
- }
662
- )
663
- ]
662
+ invertRef.current = invert;
663
+ fovSpeedRef.current = fovSpeed;
664
+ onPreviousSceneRef.current = onPreviousScene;
665
+ onNextSceneRef.current = onNextScene;
666
+ useEffect4(() => {
667
+ const element = gl.domElement;
668
+ if (!enabled) {
669
+ pressedRef.current.clear();
670
+ shiftHeldRef.current = false;
671
+ controlsRef.current?.setKeyboardActive(false);
672
+ return;
673
+ }
674
+ const syncKeyboardActive = () => {
675
+ let continuous = false;
676
+ for (const action of pressedRef.current) {
677
+ if (CONTINUOUS_ACTIONS.has(action)) {
678
+ continuous = true;
679
+ break;
680
+ }
681
+ }
682
+ controlsRef.current?.setKeyboardActive(continuous);
683
+ };
684
+ const clearPressed = () => {
685
+ pressedRef.current.clear();
686
+ shiftHeldRef.current = false;
687
+ controlsRef.current?.setKeyboardActive(false);
688
+ };
689
+ const onKeyDown = (event) => {
690
+ if (event.key === "Shift") {
691
+ shiftHeldRef.current = true;
692
+ }
693
+ const action = matchAction(event, keyMap);
694
+ if (!action) {
695
+ return;
696
+ }
697
+ if (controlsRef.current?.isInteractionLocked()) {
698
+ return;
699
+ }
700
+ event.preventDefault();
701
+ if (action === "reset") {
702
+ if (!event.repeat) {
703
+ controlsRef.current?.reset();
704
+ }
705
+ return;
706
+ }
707
+ if (action === "previousScene") {
708
+ if (!event.repeat) {
709
+ onPreviousSceneRef.current?.();
710
+ }
711
+ return;
712
+ }
713
+ if (action === "nextScene") {
714
+ if (!event.repeat) {
715
+ onNextSceneRef.current?.();
716
+ }
717
+ return;
718
+ }
719
+ pressedRef.current.add(action);
720
+ syncKeyboardActive();
721
+ };
722
+ const onKeyUp = (event) => {
723
+ if (event.key === "Shift") {
724
+ shiftHeldRef.current = false;
725
+ }
726
+ const action = matchAction(event, keyMap);
727
+ if (!action || !CONTINUOUS_ACTIONS.has(action)) {
728
+ return;
729
+ }
730
+ pressedRef.current.delete(action);
731
+ syncKeyboardActive();
732
+ };
733
+ const onBlur = () => {
734
+ clearPressed();
735
+ };
736
+ element.addEventListener("keydown", onKeyDown);
737
+ element.addEventListener("keyup", onKeyUp);
738
+ element.addEventListener("blur", onBlur);
739
+ window.addEventListener("blur", onBlur);
740
+ return () => {
741
+ element.removeEventListener("keydown", onKeyDown);
742
+ element.removeEventListener("keyup", onKeyUp);
743
+ element.removeEventListener("blur", onBlur);
744
+ window.removeEventListener("blur", onBlur);
745
+ clearPressed();
746
+ };
747
+ }, [controlsRef, enabled, gl, keyMap]);
748
+ useFrame3((_, deltaSeconds) => {
749
+ if (!enabled || pressedRef.current.size === 0) {
750
+ return;
751
+ }
752
+ const controls = controlsRef.current;
753
+ if (!controls) {
754
+ return;
755
+ }
756
+ const multiplier = shiftHeldRef.current ? resolveSpeed(shiftMultiplier, DEFAULT_SHIFT_MULTIPLIER) : 1;
757
+ const rotation = resolveSpeed(rotateSpeed, DEFAULT_ROTATE_SPEED) * multiplier * deltaSeconds;
758
+ let zoom = resolveSpeed(zoomSpeed, DEFAULT_ZOOM_SPEED) * multiplier * deltaSeconds;
759
+ const fovCap = fovSpeedRef.current;
760
+ if (Number.isFinite(fovCap) && fovCap >= 0) {
761
+ zoom = Math.min(zoom, fovCap * deltaSeconds);
762
+ }
763
+ const pitchSign = invertRef.current ? -1 : 1;
764
+ let yaw = 0;
765
+ let pitch = 0;
766
+ let fov = 0;
767
+ for (const action of pressedRef.current) {
768
+ switch (action) {
769
+ case "left":
770
+ yaw -= rotation;
771
+ break;
772
+ case "right":
773
+ yaw += rotation;
774
+ break;
775
+ case "up":
776
+ pitch += pitchSign * rotation;
777
+ break;
778
+ case "down":
779
+ pitch -= pitchSign * rotation;
780
+ break;
781
+ case "zoomIn":
782
+ fov -= zoom;
783
+ break;
784
+ case "zoomOut":
785
+ fov += zoom;
786
+ break;
787
+ case "previousScene":
788
+ case "nextScene":
789
+ case "reset":
790
+ break;
791
+ default: {
792
+ const _exhaustive = action;
793
+ void _exhaustive;
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ if (yaw !== 0 || pitch !== 0 || fov !== 0) {
799
+ controls.applyViewDelta({ yaw, pitch, fov });
800
+ }
801
+ });
802
+ return null;
803
+ }
804
+
805
+ // src/mouse-controls.tsx
806
+ import { useThree as useThree3 } from "@react-three/fiber";
807
+ import { useContext as useContext4, useEffect as useEffect5, useRef as useRef5 } from "react";
808
+ var DEFAULT_ROTATE_SPEED2 = 0.35;
809
+ var DEFAULT_ZOOM_SPEED2 = 0.08;
810
+ var BUTTON_FLAG = {
811
+ left: 0,
812
+ middle: 1,
813
+ right: 2
814
+ };
815
+ function resolveSpeed2(value, fallback) {
816
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
817
+ }
818
+ function resolveButtons(buttons) {
819
+ const list = buttons && buttons.length > 0 ? buttons : ["left"];
820
+ return new Set(list.map((button) => BUTTON_FLAG[button]));
821
+ }
822
+ function MouseControls({
823
+ enabled = true,
824
+ rotateSpeed = DEFAULT_ROTATE_SPEED2,
825
+ zoomSpeed = DEFAULT_ZOOM_SPEED2,
826
+ wheel = true,
827
+ invert = false,
828
+ buttons,
829
+ fovSpeed
830
+ }) {
831
+ const controlsRef = useContext4(PanoramaViewContext);
832
+ const { gl } = useThree3();
833
+ const pointersRef = useRef5(
834
+ /* @__PURE__ */ new Map()
835
+ );
836
+ const optionsRef = useRef5({
837
+ rotateSpeed,
838
+ zoomSpeed,
839
+ wheel,
840
+ invert,
841
+ fovSpeed
842
+ });
843
+ if (!controlsRef) {
844
+ throw new Error("<MouseControls> must be rendered inside <PanoView>.");
845
+ }
846
+ optionsRef.current = {
847
+ rotateSpeed,
848
+ zoomSpeed,
849
+ wheel,
850
+ invert,
851
+ fovSpeed
852
+ };
853
+ useEffect5(() => {
854
+ const element = gl.domElement;
855
+ const allowedButtons = resolveButtons(buttons);
856
+ const clearPointers = () => {
857
+ for (const pointerId of pointersRef.current.keys()) {
858
+ if (element.hasPointerCapture?.(pointerId)) {
859
+ element.releasePointerCapture?.(pointerId);
860
+ }
861
+ }
862
+ pointersRef.current.clear();
863
+ controlsRef.current?.setPointerActive("mouse", false);
864
+ };
865
+ if (!enabled) {
866
+ clearPointers();
867
+ return;
868
+ }
869
+ const isMouseLike = (event) => event.pointerType === "mouse" || event.pointerType === "pen";
870
+ const syncActive = () => {
871
+ controlsRef.current?.setPointerActive(
872
+ "mouse",
873
+ pointersRef.current.size > 0
874
+ );
875
+ };
876
+ const onPointerDown = (event) => {
877
+ if (!isMouseLike(event)) {
878
+ return;
879
+ }
880
+ if (controlsRef.current?.isInteractionLocked()) {
881
+ return;
882
+ }
883
+ if (!allowedButtons.has(event.button)) {
884
+ return;
885
+ }
886
+ event.preventDefault();
887
+ pointersRef.current.set(event.pointerId, {
888
+ x: event.clientX,
889
+ y: event.clientY,
890
+ at: performance.now()
891
+ });
892
+ element.setPointerCapture?.(event.pointerId);
893
+ syncActive();
894
+ };
895
+ const onPointerMove = (event) => {
896
+ if (!isMouseLike(event)) {
897
+ return;
898
+ }
899
+ if (controlsRef.current?.isInteractionLocked()) {
900
+ return;
901
+ }
902
+ const previous = pointersRef.current.get(event.pointerId);
903
+ if (!previous) {
904
+ return;
905
+ }
906
+ event.preventDefault();
907
+ const next = {
908
+ x: event.clientX,
909
+ y: event.clientY,
910
+ at: performance.now()
911
+ };
912
+ pointersRef.current.set(event.pointerId, next);
913
+ const rect = element.getBoundingClientRect();
914
+ const speed = resolveSpeed2(
915
+ optionsRef.current.rotateSpeed,
916
+ DEFAULT_ROTATE_SPEED2
917
+ );
918
+ const sign = optionsRef.current.invert ? -1 : 1;
919
+ const deltaYaw = sign * -(next.x - previous.x) * 360 * speed / Math.max(rect.width, 1);
920
+ const deltaPitch = sign * (next.y - previous.y) * 180 * speed / Math.max(rect.height, 1);
921
+ controlsRef.current?.applyViewDelta(
922
+ { yaw: deltaYaw, pitch: deltaPitch },
923
+ { recordVelocity: true }
924
+ );
925
+ };
926
+ const releasePointer = (event) => {
927
+ if (!pointersRef.current.has(event.pointerId)) {
928
+ return;
929
+ }
930
+ pointersRef.current.delete(event.pointerId);
931
+ if (element.hasPointerCapture?.(event.pointerId)) {
932
+ element.releasePointerCapture?.(event.pointerId);
933
+ }
934
+ syncActive();
935
+ };
936
+ const onWheel = (event) => {
937
+ if (!optionsRef.current.wheel) {
938
+ return;
939
+ }
940
+ if (controlsRef.current?.isInteractionLocked()) {
941
+ event.preventDefault();
942
+ return;
943
+ }
944
+ event.preventDefault();
945
+ const speed = resolveSpeed2(
946
+ optionsRef.current.zoomSpeed,
947
+ DEFAULT_ZOOM_SPEED2
948
+ );
949
+ let delta = event.deltaY * speed;
950
+ const cap = optionsRef.current.fovSpeed;
951
+ if (Number.isFinite(cap) && cap > 0) {
952
+ delta = Math.max(-cap, Math.min(cap, delta));
953
+ }
954
+ controlsRef.current?.applyViewDelta(
955
+ { fov: delta },
956
+ { recordVelocity: true }
957
+ );
958
+ };
959
+ element.addEventListener("pointerdown", onPointerDown);
960
+ element.addEventListener("pointermove", onPointerMove);
961
+ element.addEventListener("pointerup", releasePointer);
962
+ element.addEventListener("pointercancel", releasePointer);
963
+ element.addEventListener("wheel", onWheel, { passive: false });
964
+ return () => {
965
+ element.removeEventListener("pointerdown", onPointerDown);
966
+ element.removeEventListener("pointermove", onPointerMove);
967
+ element.removeEventListener("pointerup", releasePointer);
968
+ element.removeEventListener("pointercancel", releasePointer);
969
+ element.removeEventListener("wheel", onWheel);
970
+ clearPointers();
971
+ };
972
+ }, [buttons, controlsRef, enabled, gl]);
973
+ return null;
974
+ }
975
+
976
+ // src/pano-context-menu.tsx
977
+ import {
978
+ createContext as createContext4,
979
+ useCallback as useCallback2,
980
+ useContext as useContext5,
981
+ useEffect as useEffect6,
982
+ useId,
983
+ useLayoutEffect,
984
+ useMemo as useMemo3,
985
+ useRef as useRef6,
986
+ useState as useState2
987
+ } from "react";
988
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
989
+ var DEFAULT_ICON_SIZE = 16;
990
+ var MENU_EDGE_PADDING = 8;
991
+ var MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY = 0.4;
992
+ var PanoContextMenuOverlayContext = createContext4(null);
993
+ var DEFAULT_APPEARANCE = {
994
+ background: "#161616",
995
+ color: "#f4f4f4",
996
+ border: "1px solid #2e2e2e",
997
+ borderRadius: 8,
998
+ shadow: "0 8px 24px rgba(0, 0, 0, 0.45)",
999
+ opacity: 1,
1000
+ fontSize: 13,
1001
+ minWidth: 180,
1002
+ padding: 4,
1003
+ iconSize: DEFAULT_ICON_SIZE,
1004
+ itemPadding: "6px 10px",
1005
+ itemHoverBackground: "#2a2a2a",
1006
+ itemDisabledColor: "#777777",
1007
+ separatorColor: "#333333",
1008
+ separatorMargin: "4px 6px"
1009
+ };
1010
+ function clampBackgroundOpacity(value) {
1011
+ if (!Number.isFinite(value)) {
1012
+ return DEFAULT_APPEARANCE.opacity;
1013
+ }
1014
+ return Math.min(
1015
+ 1,
1016
+ Math.max(MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY, value)
1017
+ );
1018
+ }
1019
+ function isActionItem(item) {
1020
+ return item.type !== "separator";
1021
+ }
1022
+ function DefaultContextMenuPanel({
1023
+ items,
1024
+ appearance,
1025
+ className,
1026
+ style,
1027
+ itemClassName,
1028
+ separatorClassName,
1029
+ selectContext,
1030
+ onRequestClose
1031
+ }) {
1032
+ const menuRef = useRef6(null);
1033
+ const [activeIndex, setActiveIndex] = useState2(0);
1034
+ const resolved = { ...DEFAULT_APPEARANCE, ...appearance };
1035
+ const iconSize = Number.isFinite(resolved.iconSize) ? Math.max(0, resolved.iconSize) : DEFAULT_ICON_SIZE;
1036
+ const visibleItems = useMemo3(
1037
+ () => items.filter((item) => {
1038
+ if (!isActionItem(item)) {
1039
+ return true;
1040
+ }
1041
+ return !item.hidden;
1042
+ }),
1043
+ [items]
1044
+ );
1045
+ const focusableIndices = useMemo3(() => {
1046
+ const indices = [];
1047
+ visibleItems.forEach((item, index) => {
1048
+ if (isActionItem(item) && !item.disabled) {
1049
+ indices.push(index);
1050
+ }
1051
+ });
1052
+ return indices;
1053
+ }, [visibleItems]);
1054
+ useEffect6(() => {
1055
+ const first = focusableIndices[0];
1056
+ setActiveIndex(first ?? 0);
1057
+ const frame = requestAnimationFrame(() => {
1058
+ const buttons = menuRef.current?.querySelectorAll(
1059
+ '[role="menuitem"]:not([aria-disabled="true"])'
1060
+ );
1061
+ buttons?.[0]?.focus();
1062
+ });
1063
+ return () => cancelAnimationFrame(frame);
1064
+ }, [focusableIndices]);
1065
+ const activateItem = useCallback2(
1066
+ (item) => {
1067
+ if (item.disabled) {
1068
+ return;
1069
+ }
1070
+ item.onSelect(selectContext);
1071
+ onRequestClose();
1072
+ },
1073
+ [onRequestClose, selectContext]
1074
+ );
1075
+ const moveFocus = (direction) => {
1076
+ if (focusableIndices.length === 0) {
1077
+ return;
1078
+ }
1079
+ const currentSlot = focusableIndices.indexOf(activeIndex);
1080
+ const nextSlot = currentSlot === -1 ? 0 : (currentSlot + direction + focusableIndices.length) % focusableIndices.length;
1081
+ const nextIndex = focusableIndices[nextSlot];
1082
+ setActiveIndex(nextIndex);
1083
+ const buttons = menuRef.current?.querySelectorAll(
1084
+ '[role="menuitem"]'
1085
+ );
1086
+ buttons?.[nextIndex]?.focus();
1087
+ };
1088
+ const handleKeyDown = (event) => {
1089
+ switch (event.key) {
1090
+ case "ArrowDown":
1091
+ event.preventDefault();
1092
+ moveFocus(1);
1093
+ break;
1094
+ case "ArrowUp":
1095
+ event.preventDefault();
1096
+ moveFocus(-1);
1097
+ break;
1098
+ case "Home": {
1099
+ event.preventDefault();
1100
+ const first = focusableIndices[0];
1101
+ if (first !== void 0) {
1102
+ setActiveIndex(first);
1103
+ const buttons = menuRef.current?.querySelectorAll(
1104
+ '[role="menuitem"]'
1105
+ );
1106
+ buttons?.[first]?.focus();
1107
+ }
1108
+ break;
1109
+ }
1110
+ case "End": {
1111
+ event.preventDefault();
1112
+ const last = focusableIndices[focusableIndices.length - 1];
1113
+ if (last !== void 0) {
1114
+ setActiveIndex(last);
1115
+ const buttons = menuRef.current?.querySelectorAll(
1116
+ '[role="menuitem"]'
1117
+ );
1118
+ buttons?.[last]?.focus();
1119
+ }
1120
+ break;
1121
+ }
1122
+ case "Escape":
1123
+ event.preventDefault();
1124
+ onRequestClose();
1125
+ break;
1126
+ case "Enter":
1127
+ case " ": {
1128
+ event.preventDefault();
1129
+ const item = visibleItems[activeIndex];
1130
+ if (item && isActionItem(item)) {
1131
+ activateItem(item);
1132
+ }
1133
+ break;
1134
+ }
1135
+ default: {
1136
+ break;
1137
+ }
1138
+ }
1139
+ };
1140
+ const backgroundOpacity = clampBackgroundOpacity(resolved.opacity);
1141
+ const panelStyle = {
1142
+ color: resolved.color,
1143
+ border: resolved.border,
1144
+ borderRadius: resolved.borderRadius,
1145
+ boxShadow: resolved.shadow,
1146
+ fontSize: resolved.fontSize,
1147
+ minWidth: resolved.minWidth,
1148
+ padding: resolved.padding,
1149
+ pointerEvents: "auto",
1150
+ position: "relative",
1151
+ userSelect: "none",
1152
+ ...style
1153
+ };
1154
+ return /* @__PURE__ */ jsxs(
1155
+ "div",
1156
+ {
1157
+ ref: menuRef,
1158
+ "aria-label": "Panorama context menu",
1159
+ className,
1160
+ onKeyDown: handleKeyDown,
1161
+ role: "menu",
1162
+ style: panelStyle,
1163
+ tabIndex: -1,
1164
+ children: [
1165
+ /* @__PURE__ */ jsx2(
1166
+ "div",
1167
+ {
1168
+ "aria-hidden": true,
1169
+ style: {
1170
+ background: resolved.background,
1171
+ borderRadius: resolved.borderRadius,
1172
+ inset: 0,
1173
+ opacity: backgroundOpacity,
1174
+ pointerEvents: "none",
1175
+ position: "absolute",
1176
+ zIndex: 0
1177
+ }
1178
+ }
1179
+ ),
1180
+ /* @__PURE__ */ jsx2("div", { style: { position: "relative", zIndex: 1 }, children: visibleItems.map((item, index) => {
1181
+ if (!isActionItem(item)) {
1182
+ return /* @__PURE__ */ jsx2(
1183
+ "div",
1184
+ {
1185
+ "aria-hidden": true,
1186
+ className: [separatorClassName, item.className].filter(Boolean).join(" ") || void 0,
1187
+ role: "separator",
1188
+ style: {
1189
+ background: resolved.separatorColor,
1190
+ height: 1,
1191
+ margin: resolved.separatorMargin,
1192
+ ...item.style
1193
+ }
1194
+ },
1195
+ item.id ?? `separator-${index}`
1196
+ );
1197
+ }
1198
+ const disabled = item.disabled === true;
1199
+ const iconNode = item.icon ?? (item.image ? /* @__PURE__ */ jsx2(
1200
+ "img",
1201
+ {
1202
+ alt: item.imageAlt ?? "",
1203
+ draggable: false,
1204
+ src: item.image,
1205
+ style: {
1206
+ display: "block",
1207
+ height: iconSize,
1208
+ objectFit: "contain",
1209
+ width: iconSize
1210
+ }
1211
+ }
1212
+ ) : null);
1213
+ return /* @__PURE__ */ jsxs(
1214
+ "button",
1215
+ {
1216
+ "aria-disabled": disabled || void 0,
1217
+ className: [itemClassName, item.className].filter(Boolean).join(" ") || void 0,
1218
+ disabled,
1219
+ onClick: (event) => {
1220
+ event.preventDefault();
1221
+ event.stopPropagation();
1222
+ activateItem(item);
1223
+ },
1224
+ onMouseEnter: (event) => {
1225
+ if (!disabled) {
1226
+ setActiveIndex(index);
1227
+ event.currentTarget.focus();
1228
+ }
1229
+ },
1230
+ role: "menuitem",
1231
+ style: {
1232
+ alignItems: "center",
1233
+ background: !disabled && activeIndex === index ? resolved.itemHoverBackground : "transparent",
1234
+ border: "none",
1235
+ borderRadius: 4,
1236
+ color: disabled ? resolved.itemDisabledColor : "inherit",
1237
+ cursor: disabled ? "default" : "pointer",
1238
+ display: "flex",
1239
+ font: "inherit",
1240
+ gap: 8,
1241
+ padding: resolved.itemPadding,
1242
+ textAlign: "left",
1243
+ width: "100%",
1244
+ ...item.style
1245
+ },
1246
+ tabIndex: disabled ? -1 : activeIndex === index ? 0 : -1,
1247
+ type: "button",
1248
+ children: [
1249
+ /* @__PURE__ */ jsx2(
1250
+ "span",
1251
+ {
1252
+ "aria-hidden": true,
1253
+ style: {
1254
+ alignItems: "center",
1255
+ display: "inline-flex",
1256
+ flexShrink: 0,
1257
+ height: iconSize,
1258
+ justifyContent: "center",
1259
+ width: iconSize
1260
+ },
1261
+ children: iconNode
1262
+ }
1263
+ ),
1264
+ /* @__PURE__ */ jsx2("span", { style: { flex: 1 }, children: item.label })
1265
+ ]
1266
+ },
1267
+ item.id
1268
+ );
1269
+ }) })
1270
+ ]
1271
+ }
1272
+ );
1273
+ }
1274
+ function PositionedMenuShell({
1275
+ clientX,
1276
+ clientY,
1277
+ overlayElement,
1278
+ children
1279
+ }) {
1280
+ const shellRef = useRef6(null);
1281
+ const [offset, setOffset] = useState2({ left: 0, top: 0 });
1282
+ useLayoutEffect(() => {
1283
+ const shell = shellRef.current;
1284
+ if (!overlayElement || !shell) {
1285
+ return;
1286
+ }
1287
+ const overlayRect = overlayElement.getBoundingClientRect();
1288
+ const menuRect = shell.getBoundingClientRect();
1289
+ const rawLeft = clientX - overlayRect.left;
1290
+ const rawTop = clientY - overlayRect.top;
1291
+ const maxLeft = Math.max(
1292
+ MENU_EDGE_PADDING,
1293
+ overlayRect.width - menuRect.width - MENU_EDGE_PADDING
1294
+ );
1295
+ const maxTop = Math.max(
1296
+ MENU_EDGE_PADDING,
1297
+ overlayRect.height - menuRect.height - MENU_EDGE_PADDING
1298
+ );
1299
+ setOffset({
1300
+ left: Math.min(Math.max(rawLeft, MENU_EDGE_PADDING), maxLeft),
1301
+ top: Math.min(Math.max(rawTop, MENU_EDGE_PADDING), maxTop)
1302
+ });
1303
+ }, [clientX, clientY, overlayElement, children]);
1304
+ return /* @__PURE__ */ jsx2(
1305
+ "div",
1306
+ {
1307
+ ref: shellRef,
1308
+ style: {
1309
+ left: offset.left,
1310
+ position: "absolute",
1311
+ top: offset.top,
1312
+ zIndex: 20
1313
+ },
1314
+ children
1315
+ }
1316
+ );
1317
+ }
1318
+ function usePanoContextMenuOverlay() {
1319
+ const [session, setSession] = useState2(null);
1320
+ const overlayRef = useRef6(null);
1321
+ const [overlayElement, setOverlayElement] = useState2(
1322
+ null
1323
+ );
1324
+ const api = useMemo3(
1325
+ () => ({ setSession, overlayRef }),
1326
+ []
1327
+ );
1328
+ const setOverlayNode = useCallback2((node) => {
1329
+ overlayRef.current = node;
1330
+ setOverlayElement(node);
1331
+ }, []);
1332
+ useEffect6(() => {
1333
+ if (!session) {
1334
+ return;
1335
+ }
1336
+ const handlePointerDown = (event) => {
1337
+ const overlay2 = overlayRef.current;
1338
+ if (!overlay2) {
1339
+ return;
1340
+ }
1341
+ const target = event.target;
1342
+ if (target instanceof Node && overlay2.contains(target)) {
1343
+ const menuRoot = overlay2.querySelector(
1344
+ '[role="menu"], [data-pano-context-menu]'
1345
+ );
1346
+ if (menuRoot && menuRoot.contains(target)) {
1347
+ return;
1348
+ }
1349
+ }
1350
+ setSession(null);
1351
+ };
1352
+ const handleKeyDown = (event) => {
1353
+ if (event.key === "Escape") {
1354
+ setSession(null);
1355
+ }
1356
+ };
1357
+ document.addEventListener("pointerdown", handlePointerDown, true);
1358
+ document.addEventListener("keydown", handleKeyDown);
1359
+ return () => {
1360
+ document.removeEventListener("pointerdown", handlePointerDown, true);
1361
+ document.removeEventListener("keydown", handleKeyDown);
1362
+ };
1363
+ }, [session]);
1364
+ const overlay = /* @__PURE__ */ jsx2(
1365
+ "div",
1366
+ {
1367
+ ref: setOverlayNode,
1368
+ "aria-hidden": session ? void 0 : true,
1369
+ style: {
1370
+ inset: 0,
1371
+ pointerEvents: "none",
1372
+ position: "absolute",
1373
+ zIndex: 20
1374
+ },
1375
+ children: session ? /* @__PURE__ */ jsx2(
1376
+ PositionedMenuShell,
1377
+ {
1378
+ clientX: session.event.clientX,
1379
+ clientY: session.event.clientY,
1380
+ overlayElement,
1381
+ children: session.render()
1382
+ },
1383
+ session.key
1384
+ ) : null
1385
+ }
1386
+ );
1387
+ return { api, overlay };
1388
+ }
1389
+ function PanoContextMenu({
1390
+ items,
1391
+ prepend,
1392
+ append,
1393
+ appearance,
1394
+ className,
1395
+ style,
1396
+ itemClassName,
1397
+ separatorClassName,
1398
+ children
1399
+ }) {
1400
+ const eventBus = useContext5(PanoEventBusContext);
1401
+ const controlsRef = useContext5(PanoramaViewContext);
1402
+ const overlayApi = useContext5(PanoContextMenuOverlayContext);
1403
+ const actions = useContext5(PanoContextMenuActionsContext);
1404
+ const instanceId = useId();
1405
+ const sessionKeyRef = useRef6(0);
1406
+ const propsRef = useRef6({
1407
+ items,
1408
+ prepend,
1409
+ append,
1410
+ appearance,
1411
+ className,
1412
+ style,
1413
+ itemClassName,
1414
+ separatorClassName,
1415
+ children
1416
+ });
1417
+ propsRef.current = {
1418
+ items,
1419
+ prepend,
1420
+ append,
1421
+ appearance,
1422
+ className,
1423
+ style,
1424
+ itemClassName,
1425
+ separatorClassName,
1426
+ children
1427
+ };
1428
+ if (!eventBus || !controlsRef) {
1429
+ throw new Error("<PanoContextMenu> must be rendered inside <PanoView>.");
1430
+ }
1431
+ if (!overlayApi) {
1432
+ throw new Error(
1433
+ "<PanoContextMenu> requires the PanoView context-menu overlay host."
1434
+ );
1435
+ }
1436
+ const close = useCallback2(() => {
1437
+ overlayApi.setSession(null);
1438
+ }, [overlayApi]);
1439
+ const openMenu = useCallback2(
1440
+ (event) => {
1441
+ event.nativeEvent.preventDefault();
1442
+ const view = controlsRef.current?.getView() ?? {
1443
+ yaw: 0,
1444
+ pitch: 0,
1445
+ fov: 75
1446
+ };
1447
+ const selectContext = {
1448
+ position: event.position,
1449
+ view,
1450
+ nativeEvent: event.nativeEvent,
1451
+ close
1452
+ };
1453
+ const current = propsRef.current;
1454
+ sessionKeyRef.current += 1;
1455
+ const key = `${instanceId}-${sessionKeyRef.current}`;
1456
+ const hasCustomChildren = typeof current.children === "function";
1457
+ const presets = actions ? createPanoContextMenuPresets(actions) : null;
1458
+ const resolvedItems = presets ? composePanoContextMenuItems(current, presets) : current.items?.filter(isActionEntry) ?? [];
1459
+ if (!hasCustomChildren && resolvedItems.length === 0) {
1460
+ return;
1461
+ }
1462
+ overlayApi.setSession({
1463
+ key,
1464
+ event,
1465
+ view,
1466
+ render: () => {
1467
+ if (hasCustomChildren && current.children) {
1468
+ return /* @__PURE__ */ jsx2("div", { "data-pano-context-menu": true, style: { pointerEvents: "auto" }, children: current.children({ event, view, close }) });
1469
+ }
1470
+ return /* @__PURE__ */ jsx2(
1471
+ DefaultContextMenuPanel,
1472
+ {
1473
+ appearance: current.appearance,
1474
+ className: current.className,
1475
+ itemClassName: current.itemClassName,
1476
+ items: resolvedItems,
1477
+ onRequestClose: close,
1478
+ selectContext,
1479
+ separatorClassName: current.separatorClassName,
1480
+ style: current.style
1481
+ }
1482
+ );
1483
+ }
1484
+ });
1485
+ },
1486
+ [actions, close, controlsRef, instanceId, overlayApi]
1487
+ );
1488
+ useEffect6(() => {
1489
+ const unsubscribeContextMenu = eventBus.subscribe(
1490
+ "contextmenu",
1491
+ openMenu
1492
+ );
1493
+ const unsubscribeInteraction = eventBus.subscribe(
1494
+ "viewinteractionstart",
1495
+ () => {
1496
+ close();
1497
+ }
1498
+ );
1499
+ return () => {
1500
+ unsubscribeContextMenu();
1501
+ unsubscribeInteraction();
1502
+ close();
1503
+ };
1504
+ }, [close, eventBus, openMenu]);
1505
+ return null;
1506
+ }
1507
+ function ResetViewIcon() {
1508
+ return /* @__PURE__ */ jsxs(
1509
+ "svg",
1510
+ {
1511
+ "aria-hidden": true,
1512
+ fill: "none",
1513
+ height: "100%",
1514
+ stroke: "currentColor",
1515
+ strokeLinecap: "round",
1516
+ strokeLinejoin: "round",
1517
+ strokeWidth: "1.75",
1518
+ viewBox: "0 0 16 16",
1519
+ width: "100%",
1520
+ children: [
1521
+ /* @__PURE__ */ jsx2("path", { d: "M3.5 6.5A4.5 4.5 0 1 1 4 10" }),
1522
+ /* @__PURE__ */ jsx2("path", { d: "M3.5 3.5v3h3" })
1523
+ ]
1524
+ }
1525
+ );
1526
+ }
1527
+ function FullscreenIcon() {
1528
+ return /* @__PURE__ */ jsx2(
1529
+ "svg",
1530
+ {
1531
+ "aria-hidden": true,
1532
+ fill: "none",
1533
+ height: "100%",
1534
+ stroke: "currentColor",
1535
+ strokeLinecap: "round",
1536
+ strokeLinejoin: "round",
1537
+ strokeWidth: "1.75",
1538
+ viewBox: "0 0 16 16",
1539
+ width: "100%",
1540
+ children: /* @__PURE__ */ jsx2("path", { d: "M3 6V3h3M10 3h3v3M13 10v3h-3M6 13H3v-3" })
1541
+ }
1542
+ );
1543
+ }
1544
+ function ExitFullscreenIcon() {
1545
+ return /* @__PURE__ */ jsx2(
1546
+ "svg",
1547
+ {
1548
+ "aria-hidden": true,
1549
+ fill: "none",
1550
+ height: "100%",
1551
+ stroke: "currentColor",
1552
+ strokeLinecap: "round",
1553
+ strokeLinejoin: "round",
1554
+ strokeWidth: "1.75",
1555
+ viewBox: "0 0 16 16",
1556
+ width: "100%",
1557
+ children: /* @__PURE__ */ jsx2("path", { d: "M6 3v3H3M10 3v3h3M3 10h3v3M10 10h3v3" })
1558
+ }
1559
+ );
1560
+ }
1561
+ function createPanoContextMenuPresets(actions) {
1562
+ const isFullscreen = actions.isFullscreen === true;
1563
+ const resetView = {
1564
+ id: "pano-reset-view",
1565
+ label: "Reset view",
1566
+ icon: /* @__PURE__ */ jsx2(ResetViewIcon, {}),
1567
+ onSelect: () => {
1568
+ actions.reset();
1569
+ }
1570
+ };
1571
+ const fullscreen = {
1572
+ id: "pano-fullscreen",
1573
+ label: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
1574
+ icon: isFullscreen ? /* @__PURE__ */ jsx2(ExitFullscreenIcon, {}) : /* @__PURE__ */ jsx2(FullscreenIcon, {}),
1575
+ onSelect: () => {
1576
+ actions.toggleFullscreen();
1577
+ }
1578
+ };
1579
+ const separator = (id = "pano-separator") => ({
1580
+ type: "separator",
1581
+ id
1582
+ });
1583
+ return {
1584
+ resetView,
1585
+ fullscreen,
1586
+ separator,
1587
+ defaults: [resetView, separator("pano-default-separator"), fullscreen]
1588
+ };
1589
+ }
1590
+ function createDefaultPanoContextMenuItems(actions) {
1591
+ return createPanoContextMenuPresets(actions).defaults;
1592
+ }
1593
+ function isPresetId(value) {
1594
+ return value === "resetView" || value === "fullscreen" || value === "separator";
1595
+ }
1596
+ function isPresetRef(value) {
1597
+ return typeof value === "object" && value !== null && "preset" in value && isPresetId(value.preset);
1598
+ }
1599
+ function isActionEntry(value) {
1600
+ if (typeof value === "string") {
1601
+ return false;
1602
+ }
1603
+ if (isPresetRef(value)) {
1604
+ return false;
1605
+ }
1606
+ return true;
1607
+ }
1608
+ function applyPresetOverrides(item, overrides) {
1609
+ if (item.type === "separator") {
1610
+ return {
1611
+ ...item,
1612
+ id: overrides.id ?? item.id,
1613
+ className: overrides.className ?? item.className,
1614
+ style: overrides.style ?? item.style
1615
+ };
1616
+ }
1617
+ return {
1618
+ ...item,
1619
+ id: overrides.id ?? item.id,
1620
+ label: overrides.label ?? item.label,
1621
+ className: overrides.className ?? item.className,
1622
+ style: overrides.style ?? item.style,
1623
+ disabled: overrides.disabled ?? item.disabled,
1624
+ hidden: overrides.hidden ?? item.hidden
1625
+ };
1626
+ }
1627
+ function resolvePresetEntry(entry, presets) {
1628
+ const presetId = typeof entry === "string" ? entry : entry.preset;
1629
+ const overrides = typeof entry === "string" ? {} : entry;
1630
+ switch (presetId) {
1631
+ case "resetView":
1632
+ return applyPresetOverrides(presets.resetView, overrides);
1633
+ case "fullscreen":
1634
+ return applyPresetOverrides(presets.fullscreen, overrides);
1635
+ case "separator":
1636
+ return applyPresetOverrides(
1637
+ presets.separator(overrides.id ?? "pano-separator"),
1638
+ overrides
1639
+ );
1640
+ default: {
1641
+ const _exhaustive = presetId;
1642
+ return _exhaustive;
1643
+ }
1644
+ }
1645
+ }
1646
+ function resolvePanoContextMenuEntries(entries, presets) {
1647
+ const resolved = [];
1648
+ for (const entry of entries) {
1649
+ if (isActionEntry(entry)) {
1650
+ resolved.push(entry);
1651
+ continue;
1652
+ }
1653
+ resolved.push(resolvePresetEntry(entry, presets));
1654
+ }
1655
+ return resolved;
1656
+ }
1657
+ function composePanoContextMenuItems(props, presets) {
1658
+ if (props.items) {
1659
+ return resolvePanoContextMenuEntries(props.items, presets);
1660
+ }
1661
+ return [
1662
+ ...resolvePanoContextMenuEntries(props.prepend ?? [], presets),
1663
+ ...presets.defaults,
1664
+ ...resolvePanoContextMenuEntries(props.append ?? [], presets)
1665
+ ];
1666
+ }
1667
+ var PanoContextMenuActionsContext = createContext4(null);
1668
+
1669
+ // src/pano-events.tsx
1670
+ import { useFrame as useFrame4, useThree as useThree4 } from "@react-three/fiber";
1671
+ import { useContext as useContext6, useEffect as useEffect7, useRef as useRef7 } from "react";
1672
+ var DEFAULT_IDLE_TIME_MS = 2e3;
1673
+ function resolveIdleTime(value) {
1674
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIME_MS;
1675
+ }
1676
+ function usePanoEvents({
1677
+ idleTime = DEFAULT_IDLE_TIME_MS,
1678
+ onViewChange,
1679
+ onViewSettled,
1680
+ onViewInteractionStart,
1681
+ onViewInteractionEnd,
1682
+ onClick,
1683
+ onDoubleClick,
1684
+ onPointerDown,
1685
+ onPointerUp,
1686
+ onPointerMove,
1687
+ onContextMenu,
1688
+ onWheel,
1689
+ onIdle,
1690
+ onIdleEnd,
1691
+ onEnterFullscreen,
1692
+ onExitFullscreen,
1693
+ onResize,
1694
+ onAutoRotateStart,
1695
+ onAutoRotateStop,
1696
+ onAutoRotateOneRound
1697
+ }) {
1698
+ const eventBus = useContext6(PanoEventBusContext);
1699
+ const controlsRef = useContext6(PanoramaViewContext);
1700
+ const { gl } = useThree4();
1701
+ if (!eventBus || !controlsRef) {
1702
+ throw new Error("usePanoEvents must be used inside <PanoView>.");
1703
+ }
1704
+ const callbacksRef = useRef7({
1705
+ onViewChange,
1706
+ onViewSettled,
1707
+ onViewInteractionStart,
1708
+ onViewInteractionEnd,
1709
+ onClick,
1710
+ onDoubleClick,
1711
+ onPointerDown,
1712
+ onPointerUp,
1713
+ onPointerMove,
1714
+ onContextMenu,
1715
+ onWheel,
1716
+ onIdle,
1717
+ onIdleEnd,
1718
+ onEnterFullscreen,
1719
+ onExitFullscreen,
1720
+ onResize,
1721
+ onAutoRotateStart,
1722
+ onAutoRotateStop,
1723
+ onAutoRotateOneRound
1724
+ });
1725
+ callbacksRef.current = {
1726
+ onViewChange,
1727
+ onViewSettled,
1728
+ onViewInteractionStart,
1729
+ onViewInteractionEnd,
1730
+ onClick,
1731
+ onDoubleClick,
1732
+ onPointerDown,
1733
+ onPointerUp,
1734
+ onPointerMove,
1735
+ onContextMenu,
1736
+ onWheel,
1737
+ onIdle,
1738
+ onIdleEnd,
1739
+ onEnterFullscreen,
1740
+ onExitFullscreen,
1741
+ onResize,
1742
+ onAutoRotateStart,
1743
+ onAutoRotateStop,
1744
+ onAutoRotateOneRound
1745
+ };
1746
+ const idleElapsedRef = useRef7(0);
1747
+ const isIdleRef = useRef7(false);
1748
+ const idleTimeRef = useRef7(resolveIdleTime(idleTime));
1749
+ idleTimeRef.current = resolveIdleTime(idleTime);
1750
+ useEffect7(() => {
1751
+ idleElapsedRef.current = 0;
1752
+ isIdleRef.current = false;
1753
+ }, [idleTime]);
1754
+ useEffect7(() => {
1755
+ const unsubscribers = [
1756
+ eventBus.subscribe("viewchange", (view) => {
1757
+ callbacksRef.current.onViewChange?.(view);
1758
+ }),
1759
+ eventBus.subscribe("viewsettled", (view) => {
1760
+ callbacksRef.current.onViewSettled?.(view);
1761
+ }),
1762
+ eventBus.subscribe("viewinteractionstart", (event) => {
1763
+ callbacksRef.current.onViewInteractionStart?.(event);
1764
+ }),
1765
+ eventBus.subscribe("viewinteractionend", (event) => {
1766
+ callbacksRef.current.onViewInteractionEnd?.(event);
1767
+ }),
1768
+ eventBus.subscribe("click", (event) => {
1769
+ callbacksRef.current.onClick?.(event);
1770
+ }),
1771
+ eventBus.subscribe("doubleclick", (event) => {
1772
+ callbacksRef.current.onDoubleClick?.(event);
1773
+ }),
1774
+ eventBus.subscribe("pointerdown", (event) => {
1775
+ callbacksRef.current.onPointerDown?.(event);
1776
+ }),
1777
+ eventBus.subscribe("pointerup", (event) => {
1778
+ callbacksRef.current.onPointerUp?.(event);
1779
+ }),
1780
+ eventBus.subscribe("pointermove", (event) => {
1781
+ callbacksRef.current.onPointerMove?.(event);
1782
+ }),
1783
+ eventBus.subscribe("contextmenu", (event) => {
1784
+ callbacksRef.current.onContextMenu?.(event);
1785
+ }),
1786
+ eventBus.subscribe("autorotatestart", () => {
1787
+ callbacksRef.current.onAutoRotateStart?.();
1788
+ }),
1789
+ eventBus.subscribe("autorotatestop", () => {
1790
+ callbacksRef.current.onAutoRotateStop?.();
1791
+ }),
1792
+ eventBus.subscribe("autorotateoneround", () => {
1793
+ callbacksRef.current.onAutoRotateOneRound?.();
1794
+ })
1795
+ ];
1796
+ return () => {
1797
+ for (const unsubscribe of unsubscribers) {
1798
+ unsubscribe();
1799
+ }
1800
+ };
1801
+ }, [eventBus]);
1802
+ useEffect7(() => {
1803
+ const element = gl.domElement;
1804
+ const handleWheel = (nativeEvent) => {
1805
+ callbacksRef.current.onWheel?.({
1806
+ delta: nativeEvent.deltaY,
1807
+ nativeEvent
1808
+ });
1809
+ };
1810
+ element.addEventListener("wheel", handleWheel, { passive: true });
1811
+ return () => {
1812
+ element.removeEventListener("wheel", handleWheel);
1813
+ };
1814
+ }, [gl]);
1815
+ useEffect7(() => {
1816
+ if (typeof document === "undefined") {
1817
+ return;
1818
+ }
1819
+ const wasFullscreenRef = { current: false };
1820
+ const isViewerFullscreen = () => {
1821
+ const fullscreenElement = document.fullscreenElement;
1822
+ if (!fullscreenElement) {
1823
+ return false;
1824
+ }
1825
+ const canvas = gl.domElement;
1826
+ return fullscreenElement === canvas || fullscreenElement.contains(canvas) || canvas.contains(fullscreenElement);
1827
+ };
1828
+ const handleFullscreenChange = () => {
1829
+ const isOurs = isViewerFullscreen();
1830
+ if (isOurs && !wasFullscreenRef.current) {
1831
+ wasFullscreenRef.current = true;
1832
+ callbacksRef.current.onEnterFullscreen?.();
1833
+ } else if (!isOurs && wasFullscreenRef.current) {
1834
+ wasFullscreenRef.current = false;
1835
+ callbacksRef.current.onExitFullscreen?.();
1836
+ }
1837
+ };
1838
+ wasFullscreenRef.current = isViewerFullscreen();
1839
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1840
+ return () => {
1841
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1842
+ };
1843
+ }, [gl]);
1844
+ useEffect7(() => {
1845
+ const element = gl.domElement;
1846
+ if (typeof ResizeObserver === "undefined") {
1847
+ return;
1848
+ }
1849
+ const observer = new ResizeObserver((entries) => {
1850
+ const entry = entries[0];
1851
+ if (!entry) {
1852
+ return;
1853
+ }
1854
+ const { width, height } = entry.contentRect;
1855
+ callbacksRef.current.onResize?.({ width, height });
1856
+ });
1857
+ observer.observe(element);
1858
+ return () => {
1859
+ observer.disconnect();
1860
+ };
1861
+ }, [gl]);
1862
+ useFrame4((_, deltaSeconds) => {
1863
+ const interacting = controlsRef.current?.isUserInteracting() ?? false;
1864
+ if (interacting) {
1865
+ idleElapsedRef.current = 0;
1866
+ if (isIdleRef.current) {
1867
+ isIdleRef.current = false;
1868
+ callbacksRef.current.onIdleEnd?.();
1869
+ }
1870
+ return;
1871
+ }
1872
+ idleElapsedRef.current += deltaSeconds * 1e3;
1873
+ if (!isIdleRef.current && idleElapsedRef.current >= idleTimeRef.current) {
1874
+ isIdleRef.current = true;
1875
+ callbacksRef.current.onIdle?.();
1876
+ }
1877
+ });
1878
+ }
1879
+ function PanoEvents(props) {
1880
+ usePanoEvents(props);
1881
+ return null;
1882
+ }
1883
+
1884
+ // src/panorama-event-surface.tsx
1885
+ import { useThree as useThree5 } from "@react-three/fiber";
1886
+ import { useContext as useContext7, useEffect as useEffect8 } from "react";
1887
+ import { BackSide, Raycaster, Vector2, Vector3 as Vector32 } from "three";
1888
+
1889
+ // src/hotspot/coordinates.ts
1890
+ import { MathUtils as MathUtils2, Vector3 } from "three";
1891
+
1892
+ // src/hotspot/types.ts
1893
+ var MAX_HOTSPOT_PITCH = 90;
1894
+
1895
+ // src/hotspot/coordinates.ts
1896
+ var POLE_EPSILON_DEGREES = 1e-6;
1897
+ function normalizePanoYaw(yaw) {
1898
+ if (!Number.isFinite(yaw)) {
1899
+ return 0;
1900
+ }
1901
+ return MathUtils2.euclideanModulo(yaw + 180, 360) - 180;
1902
+ }
1903
+ function clampPanoPitch(pitch) {
1904
+ if (!Number.isFinite(pitch)) {
1905
+ return 0;
1906
+ }
1907
+ const clamped = MathUtils2.clamp(
1908
+ pitch,
1909
+ -MAX_HOTSPOT_PITCH,
1910
+ MAX_HOTSPOT_PITCH
1911
+ );
1912
+ return Math.abs(clamped) >= MAX_HOTSPOT_PITCH - POLE_EPSILON_DEGREES ? Math.sign(clamped) * MAX_HOTSPOT_PITCH : clamped;
1913
+ }
1914
+ function normalizePanoPosition(position) {
1915
+ const pitch = clampPanoPitch(position.pitch);
1916
+ return {
1917
+ yaw: Math.abs(pitch) === MAX_HOTSPOT_PITCH ? 0 : normalizePanoYaw(position.yaw),
1918
+ pitch
1919
+ };
1920
+ }
1921
+ function panoPositionToVector3(position, radius = 1) {
1922
+ const normalized = normalizePanoPosition(position);
1923
+ const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
1924
+ const yaw = MathUtils2.degToRad(normalized.yaw);
1925
+ const pitch = MathUtils2.degToRad(normalized.pitch);
1926
+ const cosPitch = Math.cos(pitch);
1927
+ return new Vector3(
1928
+ cosPitch * Math.sin(yaw) * safeRadius,
1929
+ Math.sin(pitch) * safeRadius,
1930
+ -cosPitch * Math.cos(yaw) * safeRadius
1931
+ );
1932
+ }
1933
+ function vector3ToPanoPosition(vector) {
1934
+ const length = Math.hypot(vector.x, vector.y, vector.z);
1935
+ if (!Number.isFinite(length) || length === 0) {
1936
+ return { yaw: 0, pitch: 0 };
1937
+ }
1938
+ return normalizePanoPosition({
1939
+ yaw: MathUtils2.radToDeg(Math.atan2(vector.x, -vector.z)),
1940
+ pitch: MathUtils2.radToDeg(
1941
+ Math.asin(MathUtils2.clamp(vector.y / length, -1, 1))
1942
+ )
1943
+ });
1944
+ }
1945
+
1946
+ // src/panorama-radius.ts
1947
+ var DEFAULT_PANORAMA_RADIUS = 1e3;
1948
+ var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
1949
+ var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
1950
+
1951
+ // src/panorama-event-surface.tsx
1952
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1953
+ var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
1954
+ function makePanoramaPointerEvent(event) {
1955
+ const nativeEvent = event.nativeEvent;
1956
+ return {
1957
+ position: vector3ToPanoPosition(event.ray.direction),
1958
+ nativeEvent,
1959
+ pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
1960
+ button: nativeEvent.button,
1961
+ buttons: nativeEvent.buttons,
1962
+ clientX: nativeEvent.clientX,
1963
+ clientY: nativeEvent.clientY
1964
+ };
1965
+ }
1966
+ function PanoramaEventSurface() {
1967
+ const eventBus = useContext7(PanoEventBusContext);
1968
+ const { camera, gl } = useThree5();
1969
+ useEffect8(() => {
1970
+ if (!eventBus) {
1971
+ return;
1972
+ }
1973
+ const element = gl.domElement;
1974
+ const ndc = new Vector2();
1975
+ const raycaster = new Raycaster();
1976
+ const direction = new Vector32();
1977
+ const handleContextMenu = (nativeEvent) => {
1978
+ const rect = element.getBoundingClientRect();
1979
+ if (rect.width <= 0 || rect.height <= 0) {
1980
+ return;
1981
+ }
1982
+ ndc.set(
1983
+ (nativeEvent.clientX - rect.left) / rect.width * 2 - 1,
1984
+ -((nativeEvent.clientY - rect.top) / rect.height) * 2 + 1
1985
+ );
1986
+ raycaster.setFromCamera(ndc, camera);
1987
+ direction.copy(raycaster.ray.direction);
1988
+ eventBus.emit("contextmenu", {
1989
+ position: vector3ToPanoPosition(direction),
1990
+ nativeEvent,
1991
+ pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
1992
+ button: nativeEvent.button,
1993
+ buttons: nativeEvent.buttons,
1994
+ clientX: nativeEvent.clientX,
1995
+ clientY: nativeEvent.clientY
1996
+ });
1997
+ };
1998
+ element.addEventListener("contextmenu", handleContextMenu);
1999
+ return () => {
2000
+ element.removeEventListener("contextmenu", handleContextMenu);
2001
+ };
2002
+ }, [camera, eventBus, gl]);
2003
+ if (!eventBus) {
2004
+ return null;
2005
+ }
2006
+ const emit = (type, event) => {
2007
+ event.stopPropagation();
2008
+ eventBus.emit(type, makePanoramaPointerEvent(event));
2009
+ };
2010
+ return /* @__PURE__ */ jsxs2(
2011
+ "mesh",
2012
+ {
2013
+ renderOrder: -200,
2014
+ onClick: (event) => emit("click", event),
2015
+ onDoubleClick: (event) => emit("doubleclick", event),
2016
+ onPointerDown: (event) => emit("pointerdown", event),
2017
+ onPointerUp: (event) => emit("pointerup", event),
2018
+ onPointerMove: (event) => emit("pointermove", event),
2019
+ children: [
2020
+ /* @__PURE__ */ jsx3("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
2021
+ /* @__PURE__ */ jsx3(
2022
+ "meshBasicMaterial",
2023
+ {
2024
+ colorWrite: false,
2025
+ depthTest: false,
2026
+ depthWrite: false,
2027
+ opacity: 0,
2028
+ side: BackSide,
2029
+ transparent: true
2030
+ }
2031
+ )
2032
+ ]
2033
+ }
2034
+ );
2035
+ }
2036
+
2037
+ // src/touch-controls.tsx
2038
+ import { useThree as useThree6 } from "@react-three/fiber";
2039
+ import { useContext as useContext8, useEffect as useEffect9, useRef as useRef8 } from "react";
2040
+ var DEFAULT_ROTATE_SPEED3 = 0.35;
2041
+ function resolveSpeed3(value, fallback) {
2042
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
2043
+ }
2044
+ function pointerDistance(a, b) {
2045
+ return Math.hypot(a.x - b.x, a.y - b.y);
2046
+ }
2047
+ function TouchControls({
2048
+ enabled = true,
2049
+ rotateSpeed = DEFAULT_ROTATE_SPEED3,
2050
+ invert = false,
2051
+ pinchZoom = true
2052
+ }) {
2053
+ const controlsRef = useContext8(PanoramaViewContext);
2054
+ const { gl } = useThree6();
2055
+ const pointersRef = useRef8(/* @__PURE__ */ new Map());
2056
+ const pinchDistanceRef = useRef8(null);
2057
+ const optionsRef = useRef8({ rotateSpeed, invert, pinchZoom });
2058
+ if (!controlsRef) {
2059
+ throw new Error("<TouchControls> must be rendered inside <PanoView>.");
2060
+ }
2061
+ optionsRef.current = { rotateSpeed, invert, pinchZoom };
2062
+ useEffect9(() => {
2063
+ const element = gl.domElement;
2064
+ const clearPointers = () => {
2065
+ for (const pointerId of pointersRef.current.keys()) {
2066
+ if (element.hasPointerCapture?.(pointerId)) {
2067
+ element.releasePointerCapture?.(pointerId);
2068
+ }
2069
+ }
2070
+ pointersRef.current.clear();
2071
+ pinchDistanceRef.current = null;
2072
+ controlsRef.current?.setPointerActive("touch", false);
2073
+ };
2074
+ if (!enabled) {
2075
+ clearPointers();
2076
+ return;
664
2077
  }
665
- );
2078
+ const syncActive = () => {
2079
+ controlsRef.current?.setPointerActive(
2080
+ "touch",
2081
+ pointersRef.current.size > 0
2082
+ );
2083
+ };
2084
+ const onPointerDown = (event) => {
2085
+ if (event.pointerType !== "touch") {
2086
+ return;
2087
+ }
2088
+ if (controlsRef.current?.isInteractionLocked()) {
2089
+ return;
2090
+ }
2091
+ event.preventDefault();
2092
+ pointersRef.current.set(event.pointerId, {
2093
+ x: event.clientX,
2094
+ y: event.clientY,
2095
+ at: performance.now()
2096
+ });
2097
+ element.setPointerCapture?.(event.pointerId);
2098
+ syncActive();
2099
+ if (pointersRef.current.size === 2) {
2100
+ const [first, second] = Array.from(pointersRef.current.values());
2101
+ pinchDistanceRef.current = pointerDistance(first, second);
2102
+ }
2103
+ };
2104
+ const onPointerMove = (event) => {
2105
+ if (event.pointerType !== "touch") {
2106
+ return;
2107
+ }
2108
+ if (controlsRef.current?.isInteractionLocked()) {
2109
+ return;
2110
+ }
2111
+ const previous = pointersRef.current.get(event.pointerId);
2112
+ if (!previous) {
2113
+ return;
2114
+ }
2115
+ event.preventDefault();
2116
+ const next = {
2117
+ x: event.clientX,
2118
+ y: event.clientY,
2119
+ at: performance.now()
2120
+ };
2121
+ pointersRef.current.set(event.pointerId, next);
2122
+ const pointers = Array.from(pointersRef.current.values());
2123
+ if (pointers.length >= 2) {
2124
+ if (!optionsRef.current.pinchZoom) {
2125
+ return;
2126
+ }
2127
+ const distance = pointerDistance(pointers[0], pointers[1]);
2128
+ const previousDistance = pinchDistanceRef.current;
2129
+ pinchDistanceRef.current = distance;
2130
+ if (previousDistance && distance > 0) {
2131
+ const view = controlsRef.current?.getTargetView();
2132
+ if (view) {
2133
+ const nextFov = view.fov * (previousDistance / distance);
2134
+ controlsRef.current?.applyViewDelta(
2135
+ { fov: nextFov - view.fov },
2136
+ { recordVelocity: true }
2137
+ );
2138
+ }
2139
+ }
2140
+ return;
2141
+ }
2142
+ const rect = element.getBoundingClientRect();
2143
+ const speed = resolveSpeed3(
2144
+ optionsRef.current.rotateSpeed,
2145
+ DEFAULT_ROTATE_SPEED3
2146
+ );
2147
+ const sign = optionsRef.current.invert ? -1 : 1;
2148
+ const deltaYaw = sign * -(next.x - previous.x) * 360 * speed / Math.max(rect.width, 1);
2149
+ const deltaPitch = sign * (next.y - previous.y) * 180 * speed / Math.max(rect.height, 1);
2150
+ controlsRef.current?.applyViewDelta(
2151
+ { yaw: deltaYaw, pitch: deltaPitch },
2152
+ { recordVelocity: true }
2153
+ );
2154
+ };
2155
+ const releasePointer = (event) => {
2156
+ if (!pointersRef.current.has(event.pointerId)) {
2157
+ return;
2158
+ }
2159
+ pointersRef.current.delete(event.pointerId);
2160
+ if (element.hasPointerCapture?.(event.pointerId)) {
2161
+ element.releasePointerCapture?.(event.pointerId);
2162
+ }
2163
+ if (pointersRef.current.size < 2) {
2164
+ pinchDistanceRef.current = null;
2165
+ }
2166
+ syncActive();
2167
+ };
2168
+ element.addEventListener("pointerdown", onPointerDown);
2169
+ element.addEventListener("pointermove", onPointerMove);
2170
+ element.addEventListener("pointerup", releasePointer);
2171
+ element.addEventListener("pointercancel", releasePointer);
2172
+ return () => {
2173
+ element.removeEventListener("pointerdown", onPointerDown);
2174
+ element.removeEventListener("pointermove", onPointerMove);
2175
+ element.removeEventListener("pointerup", releasePointer);
2176
+ element.removeEventListener("pointercancel", releasePointer);
2177
+ clearPointers();
2178
+ };
2179
+ }, [controlsRef, enabled, gl]);
2180
+ return null;
666
2181
  }
667
2182
 
668
2183
  // src/pano-view.tsx
669
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2184
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
670
2185
  var DEFAULT_VIEW = {
671
2186
  yaw: 0,
672
2187
  pitch: 0,
673
2188
  fov: 75
674
2189
  };
2190
+ var DEFAULT_POINTER_ROTATE_SPEED = 0.35;
2191
+ var DEFAULT_MOUSE_ZOOM_SPEED = 0.08;
675
2192
  var DEFAULT_CANVAS_STYLE = {
676
2193
  display: "block",
677
2194
  height: "100%",
678
2195
  width: "100%"
679
2196
  };
2197
+ function resolveChannel(value, defaults) {
2198
+ if (value === false) {
2199
+ return { mount: false };
2200
+ }
2201
+ if (value === true || value === void 0) {
2202
+ return { mount: true, props: { ...defaults } };
2203
+ }
2204
+ return {
2205
+ mount: true,
2206
+ props: {
2207
+ ...defaults,
2208
+ ...value,
2209
+ enabled: value.enabled !== false
2210
+ }
2211
+ };
2212
+ }
2213
+ function resolveContextMenuChannel(value) {
2214
+ if (value === false) {
2215
+ return { mount: false };
2216
+ }
2217
+ if (value === true || value === void 0) {
2218
+ return { mount: true, props: {} };
2219
+ }
2220
+ return { mount: true, props: { ...value } };
2221
+ }
680
2222
  var PanoView = forwardRef2(
681
2223
  function PanoView2({
682
2224
  children,
@@ -684,6 +2226,7 @@ var PanoView = forwardRef2(
684
2226
  minFov = 30,
685
2227
  maxFov = 100,
686
2228
  controls = true,
2229
+ contextMenu = true,
687
2230
  onViewChange,
688
2231
  onPanoramaClick,
689
2232
  onPanoramaDoubleClick,
@@ -693,19 +2236,21 @@ var PanoView = forwardRef2(
693
2236
  "aria-label": ariaLabel = "Panorama viewer",
694
2237
  ...divProps
695
2238
  }, ref) {
696
- const rootRef = useRef4(null);
697
- const controlsRef = useRef4(null);
2239
+ const rootRef = useRef9(null);
2240
+ const controlsRef = useRef9(null);
2241
+ const eventBus = useMemo4(() => createPanoEventBus(), []);
698
2242
  const { controls: hotspotAccessibilityControls, registry } = useHotspotAccessibilityLayer();
699
- const fallbackViewRef = useRef4(DEFAULT_VIEW);
2243
+ const { api: contextMenuOverlayApi, overlay: contextMenuOverlay } = usePanoContextMenuOverlay();
2244
+ const fallbackViewRef = useRef9(DEFAULT_VIEW);
700
2245
  const normalizedMinFov = Math.max(1, Math.min(minFov, maxFov - 1));
701
2246
  const normalizedMaxFov = Math.min(
702
2247
  179,
703
2248
  Math.max(maxFov, normalizedMinFov + 1)
704
2249
  );
705
- const normalizedInitialView = useMemo2(
2250
+ const normalizedInitialView = useMemo4(
706
2251
  () => ({
707
2252
  yaw: initialView?.yaw ?? DEFAULT_VIEW.yaw,
708
- pitch: initialView?.pitch ?? DEFAULT_VIEW.pitch,
2253
+ pitch: clampPanoPitch(initialView?.pitch ?? DEFAULT_VIEW.pitch),
709
2254
  fov: Math.max(
710
2255
  normalizedMinFov,
711
2256
  Math.min(initialView?.fov ?? DEFAULT_VIEW.fov, normalizedMaxFov)
@@ -721,15 +2266,98 @@ var PanoView = forwardRef2(
721
2266
  );
722
2267
  fallbackViewRef.current = normalizedInitialView;
723
2268
  const controlsEnabled = controls !== false;
724
- const controlOptions = useMemo2(
2269
+ const controlOptions = useMemo4(
725
2270
  () => typeof controls === "object" ? controls : {},
726
2271
  [controls]
727
2272
  );
2273
+ const userControlsEnabled = controlsEnabled && controlOptions.enabled !== false;
2274
+ const allModeInvert = controlOptions.invert === true;
2275
+ const topRotateSpeed = controlOptions.rotateSpeed ?? DEFAULT_POINTER_ROTATE_SPEED;
2276
+ const topZoomSpeed = controlOptions.zoomSpeed ?? DEFAULT_MOUSE_ZOOM_SPEED;
2277
+ const mouseChannel = useMemo4(
2278
+ () => resolveChannel(controlOptions.mouse, {
2279
+ enabled: true,
2280
+ rotateSpeed: topRotateSpeed,
2281
+ zoomSpeed: topZoomSpeed,
2282
+ wheel: true,
2283
+ invert: allModeInvert,
2284
+ buttons: ["left"]
2285
+ }),
2286
+ [
2287
+ allModeInvert,
2288
+ controlOptions.mouse,
2289
+ topRotateSpeed,
2290
+ topZoomSpeed
2291
+ ]
2292
+ );
2293
+ const touchChannel = useMemo4(
2294
+ () => resolveChannel(controlOptions.touch, {
2295
+ enabled: true,
2296
+ rotateSpeed: topRotateSpeed,
2297
+ invert: allModeInvert,
2298
+ pinchZoom: true
2299
+ }),
2300
+ [allModeInvert, controlOptions.touch, topRotateSpeed]
2301
+ );
2302
+ const keyboardChannel = useMemo4(
2303
+ () => resolveChannel(controlOptions.keyboard, {
2304
+ enabled: true
2305
+ }),
2306
+ [controlOptions.keyboard]
2307
+ );
2308
+ const toggleFullscreen = useCallback3(async () => {
2309
+ if (typeof document === "undefined") {
2310
+ return;
2311
+ }
2312
+ if (document.fullscreenElement) {
2313
+ await document.exitFullscreen?.();
2314
+ return;
2315
+ }
2316
+ await rootRef.current?.requestFullscreen?.();
2317
+ }, []);
2318
+ const [isViewerFullscreen, setIsViewerFullscreen] = useState3(false);
2319
+ useEffect10(() => {
2320
+ if (typeof document === "undefined") {
2321
+ return;
2322
+ }
2323
+ const syncFullscreen = () => {
2324
+ const fullscreenElement = document.fullscreenElement;
2325
+ const root = rootRef.current;
2326
+ if (!fullscreenElement || !root) {
2327
+ setIsViewerFullscreen(false);
2328
+ return;
2329
+ }
2330
+ setIsViewerFullscreen(
2331
+ fullscreenElement === root || fullscreenElement.contains(root) || root.contains(fullscreenElement)
2332
+ );
2333
+ };
2334
+ syncFullscreen();
2335
+ document.addEventListener("fullscreenchange", syncFullscreen);
2336
+ return () => {
2337
+ document.removeEventListener("fullscreenchange", syncFullscreen);
2338
+ };
2339
+ }, []);
2340
+ const contextMenuActions = useMemo4(
2341
+ () => ({
2342
+ reset: () => {
2343
+ controlsRef.current?.reset();
2344
+ },
2345
+ toggleFullscreen: () => {
2346
+ void toggleFullscreen();
2347
+ },
2348
+ isFullscreen: isViewerFullscreen
2349
+ }),
2350
+ [isViewerFullscreen, toggleFullscreen]
2351
+ );
2352
+ const contextMenuChannel = useMemo4(
2353
+ () => resolveContextMenuChannel(contextMenu),
2354
+ [contextMenu]
2355
+ );
728
2356
  const legacyAutoRotateOptions = controlOptions;
729
- const [legacyAutoRotate, setLegacyAutoRotate] = useState2(
2357
+ const [legacyAutoRotate, setLegacyAutoRotate] = useState3(
730
2358
  legacyAutoRotateOptions.autoRotate ?? false
731
2359
  );
732
- useEffect4(() => {
2360
+ useEffect10(() => {
733
2361
  setLegacyAutoRotate(legacyAutoRotateOptions.autoRotate ?? false);
734
2362
  }, [legacyAutoRotateOptions.autoRotate]);
735
2363
  useImperativeHandle2(
@@ -748,20 +2376,11 @@ var PanoView = forwardRef2(
748
2376
  stopAutoRotate: () => {
749
2377
  setLegacyAutoRotate(false);
750
2378
  },
751
- toggleFullscreen: async () => {
752
- if (typeof document === "undefined") {
753
- return;
754
- }
755
- if (document.fullscreenElement) {
756
- await document.exitFullscreen?.();
757
- return;
758
- }
759
- await rootRef.current?.requestFullscreen?.();
760
- }
2379
+ toggleFullscreen
761
2380
  }),
762
- []
2381
+ [toggleFullscreen]
763
2382
  );
764
- const rootStyle = useMemo2(
2383
+ const rootStyle = useMemo4(
765
2384
  () => ({
766
2385
  overflow: "hidden",
767
2386
  position: "relative",
@@ -769,7 +2388,7 @@ var PanoView = forwardRef2(
769
2388
  }),
770
2389
  [style]
771
2390
  );
772
- return /* @__PURE__ */ jsxs2(
2391
+ return /* @__PURE__ */ jsxs3(
773
2392
  "div",
774
2393
  {
775
2394
  ...divProps,
@@ -777,7 +2396,7 @@ var PanoView = forwardRef2(
777
2396
  "aria-label": ariaLabel,
778
2397
  style: rootStyle,
779
2398
  children: [
780
- /* @__PURE__ */ jsx3(
2399
+ /* @__PURE__ */ jsx4(
781
2400
  Canvas,
782
2401
  {
783
2402
  "aria-label": `${ariaLabel} canvas`,
@@ -797,41 +2416,71 @@ var PanoView = forwardRef2(
797
2416
  },
798
2417
  style: {
799
2418
  ...DEFAULT_CANVAS_STYLE,
800
- touchAction: controlsEnabled ? "none" : "auto"
2419
+ touchAction: userControlsEnabled ? "none" : "auto"
801
2420
  },
802
- tabIndex: controlsEnabled ? 0 : void 0,
803
- children: /* @__PURE__ */ jsx3(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ jsxs2(PanoramaControlsContext.Provider, { value: controlsRef, children: [
804
- /* @__PURE__ */ jsx3(
805
- PanoramaControls,
806
- {
807
- ref: controlsRef,
808
- enabled: controlsEnabled,
809
- initialView: normalizedInitialView,
810
- maxFov: normalizedMaxFov,
811
- minFov: normalizedMinFov,
812
- onViewChange,
813
- options: controlOptions
814
- }
815
- ),
816
- /* @__PURE__ */ jsx3(
817
- AutoRotate,
818
- {
819
- enabled: legacyAutoRotate,
820
- speed: legacyAutoRotateOptions.autoRotateSpeed
821
- }
822
- ),
823
- /* @__PURE__ */ jsx3(
824
- PanoramaEventSurface,
825
- {
826
- onClick: onPanoramaClick,
827
- onDoubleClick: onPanoramaDoubleClick,
828
- onPointerMove: onPanoramaPointerMove
829
- }
830
- ),
831
- children
832
- ] }) })
2421
+ tabIndex: userControlsEnabled ? 0 : void 0,
2422
+ children: /* @__PURE__ */ jsx4(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ jsx4(PanoEventBusContext.Provider, { value: eventBus, children: /* @__PURE__ */ jsx4(PanoramaViewContext.Provider, { value: controlsRef, children: /* @__PURE__ */ jsx4(
2423
+ PanoContextMenuOverlayContext.Provider,
2424
+ {
2425
+ value: contextMenuOverlayApi,
2426
+ children: /* @__PURE__ */ jsxs3(
2427
+ PanoContextMenuActionsContext.Provider,
2428
+ {
2429
+ value: contextMenuActions,
2430
+ children: [
2431
+ /* @__PURE__ */ jsx4(
2432
+ PanoramaViewRuntime,
2433
+ {
2434
+ ref: controlsRef,
2435
+ eventBus,
2436
+ initialView: normalizedInitialView,
2437
+ maxFov: normalizedMaxFov,
2438
+ minFov: normalizedMinFov,
2439
+ options: controlOptions
2440
+ }
2441
+ ),
2442
+ (onViewChange || onPanoramaClick || onPanoramaDoubleClick || onPanoramaPointerMove) && /* @__PURE__ */ jsx4(
2443
+ PanoEvents,
2444
+ {
2445
+ onViewChange,
2446
+ onClick: onPanoramaClick,
2447
+ onDoubleClick: onPanoramaDoubleClick,
2448
+ onPointerMove: onPanoramaPointerMove
2449
+ }
2450
+ ),
2451
+ /* @__PURE__ */ jsx4(
2452
+ AutoRotate,
2453
+ {
2454
+ enabled: legacyAutoRotate,
2455
+ speed: legacyAutoRotateOptions.autoRotateSpeed
2456
+ }
2457
+ ),
2458
+ userControlsEnabled && mouseChannel.mount ? /* @__PURE__ */ jsx4(
2459
+ MouseControls,
2460
+ {
2461
+ ...mouseChannel.props,
2462
+ fovSpeed: controlOptions.fovSpeed
2463
+ }
2464
+ ) : null,
2465
+ userControlsEnabled && touchChannel.mount ? /* @__PURE__ */ jsx4(TouchControls, { ...touchChannel.props }) : null,
2466
+ userControlsEnabled && keyboardChannel.mount ? /* @__PURE__ */ jsx4(
2467
+ KeyboardControls,
2468
+ {
2469
+ ...keyboardChannel.props,
2470
+ fovSpeed: keyboardChannel.props.fovSpeed ?? controlOptions.fovSpeed
2471
+ }
2472
+ ) : null,
2473
+ /* @__PURE__ */ jsx4(PanoramaEventSurface, {}),
2474
+ contextMenuChannel.mount ? /* @__PURE__ */ jsx4(PanoContextMenu, { ...contextMenuChannel.props }) : null,
2475
+ children
2476
+ ]
2477
+ }
2478
+ )
2479
+ }
2480
+ ) }) }) })
833
2481
  }
834
2482
  ),
2483
+ contextMenuOverlay,
835
2484
  hotspotAccessibilityControls
836
2485
  ]
837
2486
  }
@@ -840,8 +2489,8 @@ var PanoView = forwardRef2(
840
2489
  );
841
2490
 
842
2491
  // src/sphere.tsx
843
- import { useThree as useThree2 } from "@react-three/fiber";
844
- import { useEffect as useEffect5, useRef as useRef5, useState as useState3 } from "react";
2492
+ import { useThree as useThree7 } from "@react-three/fiber";
2493
+ import { useEffect as useEffect11, useRef as useRef10, useState as useState4 } from "react";
845
2494
  import {
846
2495
  BackSide as BackSide2,
847
2496
  LinearFilter,
@@ -851,7 +2500,7 @@ import {
851
2500
  SRGBColorSpace,
852
2501
  TextureLoader
853
2502
  } from "three";
854
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2503
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
855
2504
  function Sphere({
856
2505
  src,
857
2506
  yawOffset = 0,
@@ -859,13 +2508,13 @@ function Sphere({
859
2508
  onError,
860
2509
  visible = true
861
2510
  }) {
862
- const gl = useThree2((state) => state.gl);
863
- const [texture, setTexture] = useState3(null);
864
- const onLoadRef = useRef5(onLoad);
865
- const onErrorRef = useRef5(onError);
2511
+ const gl = useThree7((state) => state.gl);
2512
+ const [texture, setTexture] = useState4(null);
2513
+ const onLoadRef = useRef10(onLoad);
2514
+ const onErrorRef = useRef10(onError);
866
2515
  onLoadRef.current = onLoad;
867
2516
  onErrorRef.current = onError;
868
- useEffect5(() => {
2517
+ useEffect11(() => {
869
2518
  let active = true;
870
2519
  const loader = new TextureLoader();
871
2520
  setTexture(null);
@@ -899,7 +2548,7 @@ function Sphere({
899
2548
  active = false;
900
2549
  };
901
2550
  }, [gl, src]);
902
- useEffect5(
2551
+ useEffect11(
903
2552
  () => () => {
904
2553
  texture?.dispose();
905
2554
  },
@@ -908,14 +2557,14 @@ function Sphere({
908
2557
  if (!texture) {
909
2558
  return null;
910
2559
  }
911
- return /* @__PURE__ */ jsxs3(
2560
+ return /* @__PURE__ */ jsxs4(
912
2561
  "mesh",
913
2562
  {
914
2563
  rotation: [0, MathUtils3.degToRad(90 + yawOffset), 0],
915
2564
  visible,
916
2565
  children: [
917
- /* @__PURE__ */ jsx4("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
918
- /* @__PURE__ */ jsx4(
2566
+ /* @__PURE__ */ jsx5("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
2567
+ /* @__PURE__ */ jsx5(
919
2568
  "meshBasicMaterial",
920
2569
  {
921
2570
  map: texture,
@@ -929,15 +2578,15 @@ function Sphere({
929
2578
  }
930
2579
 
931
2580
  // src/panorama-scenes.tsx
932
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
2581
+ import { useFrame as useFrame6, useThree as useThree10 } from "@react-three/fiber";
933
2582
  import {
934
2583
  Fragment,
935
- useCallback as useCallback2,
936
- useContext as useContext4,
937
- useEffect as useEffect8,
938
- useMemo as useMemo5,
939
- useRef as useRef7,
940
- useState as useState5
2584
+ useCallback as useCallback4,
2585
+ useContext as useContext10,
2586
+ useEffect as useEffect14,
2587
+ useMemo as useMemo7,
2588
+ useRef as useRef12,
2589
+ useState as useState6
941
2590
  } from "react";
942
2591
  import {
943
2592
  DoubleSide,
@@ -945,17 +2594,17 @@ import {
945
2594
  LinearFilter as LinearFilter4,
946
2595
  PlaneGeometry as PlaneGeometry2,
947
2596
  ShaderMaterial,
948
- Vector3 as Vector34,
2597
+ Vector3 as Vector35,
949
2598
  WebGLRenderTarget
950
2599
  } from "three";
951
2600
 
952
2601
  // src/tile/texture-manager-context.tsx
953
- import { useThree as useThree3 } from "@react-three/fiber";
2602
+ import { useThree as useThree8 } from "@react-three/fiber";
954
2603
  import {
955
- createContext as createContext3,
956
- useContext as useContext3,
957
- useEffect as useEffect6,
958
- useMemo as useMemo3
2604
+ createContext as createContext5,
2605
+ useContext as useContext9,
2606
+ useEffect as useEffect12,
2607
+ useMemo as useMemo5
959
2608
  } from "react";
960
2609
 
961
2610
  // src/tile/texture-manager.ts
@@ -1185,8 +2834,8 @@ var TileTextureManager = class {
1185
2834
  };
1186
2835
 
1187
2836
  // src/tile/texture-manager-context.tsx
1188
- import { jsx as jsx5 } from "react/jsx-runtime";
1189
- var TileTextureManagerContext = createContext3(
2837
+ import { jsx as jsx6 } from "react/jsx-runtime";
2838
+ var TileTextureManagerContext = createContext5(
1190
2839
  null
1191
2840
  );
1192
2841
  function TileTextureManagerProvider({
@@ -1194,9 +2843,9 @@ function TileTextureManagerProvider({
1194
2843
  maxTextureMemoryMb = 128,
1195
2844
  maxConcurrentLoads = 4
1196
2845
  }) {
1197
- const gl = useThree3((state) => state.gl);
2846
+ const gl = useThree8((state) => state.gl);
1198
2847
  const anisotropy = gl.capabilities.getMaxAnisotropy();
1199
- const manager = useMemo3(
2848
+ const manager = useMemo5(
1200
2849
  () => new TileTextureManager({
1201
2850
  anisotropy,
1202
2851
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1205,24 +2854,24 @@ function TileTextureManagerProvider({
1205
2854
  }),
1206
2855
  [anisotropy, maxConcurrentLoads, maxTextureMemoryMb]
1207
2856
  );
1208
- useEffect6(() => {
2857
+ useEffect12(() => {
1209
2858
  manager.resume();
1210
2859
  return () => manager.dispose();
1211
2860
  }, [manager]);
1212
- return /* @__PURE__ */ jsx5(TileTextureManagerContext.Provider, { value: manager, children });
2861
+ return /* @__PURE__ */ jsx6(TileTextureManagerContext.Provider, { value: manager, children });
1213
2862
  }
1214
2863
  function useSharedTileTextureManager() {
1215
- return useContext3(TileTextureManagerContext);
2864
+ return useContext9(TileTextureManagerContext);
1216
2865
  }
1217
2866
 
1218
2867
  // src/tile/tile.tsx
1219
- import { useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
2868
+ import { useFrame as useFrame5, useThree as useThree9 } from "@react-three/fiber";
1220
2869
  import {
1221
2870
  memo,
1222
- useEffect as useEffect7,
1223
- useMemo as useMemo4,
1224
- useRef as useRef6,
1225
- useState as useState4
2871
+ useEffect as useEffect13,
2872
+ useMemo as useMemo6,
2873
+ useRef as useRef11,
2874
+ useState as useState5
1226
2875
  } from "react";
1227
2876
  import {
1228
2877
  ClampToEdgeWrapping as ClampToEdgeWrapping2,
@@ -1233,7 +2882,7 @@ import {
1233
2882
  PerspectiveCamera as PerspectiveCamera2,
1234
2883
  SRGBColorSpace as SRGBColorSpace3,
1235
2884
  TextureLoader as TextureLoader3,
1236
- Vector3 as Vector33
2885
+ Vector3 as Vector34
1237
2886
  } from "three";
1238
2887
 
1239
2888
  // src/tile/face-layout.ts
@@ -1241,7 +2890,7 @@ import {
1241
2890
  Box3,
1242
2891
  Euler as Euler2,
1243
2892
  Quaternion,
1244
- Vector3 as Vector32
2893
+ Vector3 as Vector33
1245
2894
  } from "three";
1246
2895
  var CUBE_FACES = ["f", "r", "b", "l", "u", "d"];
1247
2896
  var PREVIEW_FACE_ORDER = ["l", "f", "r", "b", "u", "d"];
@@ -1305,12 +2954,12 @@ function getTileBoundingBox(address, radius = CUBE_RADIUS) {
1305
2954
  const halfWidth = local.width / 2;
1306
2955
  const halfHeight = local.height / 2;
1307
2956
  const quaternion = getFaceQuaternion(address.face);
1308
- const position = new Vector32(...transform.position);
2957
+ const position = new Vector33(...transform.position);
1309
2958
  const corners = [
1310
- new Vector32(local.x - halfWidth, local.y - halfHeight, 0),
1311
- new Vector32(local.x + halfWidth, local.y - halfHeight, 0),
1312
- new Vector32(local.x - halfWidth, local.y + halfHeight, 0),
1313
- new Vector32(local.x + halfWidth, local.y + halfHeight, 0)
2959
+ new Vector33(local.x - halfWidth, local.y - halfHeight, 0),
2960
+ new Vector33(local.x + halfWidth, local.y - halfHeight, 0),
2961
+ new Vector33(local.x - halfWidth, local.y + halfHeight, 0),
2962
+ new Vector33(local.x + halfWidth, local.y + halfHeight, 0)
1314
2963
  ];
1315
2964
  for (const corner of corners) {
1316
2965
  corner.applyQuaternion(quaternion).add(position);
@@ -1419,7 +3068,7 @@ function resolveTemplateUrl(template, address) {
1419
3068
  }
1420
3069
 
1421
3070
  // src/tile/tile.tsx
1422
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
3071
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1423
3072
  var MAX_LOD_UPDATE_INTERVAL_MS = 100;
1424
3073
  var FAST_ROTATION_THRESHOLD_RADIANS = Math.PI / 180;
1425
3074
  var FAST_FOV_THRESHOLD_DEGREES = 0.5;
@@ -1428,10 +3077,10 @@ var VISIBILITY_FOV_PADDING_DEGREES = 8;
1428
3077
  var PREVIEW_RADIUS = CUBE_RADIUS + 0.05;
1429
3078
  var SHARED_TILE_GEOMETRY = new PlaneGeometry(1, 1);
1430
3079
  function usePreviewTexture(url, anisotropy, onError) {
1431
- const [texture, setTexture] = useState4(null);
1432
- const onErrorRef = useRef6(onError);
3080
+ const [texture, setTexture] = useState5(null);
3081
+ const onErrorRef = useRef11(onError);
1433
3082
  onErrorRef.current = onError;
1434
- useEffect7(() => {
3083
+ useEffect13(() => {
1435
3084
  if (!url) {
1436
3085
  setTexture(null);
1437
3086
  return;
@@ -1468,7 +3117,7 @@ function usePreviewTexture(url, anisotropy, onError) {
1468
3117
  active = false;
1469
3118
  };
1470
3119
  }, [anisotropy, url]);
1471
- useEffect7(
3120
+ useEffect13(
1472
3121
  () => () => {
1473
3122
  texture?.dispose();
1474
3123
  },
@@ -1481,7 +3130,7 @@ function PreviewFace({
1481
3130
  texture,
1482
3131
  faceOrder
1483
3132
  }) {
1484
- const geometry = useMemo4(() => {
3133
+ const geometry = useMemo6(() => {
1485
3134
  const nextGeometry = new PlaneGeometry(
1486
3135
  PREVIEW_RADIUS * 2,
1487
3136
  PREVIEW_RADIUS * 2
@@ -1499,21 +3148,21 @@ function PreviewFace({
1499
3148
  uv.needsUpdate = true;
1500
3149
  return nextGeometry;
1501
3150
  }, [face, faceOrder]);
1502
- useEffect7(
3151
+ useEffect13(
1503
3152
  () => () => {
1504
3153
  geometry.dispose();
1505
3154
  },
1506
3155
  [geometry]
1507
3156
  );
1508
3157
  const transform = getFaceTransform(face, PREVIEW_RADIUS);
1509
- return /* @__PURE__ */ jsx6(
3158
+ return /* @__PURE__ */ jsx7(
1510
3159
  "mesh",
1511
3160
  {
1512
3161
  geometry,
1513
3162
  position: transform.position,
1514
3163
  renderOrder: -100,
1515
3164
  rotation: transform.rotation,
1516
- children: /* @__PURE__ */ jsx6(
3165
+ children: /* @__PURE__ */ jsx7(
1517
3166
  "meshBasicMaterial",
1518
3167
  {
1519
3168
  depthTest: false,
@@ -1529,7 +3178,7 @@ function PreviewCube({
1529
3178
  texture,
1530
3179
  faceOrder
1531
3180
  }) {
1532
- return CUBE_FACES.map((face) => /* @__PURE__ */ jsx6(
3181
+ return CUBE_FACES.map((face) => /* @__PURE__ */ jsx7(
1533
3182
  PreviewFace,
1534
3183
  {
1535
3184
  face,
@@ -1540,14 +3189,14 @@ function PreviewCube({
1540
3189
  ));
1541
3190
  }
1542
3191
  function useManagedTexture(manager, key, url, address) {
1543
- const [managedState, setManagedState] = useState4(() => ({
3192
+ const [managedState, setManagedState] = useState5(() => ({
1544
3193
  manager,
1545
3194
  key,
1546
3195
  url,
1547
3196
  snapshot: manager.getSnapshot(key)
1548
3197
  }));
1549
3198
  const snapshot = managedState.manager === manager && managedState.key === key && managedState.url === url ? managedState.snapshot : manager.getSnapshot(key);
1550
- useEffect7(
3199
+ useEffect13(
1551
3200
  () => manager.acquire(key, url, address, (nextSnapshot) => {
1552
3201
  setManagedState({
1553
3202
  manager,
@@ -1574,14 +3223,14 @@ var TileMesh = memo(function TileMesh2({
1574
3223
  if (!snapshot.texture) {
1575
3224
  return null;
1576
3225
  }
1577
- return /* @__PURE__ */ jsx6("group", { position: transform.position, rotation: transform.rotation, children: /* @__PURE__ */ jsx6(
3226
+ return /* @__PURE__ */ jsx7("group", { position: transform.position, rotation: transform.rotation, children: /* @__PURE__ */ jsx7(
1578
3227
  "mesh",
1579
3228
  {
1580
3229
  geometry: SHARED_TILE_GEOMETRY,
1581
3230
  position: [layout.x, layout.y, 0],
1582
3231
  renderOrder,
1583
3232
  scale: [layout.width, layout.height, 1],
1584
- children: /* @__PURE__ */ jsx6(
3233
+ children: /* @__PURE__ */ jsx7(
1585
3234
  "meshBasicMaterial",
1586
3235
  {
1587
3236
  depthTest: false,
@@ -1598,7 +3247,7 @@ var TileLayerMeshes = memo(function TileLayerMeshes2({
1598
3247
  manager,
1599
3248
  resolveUrl
1600
3249
  }) {
1601
- return layer.addresses.map((address) => /* @__PURE__ */ jsx6(
3250
+ return layer.addresses.map((address) => /* @__PURE__ */ jsx7(
1602
3251
  TileMesh,
1603
3252
  {
1604
3253
  address,
@@ -1698,8 +3347,8 @@ function parentAddresses(child, parentLevel, parentFaceSize, tileSize) {
1698
3347
  return addresses;
1699
3348
  }
1700
3349
  function centerFacingAddress(candidates, camera) {
1701
- const direction = camera.getWorldDirection(new Vector33());
1702
- const center = new Vector33();
3350
+ const direction = camera.getWorldDirection(new Vector34());
3351
+ const center = new Vector34();
1703
3352
  let bestCandidate = candidates[0];
1704
3353
  let bestAlignment = -Infinity;
1705
3354
  for (const candidate of candidates) {
@@ -1766,11 +3415,11 @@ function Tile({
1766
3415
  onTileError,
1767
3416
  onLevelChange
1768
3417
  }) {
1769
- const { camera, gl, size } = useThree4();
1770
- const multiresConfig = useMemo4(() => parseMultires(multires), [multires]);
3418
+ const { camera, gl, size } = useThree9();
3419
+ const multiresConfig = useMemo6(() => parseMultires(multires), [multires]);
1771
3420
  const { levels, tileSize } = multiresConfig;
1772
3421
  const multiresToken = `${tileSize}:${levels.join(",")}`;
1773
- const candidatesByLevel = useMemo4(
3422
+ const candidatesByLevel = useMemo6(
1774
3423
  () => buildTileCandidates(levels, tileSize),
1775
3424
  [levels, tileSize]
1776
3425
  );
@@ -1782,13 +3431,13 @@ function Tile({
1782
3431
  onPreviewError
1783
3432
  );
1784
3433
  const sharedManager = useSharedTileTextureManager();
1785
- const progressRef = useRef6(onLoadProgress);
1786
- const errorRef = useRef6(onTileError);
1787
- const levelChangeRef = useRef6(onLevelChange);
3434
+ const progressRef = useRef11(onLoadProgress);
3435
+ const errorRef = useRef11(onTileError);
3436
+ const levelChangeRef = useRef11(onLevelChange);
1788
3437
  progressRef.current = onLoadProgress;
1789
3438
  errorRef.current = onTileError;
1790
3439
  levelChangeRef.current = onLevelChange;
1791
- const ownedManager = useMemo4(
3440
+ const ownedManager = useMemo6(
1792
3441
  () => new TileTextureManager({
1793
3442
  anisotropy,
1794
3443
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1809,7 +3458,7 @@ function Tile({
1809
3458
  ]
1810
3459
  );
1811
3460
  const manager = sharedManager ?? ownedManager;
1812
- useEffect7(() => {
3461
+ useEffect13(() => {
1813
3462
  manager.resume();
1814
3463
  return () => {
1815
3464
  if (manager === ownedManager) {
@@ -1819,11 +3468,11 @@ function Tile({
1819
3468
  queueMicrotask(() => manager.releaseUnused());
1820
3469
  };
1821
3470
  }, [manager, ownedManager]);
1822
- const readyRef = useRef6(false);
1823
- useEffect7(() => {
3471
+ const readyRef = useRef11(false);
3472
+ useEffect13(() => {
1824
3473
  readyRef.current = false;
1825
3474
  }, [effectivePreviewUrl]);
1826
- useEffect7(() => {
3475
+ useEffect13(() => {
1827
3476
  if (!previewTexture || readyRef.current) {
1828
3477
  return;
1829
3478
  }
@@ -1831,31 +3480,31 @@ function Tile({
1831
3480
  onReady?.();
1832
3481
  }, [onReady, previewTexture]);
1833
3482
  const template = urlTemplate ?? buildDefaultTileUrlTemplate();
1834
- const resolveUrl = useMemo4(
3483
+ const resolveUrl = useMemo6(
1835
3484
  () => (address) => {
1836
3485
  const relativePath = resolveTileUrl ? resolveTileUrl(address) : resolveTemplateUrl(template, address);
1837
3486
  return resolveRelativeTileUrl(baseUrl, relativePath);
1838
3487
  },
1839
3488
  [baseUrl, resolveTileUrl, template]
1840
3489
  );
1841
- const [layers, setLayers] = useState4([]);
1842
- const layerTokenRef = useRef6("");
1843
- const currentLevelRef = useRef6(1);
1844
- const lastUpdateAtRef = useRef6(-Infinity);
1845
- const visibilityDirtyRef = useRef6(true);
1846
- const lastVisibilityQuaternionRef = useRef6(camera.quaternion.clone());
1847
- const lastVisibilityFovRef = useRef6(Number.NaN);
1848
- const lastViewportRef = useRef6({ width: 0, height: 0, dpr: 0 });
1849
- const frustumRef = useRef6(new Frustum());
1850
- const projectionMatrixRef = useRef6(new Matrix4());
1851
- const visibilityCameraRef = useRef6(new PerspectiveCamera2());
1852
- useEffect7(() => {
3490
+ const [layers, setLayers] = useState5([]);
3491
+ const layerTokenRef = useRef11("");
3492
+ const currentLevelRef = useRef11(1);
3493
+ const lastUpdateAtRef = useRef11(-Infinity);
3494
+ const visibilityDirtyRef = useRef11(true);
3495
+ const lastVisibilityQuaternionRef = useRef11(camera.quaternion.clone());
3496
+ const lastVisibilityFovRef = useRef11(Number.NaN);
3497
+ const lastViewportRef = useRef11({ width: 0, height: 0, dpr: 0 });
3498
+ const frustumRef = useRef11(new Frustum());
3499
+ const projectionMatrixRef = useRef11(new Matrix4());
3500
+ const visibilityCameraRef = useRef11(new PerspectiveCamera2());
3501
+ useEffect13(() => {
1853
3502
  currentLevelRef.current = 1;
1854
3503
  layerTokenRef.current = "";
1855
3504
  visibilityDirtyRef.current = true;
1856
3505
  setLayers([]);
1857
3506
  }, [manager, multiresToken]);
1858
- useEffect7(() => {
3507
+ useEffect13(() => {
1859
3508
  if (loadMode !== "preview") {
1860
3509
  return;
1861
3510
  }
@@ -1869,7 +3518,7 @@ function Tile({
1869
3518
  setLayers(nextLayers);
1870
3519
  }
1871
3520
  };
1872
- useFrame3(() => {
3521
+ useFrame5(() => {
1873
3522
  if (loadMode === "preview") {
1874
3523
  return;
1875
3524
  }
@@ -1935,15 +3584,15 @@ function Tile({
1935
3584
  };
1936
3585
  commitLayers(nextLayers);
1937
3586
  });
1938
- return /* @__PURE__ */ jsxs4("group", { visible, children: [
1939
- previewTexture ? /* @__PURE__ */ jsx6(
3587
+ return /* @__PURE__ */ jsxs5("group", { visible, children: [
3588
+ previewTexture ? /* @__PURE__ */ jsx7(
1940
3589
  PreviewCube,
1941
3590
  {
1942
3591
  texture: previewTexture,
1943
3592
  faceOrder: previewFaceOrder
1944
3593
  }
1945
3594
  ) : null,
1946
- layers.map((layer) => /* @__PURE__ */ jsx6(
3595
+ layers.map((layer) => /* @__PURE__ */ jsx7(
1947
3596
  TileLayerMeshes,
1948
3597
  {
1949
3598
  layer,
@@ -1956,7 +3605,7 @@ function Tile({
1956
3605
  }
1957
3606
 
1958
3607
  // src/panorama-scenes.tsx
1959
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
3608
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1960
3609
  var TRANSITION_DEFAULTS = {
1961
3610
  none: { duration: 0, krpanoBlend: "NOBLEND" },
1962
3611
  crossfade: { duration: 1, krpanoBlend: "BLEND(1.0, easeInCubic)" },
@@ -2006,7 +3655,7 @@ function SceneSource({
2006
3655
  return null;
2007
3656
  }
2008
3657
  if (scene.type === "sphere") {
2009
- return /* @__PURE__ */ jsx7(
3658
+ return /* @__PURE__ */ jsx8(
2010
3659
  Sphere,
2011
3660
  {
2012
3661
  src: scene.src,
@@ -2018,7 +3667,7 @@ function SceneSource({
2018
3667
  );
2019
3668
  }
2020
3669
  const { id: _id, type: _type, ...tileProps } = scene;
2021
- return /* @__PURE__ */ jsx7(
3670
+ return /* @__PURE__ */ jsx8(
2022
3671
  Tile,
2023
3672
  {
2024
3673
  ...tileProps,
@@ -2034,17 +3683,17 @@ function SnapshotCapture({
2034
3683
  maxPixels,
2035
3684
  onCaptured
2036
3685
  }) {
2037
- const { camera, gl, scene, size } = useThree5();
2038
- const capturedRequestRef = useRef7(0);
2039
- const renderTargetRef = useRef7(null);
2040
- useEffect8(
3686
+ const { camera, gl, scene, size } = useThree10();
3687
+ const capturedRequestRef = useRef12(0);
3688
+ const renderTargetRef = useRef12(null);
3689
+ useEffect14(
2041
3690
  () => () => {
2042
3691
  renderTargetRef.current?.dispose();
2043
3692
  renderTargetRef.current = null;
2044
3693
  },
2045
3694
  []
2046
3695
  );
2047
- useFrame4(() => {
3696
+ useFrame6(() => {
2048
3697
  if (requestId === 0 || requestId === capturedRequestRef.current) {
2049
3698
  return;
2050
3699
  }
@@ -2090,12 +3739,12 @@ function SnapshotOverlay({
2090
3739
  runId,
2091
3740
  onFinish
2092
3741
  }) {
2093
- const { camera, size } = useThree5();
2094
- const meshRef = useRef7(null);
2095
- const startRef = useRef7(null);
2096
- const finishedRef = useRef7(false);
2097
- const directionRef = useRef7(new Vector34());
2098
- const material = useMemo5(
3742
+ const { camera, size } = useThree10();
3743
+ const meshRef = useRef12(null);
3744
+ const startRef = useRef12(null);
3745
+ const finishedRef = useRef12(false);
3746
+ const directionRef = useRef12(new Vector35());
3747
+ const material = useMemo7(
2099
3748
  () => new ShaderMaterial({
2100
3749
  transparent: true,
2101
3750
  depthTest: false,
@@ -2174,13 +3823,13 @@ function SnapshotOverlay({
2174
3823
  }),
2175
3824
  [snapshot.texture, transition.preset]
2176
3825
  );
2177
- useEffect8(() => () => material.dispose(), [material]);
2178
- useEffect8(() => {
3826
+ useEffect14(() => () => material.dispose(), [material]);
3827
+ useEffect14(() => {
2179
3828
  startRef.current = null;
2180
3829
  finishedRef.current = false;
2181
3830
  material.uniforms.progress.value = 0;
2182
3831
  }, [material, runId]);
2183
- useFrame4((state) => {
3832
+ useFrame6((state) => {
2184
3833
  const mesh = meshRef.current;
2185
3834
  if (!mesh || !camera.isPerspectiveCamera) {
2186
3835
  return;
@@ -2205,9 +3854,9 @@ function SnapshotOverlay({
2205
3854
  onFinish();
2206
3855
  }
2207
3856
  });
2208
- return /* @__PURE__ */ jsxs5("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
2209
- /* @__PURE__ */ jsx7("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
2210
- /* @__PURE__ */ jsx7("primitive", { attach: "material", object: material })
3857
+ return /* @__PURE__ */ jsxs6("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
3858
+ /* @__PURE__ */ jsx8("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
3859
+ /* @__PURE__ */ jsx8("primitive", { attach: "material", object: material })
2211
3860
  ] });
2212
3861
  }
2213
3862
  var FULLSCREEN_PLANE = new PlaneGeometry2(1, 1);
@@ -2220,47 +3869,47 @@ function PanoramaScenesController({
2220
3869
  onTransitionError,
2221
3870
  snapshotMaxPixels = 3686400
2222
3871
  }) {
2223
- const controlsRef = useContext4(PanoramaControlsContext);
3872
+ const controlsRef = useContext10(PanoramaViewContext);
2224
3873
  const manager = useSharedTileTextureManager();
2225
- const transition = useMemo5(
3874
+ const transition = useMemo7(
2226
3875
  () => resolveTransition(transitionInput),
2227
3876
  [transitionInput]
2228
3877
  );
2229
- const sceneMap = useMemo5(
3878
+ const sceneMap = useMemo7(
2230
3879
  () => new Map(scenes.map((scene) => [scene.id, scene])),
2231
3880
  [scenes]
2232
3881
  );
2233
3882
  const initialScene = sceneMap.get(activeSceneId) ?? null;
2234
- const [slots, setSlots] = useState5([
3883
+ const [slots, setSlots] = useState6([
2235
3884
  initialScene,
2236
3885
  null
2237
3886
  ]);
2238
- const [activeSlot, setActiveSlot] = useState5(0);
2239
- const [liveSlot, setLiveSlot] = useState5(0);
2240
- const [pendingSlot, setPendingSlot] = useState5(null);
2241
- const [phase, setPhase] = useState5(initialScene ? "idle" : "preloading");
2242
- const [captureRequestId, setCaptureRequestId] = useState5(0);
2243
- const [transitionRunId, setTransitionRunId] = useState5(0);
2244
- const [snapshot, setSnapshot] = useState5(null);
2245
- const [recoveryKey, setRecoveryKey] = useState5(0);
2246
- const requestedSceneIdRef = useRef7(activeSceneId);
2247
- const captureReasonRef = useRef7("start");
2248
- const interactionReleaseRef = useRef7(null);
2249
- const slotsRef = useRef7(slots);
2250
- const phaseRef = useRef7(phase);
2251
- const liveSlotRef = useRef7(liveSlot);
2252
- const transitionFromRef = useRef7(initialScene);
3887
+ const [activeSlot, setActiveSlot] = useState6(0);
3888
+ const [liveSlot, setLiveSlot] = useState6(0);
3889
+ const [pendingSlot, setPendingSlot] = useState6(null);
3890
+ const [phase, setPhase] = useState6(initialScene ? "idle" : "preloading");
3891
+ const [captureRequestId, setCaptureRequestId] = useState6(0);
3892
+ const [transitionRunId, setTransitionRunId] = useState6(0);
3893
+ const [snapshot, setSnapshot] = useState6(null);
3894
+ const [recoveryKey, setRecoveryKey] = useState6(0);
3895
+ const requestedSceneIdRef = useRef12(activeSceneId);
3896
+ const captureReasonRef = useRef12("start");
3897
+ const interactionReleaseRef = useRef12(null);
3898
+ const slotsRef = useRef12(slots);
3899
+ const phaseRef = useRef12(phase);
3900
+ const liveSlotRef = useRef12(liveSlot);
3901
+ const transitionFromRef = useRef12(initialScene);
2253
3902
  slotsRef.current = slots;
2254
3903
  phaseRef.current = phase;
2255
3904
  liveSlotRef.current = liveSlot;
2256
- const releaseInteraction = useCallback2(() => {
3905
+ const releaseInteraction = useCallback4(() => {
2257
3906
  interactionReleaseRef.current?.();
2258
3907
  interactionReleaseRef.current = null;
2259
3908
  }, []);
2260
- const reportError = useCallback2((sceneId, error) => {
3909
+ const reportError = useCallback4((sceneId, error) => {
2261
3910
  onTransitionError?.({ sceneId, error });
2262
3911
  }, [onTransitionError]);
2263
- const startPreload = useCallback2((scene) => {
3912
+ const startPreload = useCallback4((scene) => {
2264
3913
  const nextSlot = 1 - liveSlotRef.current;
2265
3914
  requestedSceneIdRef.current = scene.id;
2266
3915
  setSlots((current) => {
@@ -2271,7 +3920,7 @@ function PanoramaScenesController({
2271
3920
  setPendingSlot(nextSlot);
2272
3921
  setPhase("preloading");
2273
3922
  }, []);
2274
- useEffect8(() => {
3923
+ useEffect14(() => {
2275
3924
  const requested = sceneMap.get(activeSceneId);
2276
3925
  if (!requested) {
2277
3926
  reportError(activeSceneId, new Error(`Unknown panorama scene: ${activeSceneId}`));
@@ -2295,7 +3944,7 @@ function PanoramaScenesController({
2295
3944
  }
2296
3945
  startPreload(requested);
2297
3946
  }, [activeSceneId, pendingSlot, reportError, sceneMap, startPreload]);
2298
- const handleReady = useCallback2((slot) => {
3947
+ const handleReady = useCallback4((slot) => {
2299
3948
  if (slot !== pendingSlot || phaseRef.current !== "preloading") {
2300
3949
  return;
2301
3950
  }
@@ -2326,7 +3975,7 @@ function PanoramaScenesController({
2326
3975
  setPhase("capturing");
2327
3976
  setCaptureRequestId((current) => current + 1);
2328
3977
  }, [controlsRef, onTransitionEnd, pendingSlot, transition.preset]);
2329
- const handleCaptured = useCallback2((nextSnapshot) => {
3978
+ const handleCaptured = useCallback4((nextSnapshot) => {
2330
3979
  setSnapshot((current) => {
2331
3980
  current?.dispose();
2332
3981
  return nextSnapshot;
@@ -2348,7 +3997,7 @@ function PanoramaScenesController({
2348
3997
  setPhase("transitioning");
2349
3998
  setTransitionRunId((current) => current + 1);
2350
3999
  }, [manager, pendingSlot, sceneMap, startPreload]);
2351
- const finishTransition = useCallback2(() => {
4000
+ const finishTransition = useCallback4(() => {
2352
4001
  const nextSlot = pendingSlot;
2353
4002
  const previousScene = transitionFromRef.current;
2354
4003
  const nextScene = nextSlot === null ? null : slotsRef.current[nextSlot];
@@ -2373,12 +4022,12 @@ function PanoramaScenesController({
2373
4022
  });
2374
4023
  }
2375
4024
  }, [onTransitionEnd, pendingSlot, releaseInteraction, snapshot, transition.preset]);
2376
- useEffect8(() => () => {
4025
+ useEffect14(() => () => {
2377
4026
  releaseInteraction();
2378
4027
  snapshot?.dispose();
2379
4028
  }, [releaseInteraction, snapshot]);
2380
- const { gl } = useThree5();
2381
- useEffect8(() => {
4029
+ const { gl } = useThree10();
4030
+ useEffect14(() => {
2382
4031
  const canvas = gl.domElement;
2383
4032
  const onContextLost = (event) => {
2384
4033
  event.preventDefault();
@@ -2402,8 +4051,8 @@ function PanoramaScenesController({
2402
4051
  }, [gl, manager, releaseInteraction, snapshot]);
2403
4052
  const renderedScene = slots[liveSlot];
2404
4053
  const snapshotVisible = snapshot !== null && (phase === "transitioning" || phase === "recapturing" || phase === "preloading");
2405
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
2406
- /* @__PURE__ */ jsx7(
4054
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
4055
+ /* @__PURE__ */ jsx8(
2407
4056
  SnapshotCapture,
2408
4057
  {
2409
4058
  requestId: captureRequestId,
@@ -2415,7 +4064,7 @@ function PanoramaScenesController({
2415
4064
  const source = slots[slot];
2416
4065
  const isLive = slot === liveSlot;
2417
4066
  const isPending = slot === pendingSlot;
2418
- return /* @__PURE__ */ jsx7(
4067
+ return /* @__PURE__ */ jsx8(
2419
4068
  SceneSource,
2420
4069
  {
2421
4070
  scene: source,
@@ -2431,7 +4080,7 @@ function PanoramaScenesController({
2431
4080
  `${slot}:${source?.id ?? "empty"}:${recoveryKey}`
2432
4081
  );
2433
4082
  }),
2434
- snapshotVisible ? /* @__PURE__ */ jsx7(
4083
+ snapshotVisible ? /* @__PURE__ */ jsx8(
2435
4084
  SnapshotOverlay,
2436
4085
  {
2437
4086
  snapshot,
@@ -2449,19 +4098,19 @@ function PanoramaScenes({
2449
4098
  maxConcurrentTileLoads,
2450
4099
  ...props
2451
4100
  }) {
2452
- return /* @__PURE__ */ jsx7(
4101
+ return /* @__PURE__ */ jsx8(
2453
4102
  TileTextureManagerProvider,
2454
4103
  {
2455
4104
  maxTextureMemoryMb,
2456
4105
  maxConcurrentLoads: maxConcurrentTileLoads,
2457
- children: /* @__PURE__ */ jsx7(PanoramaScenesController, { ...props })
4106
+ children: /* @__PURE__ */ jsx8(PanoramaScenesController, { ...props })
2458
4107
  }
2459
4108
  );
2460
4109
  }
2461
4110
 
2462
4111
  // src/hotspot/image-hotspot.tsx
2463
- import { useThree as useThree6 } from "@react-three/fiber";
2464
- import { useEffect as useEffect10, useRef as useRef9, useState as useState6 } from "react";
4112
+ import { useThree as useThree11 } from "@react-three/fiber";
4113
+ import { useEffect as useEffect16, useRef as useRef14, useState as useState7 } from "react";
2465
4114
  import {
2466
4115
  ClampToEdgeWrapping as ClampToEdgeWrapping3,
2467
4116
  DoubleSide as DoubleSide2,
@@ -2472,12 +4121,12 @@ import {
2472
4121
  } from "three";
2473
4122
 
2474
4123
  // src/hotspot/hotspot-anchor.tsx
2475
- import { useFrame as useFrame5 } from "@react-three/fiber";
4124
+ import { useFrame as useFrame7 } from "@react-three/fiber";
2476
4125
  import {
2477
- useContext as useContext5,
2478
- useEffect as useEffect9,
2479
- useMemo as useMemo6,
2480
- useRef as useRef8
4126
+ useContext as useContext11,
4127
+ useEffect as useEffect15,
4128
+ useMemo as useMemo8,
4129
+ useRef as useRef13
2481
4130
  } from "react";
2482
4131
  import {
2483
4132
  EdgesGeometry,
@@ -2486,9 +4135,9 @@ import {
2486
4135
  PlaneGeometry as PlaneGeometry3,
2487
4136
  PerspectiveCamera as PerspectiveCamera4,
2488
4137
  Quaternion as Quaternion2,
2489
- Vector3 as Vector35
4138
+ Vector3 as Vector36
2490
4139
  } from "three";
2491
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4140
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2492
4141
  var DEFAULT_FLOATING_DISTANCE = 10;
2493
4142
  var PANORAMA_RADIUS = DEFAULT_PANORAMA_RADIUS;
2494
4143
  var PANORAMA_SURFACE_INSET = 4;
@@ -2496,7 +4145,7 @@ var DEFAULT_REFERENCE_FOV = 75;
2496
4145
  var MIN_ANGULAR_SIZE = 0.01;
2497
4146
  var MAX_ANGULAR_SIZE = 179;
2498
4147
  var DRAG_EPSILON_DEGREES = 1e-3;
2499
- var LOCAL_FORWARD = new Vector35(0, 0, 1);
4148
+ var LOCAL_FORWARD = new Vector36(0, 0, 1);
2500
4149
  var HOTSPOT_MODE_RENDERING = {
2501
4150
  surface: { orientation: "surface", placement: "surface" },
2502
4151
  billboard: { orientation: "billboard", placement: "floating" }
@@ -2505,6 +4154,9 @@ function angularSizeToWorldSize(size, radius) {
2505
4154
  const safeSize = Number.isFinite(size) ? MathUtils4.clamp(Math.abs(size), MIN_ANGULAR_SIZE, MAX_ANGULAR_SIZE) : MIN_ANGULAR_SIZE;
2506
4155
  return 2 * radius * Math.tan(MathUtils4.degToRad(safeSize) / 2);
2507
4156
  }
4157
+ function resolveScale(scale) {
4158
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
4159
+ }
2508
4160
  function resolveDistance(placement, distance, width, height) {
2509
4161
  const maximumDistance = maximumHotspotDistance(width, height);
2510
4162
  if (placement === "surface") {
@@ -2542,7 +4194,7 @@ function createSurfaceQuaternion(position) {
2542
4194
  const inward = outward.clone().negate();
2543
4195
  const pitch = MathUtils4.degToRad(position.pitch);
2544
4196
  const yaw = MathUtils4.degToRad(position.yaw);
2545
- const localUp = new Vector35(
4197
+ const localUp = new Vector36(
2546
4198
  -Math.sin(pitch) * Math.sin(yaw),
2547
4199
  Math.cos(pitch),
2548
4200
  Math.sin(pitch) * Math.cos(yaw)
@@ -2569,6 +4221,7 @@ function HotspotAnchor({
2569
4221
  position,
2570
4222
  width,
2571
4223
  height,
4224
+ scale = 1,
2572
4225
  useAngularScale = true,
2573
4226
  focusContent,
2574
4227
  mode = "billboard",
@@ -2590,25 +4243,33 @@ function HotspotAnchor({
2590
4243
  onPositionChange,
2591
4244
  onDragEnd
2592
4245
  }) {
2593
- const controlsRef = useContext5(PanoramaControlsContext);
2594
- const groupRef = useRef8(null);
2595
- const dragStateRef = useRef8(null);
2596
- const suppressNextClickRef = useRef8(false);
4246
+ const controlsRef = useContext11(PanoramaViewContext);
4247
+ const groupRef = useRef13(null);
4248
+ const dragStateRef = useRef13(null);
4249
+ const suppressNextClickRef = useRef13(false);
2597
4250
  const normalizedPosition = normalizePanoPosition(position);
2598
4251
  const rendering = internalOrientation && internalPlacement ? { orientation: internalOrientation, placement: internalPlacement } : HOTSPOT_MODE_RENDERING[mode];
2599
4252
  const { orientation: orientation2, placement } = rendering;
2600
- const resolvedDistance = resolveDistance(placement, distance, width, height);
2601
- const worldPosition = useMemo6(
4253
+ const resolvedScale = resolveScale(scale);
4254
+ const scaledWidth = width * resolvedScale;
4255
+ const scaledHeight = height * resolvedScale;
4256
+ const resolvedDistance = resolveDistance(
4257
+ placement,
4258
+ distance,
4259
+ scaledWidth,
4260
+ scaledHeight
4261
+ );
4262
+ const worldPosition = useMemo8(
2602
4263
  () => panoPositionToVector3(normalizedPosition, resolvedDistance),
2603
4264
  [normalizedPosition.pitch, normalizedPosition.yaw, resolvedDistance]
2604
4265
  );
2605
- const surfaceQuaternion3 = useMemo6(
4266
+ const surfaceQuaternion3 = useMemo8(
2606
4267
  () => createSurfaceQuaternion(normalizedPosition),
2607
4268
  [normalizedPosition.pitch, normalizedPosition.yaw]
2608
4269
  );
2609
- const worldWidth = angularSizeToWorldSize(width, resolvedDistance);
2610
- const worldHeight = angularSizeToWorldSize(height, resolvedDistance);
2611
- const focusGeometry = useMemo6(() => {
4270
+ const worldWidth = angularSizeToWorldSize(scaledWidth, resolvedDistance);
4271
+ const worldHeight = angularSizeToWorldSize(scaledHeight, resolvedDistance);
4272
+ const focusGeometry = useMemo8(() => {
2612
4273
  const planeGeometry = new PlaneGeometry3(1, 1);
2613
4274
  const geometry = new EdgesGeometry(planeGeometry);
2614
4275
  planeGeometry.dispose();
@@ -2626,15 +4287,15 @@ function HotspotAnchor({
2626
4287
  });
2627
4288
  } : void 0
2628
4289
  });
2629
- useEffect9(
4290
+ useEffect15(
2630
4291
  () => () => {
2631
4292
  dragStateRef.current?.releaseInteractionLock();
2632
4293
  dragStateRef.current = null;
2633
4294
  },
2634
4295
  []
2635
4296
  );
2636
- useEffect9(() => () => focusGeometry.dispose(), [focusGeometry]);
2637
- useFrame5(({ camera }) => {
4297
+ useEffect15(() => () => focusGeometry.dispose(), [focusGeometry]);
4298
+ useFrame7(({ camera }) => {
2638
4299
  if (!groupRef.current) {
2639
4300
  return;
2640
4301
  }
@@ -2645,7 +4306,7 @@ function HotspotAnchor({
2645
4306
  }
2646
4307
  groupRef.current.rotateOnAxis(
2647
4308
  LOCAL_FORWARD,
2648
- MathUtils4.degToRad(rotation)
4309
+ MathUtils4.degToRad(-rotation)
2649
4310
  );
2650
4311
  const scaleFactor = scaleMode === "fixed" && camera instanceof PerspectiveCamera4 ? fixedScaleFactor(camera.fov, referenceFov) : 1;
2651
4312
  groupRef.current.scale.set(
@@ -2680,7 +4341,7 @@ function HotspotAnchor({
2680
4341
  dragState.releaseInteractionLock();
2681
4342
  dragStateRef.current = null;
2682
4343
  };
2683
- return /* @__PURE__ */ jsxs6(
4344
+ return /* @__PURE__ */ jsxs7(
2684
4345
  "group",
2685
4346
  {
2686
4347
  ref: groupRef,
@@ -2750,14 +4411,14 @@ function HotspotAnchor({
2750
4411
  } : void 0,
2751
4412
  onPointerUp: interactive ? (event) => releasePointer(event, true) : void 0,
2752
4413
  children: [
2753
- focused ? focusContent ?? /* @__PURE__ */ jsx8(
4414
+ focused ? focusContent ?? /* @__PURE__ */ jsx9(
2754
4415
  "lineSegments",
2755
4416
  {
2756
4417
  geometry: focusGeometry,
2757
4418
  position: [0, 0, 0.01],
2758
4419
  renderOrder: renderOrder + 1,
2759
4420
  scale: [1.12, 1.12, 1],
2760
- children: /* @__PURE__ */ jsx8("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
4421
+ children: /* @__PURE__ */ jsx9("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
2761
4422
  }
2762
4423
  ) : null,
2763
4424
  children
@@ -2767,7 +4428,7 @@ function HotspotAnchor({
2767
4428
  }
2768
4429
 
2769
4430
  // src/hotspot/image-hotspot.tsx
2770
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
4431
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2771
4432
  function clampOpacity(opacity) {
2772
4433
  if (!Number.isFinite(opacity)) {
2773
4434
  return 1;
@@ -2775,13 +4436,13 @@ function clampOpacity(opacity) {
2775
4436
  return Math.max(0, Math.min(opacity, 1));
2776
4437
  }
2777
4438
  function useHotspotImageTexture(src, onLoad, onError) {
2778
- const gl = useThree6((state) => state.gl);
2779
- const [texture, setTexture] = useState6(null);
2780
- const onLoadRef = useRef9(onLoad);
2781
- const onErrorRef = useRef9(onError);
4439
+ const gl = useThree11((state) => state.gl);
4440
+ const [texture, setTexture] = useState7(null);
4441
+ const onLoadRef = useRef14(onLoad);
4442
+ const onErrorRef = useRef14(onError);
2782
4443
  onLoadRef.current = onLoad;
2783
4444
  onErrorRef.current = onError;
2784
- useEffect10(() => {
4445
+ useEffect16(() => {
2785
4446
  let active = true;
2786
4447
  setTexture(null);
2787
4448
  if (!src) {
@@ -2819,7 +4480,7 @@ function useHotspotImageTexture(src, onLoad, onError) {
2819
4480
  active = false;
2820
4481
  };
2821
4482
  }, [gl, src]);
2822
- useEffect10(
4483
+ useEffect16(
2823
4484
  () => () => {
2824
4485
  texture?.dispose();
2825
4486
  },
@@ -2837,9 +4498,9 @@ function ImageHotspot({
2837
4498
  ...anchorProps
2838
4499
  }) {
2839
4500
  const texture = useHotspotImageTexture(src, onLoad, onError);
2840
- return /* @__PURE__ */ jsx9(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs7("mesh", { children: [
2841
- /* @__PURE__ */ jsx9("planeGeometry", { args: [1, 1] }),
2842
- /* @__PURE__ */ jsx9(
4501
+ return /* @__PURE__ */ jsx10(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs8("mesh", { children: [
4502
+ /* @__PURE__ */ jsx10("planeGeometry", { args: [1, 1] }),
4503
+ /* @__PURE__ */ jsx10(
2843
4504
  "meshBasicMaterial",
2844
4505
  {
2845
4506
  map: texture,
@@ -2853,8 +4514,8 @@ function ImageHotspot({
2853
4514
  }
2854
4515
 
2855
4516
  // src/hotspot/polygon-hotspot.tsx
2856
- import { useThree as useThree7 } from "@react-three/fiber";
2857
- import { useEffect as useEffect11, useMemo as useMemo7, useRef as useRef10 } from "react";
4517
+ import { useThree as useThree12 } from "@react-three/fiber";
4518
+ import { useEffect as useEffect17, useMemo as useMemo9, useRef as useRef15 } from "react";
2858
4519
  import {
2859
4520
  BufferGeometry,
2860
4521
  DoubleSide as DoubleSide3,
@@ -2862,13 +4523,13 @@ import {
2862
4523
  Matrix4 as Matrix43,
2863
4524
  Quaternion as Quaternion3,
2864
4525
  ShapeUtils,
2865
- Vector2 as Vector22,
2866
- Vector3 as Vector36
4526
+ Vector2 as Vector23,
4527
+ Vector3 as Vector37
2867
4528
  } from "three";
2868
4529
  import { Line2 } from "three/addons/lines/Line2.js";
2869
4530
  import { LineGeometry } from "three/addons/lines/LineGeometry.js";
2870
4531
  import { LineMaterial } from "three/addons/lines/LineMaterial.js";
2871
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
4532
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2872
4533
  var POLYGON_RADIUS = 990;
2873
4534
  var MAX_FILL_SEGMENT_DEGREES = 7;
2874
4535
  function cloneVertices(vertices) {
@@ -2903,7 +4564,7 @@ function intersects(a, b, c, d) {
2903
4564
  }
2904
4565
  function hasSelfIntersection(vertices) {
2905
4566
  const points = vertices.map(
2906
- (vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch)
4567
+ (vertex) => new Vector23(vertex.unwrappedYaw, vertex.pitch)
2907
4568
  );
2908
4569
  for (let index = 0; index < points.length; index += 1) {
2909
4570
  const nextIndex = (index + 1) % points.length;
@@ -2940,7 +4601,7 @@ function polygonContainsDirection(vertices, direction) {
2940
4601
  }
2941
4602
  function fitsHemisphere(vertices) {
2942
4603
  const directions = vertices.map((vertex) => panoPositionToVector3(vertex).normalize());
2943
- const candidates = [new Vector36()];
4604
+ const candidates = [new Vector37()];
2944
4605
  for (const direction of directions) {
2945
4606
  candidates[0].add(direction);
2946
4607
  candidates.push(direction.clone());
@@ -2964,7 +4625,7 @@ function validatePolygonVertices(vertices) {
2964
4625
  if (hasSelfIntersection(unwrapped)) {
2965
4626
  issues.push({ code: "self_intersection", message: "Polygon edges cannot cross." });
2966
4627
  }
2967
- if (polygonContainsDirection(normalized, new Vector36(0, 1, 0)) || polygonContainsDirection(normalized, new Vector36(0, -1, 0))) {
4628
+ if (polygonContainsDirection(normalized, new Vector37(0, 1, 0)) || polygonContainsDirection(normalized, new Vector37(0, -1, 0))) {
2968
4629
  issues.push({ code: "contains_pole", message: "Polygons cannot contain a panorama pole." });
2969
4630
  }
2970
4631
  if (!fitsHemisphere(normalized)) {
@@ -2975,7 +4636,7 @@ function validatePolygonVertices(vertices) {
2975
4636
  function polygonCenter(vertices) {
2976
4637
  const sum = vertices.reduce(
2977
4638
  (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
2978
- new Vector36()
4639
+ new Vector37()
2979
4640
  );
2980
4641
  return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
2981
4642
  }
@@ -2984,7 +4645,7 @@ function surfaceQuaternion(position) {
2984
4645
  const inward = outward.clone().negate();
2985
4646
  const pitch = position.pitch * Math.PI / 180;
2986
4647
  const yaw = position.yaw * Math.PI / 180;
2987
- const localUp = new Vector36(
4648
+ const localUp = new Vector37(
2988
4649
  -Math.sin(pitch) * Math.sin(yaw),
2989
4650
  Math.cos(pitch),
2990
4651
  Math.sin(pitch) * Math.cos(yaw)
@@ -2997,7 +4658,7 @@ function makeWorldToLocal(center) {
2997
4658
  return new Matrix43().compose(
2998
4659
  panoPositionToVector3(center, POLYGON_RADIUS),
2999
4660
  surfaceQuaternion(center),
3000
- new Vector36(1, 1, 1)
4661
+ new Vector37(1, 1, 1)
3001
4662
  ).invert();
3002
4663
  }
3003
4664
  function projectedPoint(a, b, c, weightB, weightC, radius, worldToLocal) {
@@ -3052,7 +4713,7 @@ function addSubdividedTriangle(positions, indices, a, b, c, worldToLocal, steps)
3052
4713
  }
3053
4714
  function makeFillGeometry(vertices, center) {
3054
4715
  const unwrapped = unwrapPolygonVertices(vertices);
3055
- const contour = unwrapped.map((vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch));
4716
+ const contour = unwrapped.map((vertex) => new Vector23(vertex.unwrappedYaw, vertex.pitch));
3056
4717
  const triangles = ShapeUtils.triangulateShape(contour, []);
3057
4718
  const positions = [];
3058
4719
  const indices = [];
@@ -3141,38 +4802,38 @@ function PolygonHotspot({
3141
4802
  onInvalid,
3142
4803
  ...anchorProps
3143
4804
  }) {
3144
- const gl = useThree7((state) => state.gl);
3145
- const size = useThree7((state) => state.size);
3146
- const issues = useMemo7(() => validatePolygonVertices(vertices), [vertices]);
4805
+ const gl = useThree12((state) => state.gl);
4806
+ const size = useThree12((state) => state.size);
4807
+ const issues = useMemo9(() => validatePolygonVertices(vertices), [vertices]);
3147
4808
  const valid = issues.length === 0;
3148
- const center = useMemo7(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
3149
- const fillGeometry = useMemo7(
4809
+ const center = useMemo9(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
4810
+ const fillGeometry = useMemo9(
3150
4811
  () => valid && center ? makeFillGeometry(vertices, center) : null,
3151
4812
  [center, valid, vertices]
3152
4813
  );
3153
- const strokePositions = useMemo7(
4814
+ const strokePositions = useMemo9(
3154
4815
  () => valid && center ? makeStrokePositions(vertices, center) : null,
3155
4816
  [center, valid, vertices]
3156
4817
  );
3157
4818
  const resolvedStrokeOpacity = Math.max(0, Math.min(strokeOpacity, 1));
3158
4819
  const devicePixelRatio = gl.getPixelRatio();
3159
- const strokeLine = useMemo7(
4820
+ const strokeLine = useMemo9(
3160
4821
  () => strokePositions ? makeStrokeLine(strokePositions, stroke, strokeWidth * devicePixelRatio, resolvedStrokeOpacity) : null,
3161
4822
  [devicePixelRatio, resolvedStrokeOpacity, stroke, strokePositions, strokeWidth]
3162
4823
  );
3163
- const focusStrokeLine = useMemo7(
4824
+ const focusStrokeLine = useMemo9(
3164
4825
  () => strokePositions ? makeStrokeLine(strokePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3165
4826
  [devicePixelRatio, strokePositions, strokeWidth]
3166
4827
  );
3167
- const startVerticesRef = useRef10(null);
4828
+ const startVerticesRef = useRef15(null);
3168
4829
  const issuesKey = issues.map((issue) => issue.code).join(",");
3169
- useEffect11(() => {
4830
+ useEffect17(() => {
3170
4831
  if (!valid) onInvalid?.(issues);
3171
4832
  }, [issues, issuesKey, onInvalid, valid]);
3172
- useEffect11(() => () => fillGeometry?.dispose(), [fillGeometry]);
3173
- useEffect11(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
3174
- useEffect11(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
3175
- useEffect11(() => {
4833
+ useEffect17(() => () => fillGeometry?.dispose(), [fillGeometry]);
4834
+ useEffect17(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
4835
+ useEffect17(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
4836
+ useEffect17(() => {
3176
4837
  const width = gl.domElement.width;
3177
4838
  const height = gl.domElement.height;
3178
4839
  for (const line of [strokeLine, focusStrokeLine]) {
@@ -3191,7 +4852,7 @@ function PolygonHotspot({
3191
4852
  startPosition: event.startPosition
3192
4853
  });
3193
4854
  };
3194
- return /* @__PURE__ */ jsxs8(
4855
+ return /* @__PURE__ */ jsxs9(
3195
4856
  HotspotAnchor,
3196
4857
  {
3197
4858
  ...anchorProps,
@@ -3206,7 +4867,7 @@ function PolygonHotspot({
3206
4867
  draggable,
3207
4868
  interactive,
3208
4869
  useAngularScale: false,
3209
- focusContent: /* @__PURE__ */ jsx10("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
4870
+ focusContent: /* @__PURE__ */ jsx11("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
3210
4871
  onDragStart: (event) => {
3211
4872
  startVerticesRef.current = cloneVertices(vertices);
3212
4873
  emitVerticesChange(event, onDragStart);
@@ -3217,7 +4878,7 @@ function PolygonHotspot({
3217
4878
  startVerticesRef.current = null;
3218
4879
  },
3219
4880
  children: [
3220
- /* @__PURE__ */ jsx10("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ jsx10(
4881
+ /* @__PURE__ */ jsx11("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ jsx11(
3221
4882
  "meshBasicMaterial",
3222
4883
  {
3223
4884
  color: fill,
@@ -3227,25 +4888,25 @@ function PolygonHotspot({
3227
4888
  transparent: true
3228
4889
  }
3229
4890
  ) }),
3230
- /* @__PURE__ */ jsx10("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
4891
+ /* @__PURE__ */ jsx11("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
3231
4892
  ]
3232
4893
  }
3233
4894
  );
3234
4895
  }
3235
4896
 
3236
4897
  // src/hotspot/polyline-hotspot.tsx
3237
- import { useThree as useThree8 } from "@react-three/fiber";
3238
- import { useEffect as useEffect12, useMemo as useMemo8, useRef as useRef11 } from "react";
4898
+ import { useThree as useThree13 } from "@react-three/fiber";
4899
+ import { useEffect as useEffect18, useMemo as useMemo10, useRef as useRef16 } from "react";
3239
4900
  import {
3240
4901
  MathUtils as MathUtils5,
3241
4902
  Matrix4 as Matrix44,
3242
4903
  Quaternion as Quaternion4,
3243
- Vector3 as Vector37
4904
+ Vector3 as Vector38
3244
4905
  } from "three";
3245
4906
  import { Line2 as Line22 } from "three/addons/lines/Line2.js";
3246
4907
  import { LineGeometry as LineGeometry2 } from "three/addons/lines/LineGeometry.js";
3247
4908
  import { LineMaterial as LineMaterial2 } from "three/addons/lines/LineMaterial.js";
3248
- import { jsx as jsx11 } from "react/jsx-runtime";
4909
+ import { jsx as jsx12 } from "react/jsx-runtime";
3249
4910
  var POLYLINE_RADIUS = 990;
3250
4911
  var MAX_SEGMENT_DEGREES = 7;
3251
4912
  function cloneVertices2(vertices) {
@@ -3270,7 +4931,7 @@ function unwrapVertices(vertices) {
3270
4931
  function polylineCenter(vertices) {
3271
4932
  const sum = vertices.reduce(
3272
4933
  (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
3273
- new Vector37()
4934
+ new Vector38()
3274
4935
  );
3275
4936
  return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
3276
4937
  }
@@ -3279,7 +4940,7 @@ function surfaceQuaternion2(position) {
3279
4940
  const inward = outward.clone().negate();
3280
4941
  const pitch = MathUtils5.degToRad(position.pitch);
3281
4942
  const yaw = MathUtils5.degToRad(position.yaw);
3282
- const localUp = new Vector37(
4943
+ const localUp = new Vector38(
3283
4944
  -Math.sin(pitch) * Math.sin(yaw),
3284
4945
  Math.cos(pitch),
3285
4946
  Math.sin(pitch) * Math.cos(yaw)
@@ -3292,7 +4953,7 @@ function makeWorldToLocal2(center) {
3292
4953
  return new Matrix44().compose(
3293
4954
  panoPositionToVector3(center, POLYLINE_RADIUS),
3294
4955
  surfaceQuaternion2(center),
3295
- new Vector37(1, 1, 1)
4956
+ new Vector38(1, 1, 1)
3296
4957
  ).invert();
3297
4958
  }
3298
4959
  function subdivisionSteps(vertices) {
@@ -3382,32 +5043,32 @@ function PolylineHotspot({
3382
5043
  onInvalid,
3383
5044
  ...anchorProps
3384
5045
  }) {
3385
- const gl = useThree8((state) => state.gl);
3386
- const size = useThree8((state) => state.size);
3387
- const issues = useMemo8(() => validatePolylineVertices(vertices), [vertices]);
5046
+ const gl = useThree13((state) => state.gl);
5047
+ const size = useThree13((state) => state.size);
5048
+ const issues = useMemo10(() => validatePolylineVertices(vertices), [vertices]);
3388
5049
  const valid = issues.length === 0;
3389
- const center = useMemo8(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
3390
- const linePositions = useMemo8(
5050
+ const center = useMemo10(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
5051
+ const linePositions = useMemo10(
3391
5052
  () => valid && center ? makeLinePositions(vertices, center) : null,
3392
5053
  [center, valid, vertices]
3393
5054
  );
3394
5055
  const devicePixelRatio = gl.getPixelRatio();
3395
5056
  const opacity = Math.max(0, Math.min(strokeOpacity, 1));
3396
- const line = useMemo8(
5057
+ const line = useMemo10(
3397
5058
  () => linePositions ? makeLine(linePositions, stroke, strokeWidth * devicePixelRatio, opacity) : null,
3398
5059
  [devicePixelRatio, linePositions, opacity, stroke, strokeWidth]
3399
5060
  );
3400
- const focusLine = useMemo8(
5061
+ const focusLine = useMemo10(
3401
5062
  () => linePositions ? makeLine(linePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3402
5063
  [devicePixelRatio, linePositions, strokeWidth]
3403
5064
  );
3404
- const startVerticesRef = useRef11(null);
3405
- useEffect12(() => {
5065
+ const startVerticesRef = useRef16(null);
5066
+ useEffect18(() => {
3406
5067
  if (!valid) onInvalid?.(issues);
3407
5068
  }, [issues, onInvalid, valid]);
3408
- useEffect12(() => () => disposeLine(line), [line]);
3409
- useEffect12(() => () => disposeLine(focusLine), [focusLine]);
3410
- useEffect12(() => {
5069
+ useEffect18(() => () => disposeLine(line), [line]);
5070
+ useEffect18(() => () => disposeLine(focusLine), [focusLine]);
5071
+ useEffect18(() => {
3411
5072
  const width = gl.domElement.width;
3412
5073
  const height = gl.domElement.height;
3413
5074
  for (const nextLine of [line, focusLine]) {
@@ -3425,7 +5086,7 @@ function PolylineHotspot({
3425
5086
  startPosition: event.startPosition
3426
5087
  });
3427
5088
  };
3428
- return /* @__PURE__ */ jsx11(
5089
+ return /* @__PURE__ */ jsx12(
3429
5090
  HotspotAnchor,
3430
5091
  {
3431
5092
  ...anchorProps,
@@ -3439,7 +5100,7 @@ function PolylineHotspot({
3439
5100
  renderOrder,
3440
5101
  draggable,
3441
5102
  useAngularScale: false,
3442
- focusContent: /* @__PURE__ */ jsx11("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
5103
+ focusContent: /* @__PURE__ */ jsx12("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
3443
5104
  onDragStart: (event) => {
3444
5105
  startVerticesRef.current = cloneVertices2(vertices);
3445
5106
  emitVerticesChange(event, onDragStart);
@@ -3449,13 +5110,13 @@ function PolylineHotspot({
3449
5110
  emitVerticesChange(event, onDragEnd);
3450
5111
  startVerticesRef.current = null;
3451
5112
  },
3452
- children: /* @__PURE__ */ jsx11("primitive", { object: line, renderOrder: renderOrder + 1 })
5113
+ children: /* @__PURE__ */ jsx12("primitive", { object: line, renderOrder: renderOrder + 1 })
3453
5114
  }
3454
5115
  );
3455
5116
  }
3456
5117
 
3457
5118
  // src/hotspot/graphic-hotspot.tsx
3458
- import { useEffect as useEffect13, useRef as useRef12, useState as useState7 } from "react";
5119
+ import { useEffect as useEffect19, useRef as useRef17, useState as useState8 } from "react";
3459
5120
  import {
3460
5121
  CanvasTexture,
3461
5122
  ClampToEdgeWrapping as ClampToEdgeWrapping4,
@@ -3464,7 +5125,7 @@ import {
3464
5125
  LinearMipmapLinearFilter as LinearMipmapLinearFilter3,
3465
5126
  SRGBColorSpace as SRGBColorSpace5
3466
5127
  } from "three";
3467
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
5128
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3468
5129
  var GRAPHIC_TEXTURE_SIZE = 512;
3469
5130
  function clampOpacity2(opacity) {
3470
5131
  if (!Number.isFinite(opacity)) {
@@ -3475,14 +5136,34 @@ function clampOpacity2(opacity) {
3475
5136
  function resolveStrokeWidth(value) {
3476
5137
  return Number.isFinite(value) ? Math.max(value, 0) : 8;
3477
5138
  }
3478
- function roundedRectanglePath(context, x, y, width, height, radius) {
3479
- const safeRadius = Math.min(Math.max(radius, 0), width / 2, height / 2);
5139
+ function resolveCornerRadius(value) {
5140
+ return Number.isFinite(value) ? Math.max(0, Math.min(value, 0.5)) : 0;
5141
+ }
5142
+ function roundedRectanglePath(context, x, y, width, height, radiusRatio, aspectWidth, aspectHeight) {
5143
+ const safeAspectWidth = Number.isFinite(aspectWidth) && aspectWidth > 0 ? aspectWidth : 1;
5144
+ const safeAspectHeight = Number.isFinite(aspectHeight) && aspectHeight > 0 ? aspectHeight : 1;
5145
+ const shortSide = Math.min(safeAspectWidth, safeAspectHeight);
5146
+ const safeRadiusRatio = resolveCornerRadius(radiusRatio);
5147
+ const radiusX = Math.min(width / 2, width * safeRadiusRatio * shortSide / safeAspectWidth);
5148
+ const radiusY = Math.min(height / 2, height * safeRadiusRatio * shortSide / safeAspectHeight);
5149
+ context.beginPath();
5150
+ context.moveTo(x + radiusX, y);
5151
+ context.lineTo(x + width - radiusX, y);
5152
+ context.ellipse(x + width - radiusX, y + radiusY, radiusX, radiusY, 0, -Math.PI / 2, 0);
5153
+ context.lineTo(x + width, y + height - radiusY);
5154
+ context.ellipse(x + width - radiusX, y + height - radiusY, radiusX, radiusY, 0, 0, Math.PI / 2);
5155
+ context.lineTo(x + radiusX, y + height);
5156
+ context.ellipse(x + radiusX, y + height - radiusY, radiusX, radiusY, 0, Math.PI / 2, Math.PI);
5157
+ context.lineTo(x, y + radiusY);
5158
+ context.ellipse(x + radiusX, y + radiusY, radiusX, radiusY, 0, Math.PI, Math.PI * 1.5);
5159
+ context.closePath();
5160
+ }
5161
+ function polygonPath(context, points) {
3480
5162
  context.beginPath();
3481
- context.moveTo(x + safeRadius, y);
3482
- context.arcTo(x + width, y, x + width, y + height, safeRadius);
3483
- context.arcTo(x + width, y + height, x, y + height, safeRadius);
3484
- context.arcTo(x, y + height, x, y, safeRadius);
3485
- context.arcTo(x, y, x + width, y, safeRadius);
5163
+ context.moveTo(points[0][0], points[0][1]);
5164
+ for (const [x, y] of points.slice(1)) {
5165
+ context.lineTo(x, y);
5166
+ }
3486
5167
  context.closePath();
3487
5168
  }
3488
5169
  function paintPath(context, graphic, path) {
@@ -3496,7 +5177,7 @@ function paintPath(context, graphic, path) {
3496
5177
  path ? context.stroke(path) : context.stroke();
3497
5178
  }
3498
5179
  }
3499
- function drawGraphic(context, graphic) {
5180
+ function drawGraphic(context, graphic, aspectWidth, aspectHeight) {
3500
5181
  const padding = Math.max(resolveStrokeWidth(graphic.strokeWidth) / 2 + 2, 4);
3501
5182
  const drawableSize = GRAPHIC_TEXTURE_SIZE - padding * 2;
3502
5183
  context.clearRect(0, 0, GRAPHIC_TEXTURE_SIZE, GRAPHIC_TEXTURE_SIZE);
@@ -3514,6 +5195,63 @@ function drawGraphic(context, graphic) {
3514
5195
  paintPath(context, graphic);
3515
5196
  return;
3516
5197
  }
5198
+ if (graphic.kind === "triangle") {
5199
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5200
+ polygonPath(context, [
5201
+ [center, padding],
5202
+ [padding + drawableSize, padding + drawableSize],
5203
+ [padding, padding + drawableSize]
5204
+ ]);
5205
+ paintPath(context, graphic);
5206
+ return;
5207
+ }
5208
+ if (graphic.kind === "diamond") {
5209
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5210
+ polygonPath(context, [
5211
+ [center, padding],
5212
+ [padding + drawableSize, center],
5213
+ [center, padding + drawableSize],
5214
+ [padding, center]
5215
+ ]);
5216
+ paintPath(context, graphic);
5217
+ return;
5218
+ }
5219
+ if (graphic.kind === "star") {
5220
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5221
+ const outerRadius = drawableSize / 2;
5222
+ const innerRadius = outerRadius * 0.45;
5223
+ const points = [];
5224
+ for (let index = 0; index < 10; index += 1) {
5225
+ const angle = -Math.PI / 2 + index * Math.PI / 5;
5226
+ const radius = index % 2 === 0 ? outerRadius : innerRadius;
5227
+ points.push([
5228
+ center + Math.cos(angle) * radius,
5229
+ center + Math.sin(angle) * radius
5230
+ ]);
5231
+ }
5232
+ polygonPath(context, points);
5233
+ paintPath(context, graphic);
5234
+ return;
5235
+ }
5236
+ if (graphic.kind === "arrow") {
5237
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5238
+ const top = padding;
5239
+ const bottom = padding + drawableSize;
5240
+ const headBottom = top + drawableSize * 0.43;
5241
+ const headHalfWidth = drawableSize * 0.38;
5242
+ const stemHalfWidth = drawableSize * 0.14;
5243
+ polygonPath(context, [
5244
+ [center, top],
5245
+ [center + headHalfWidth, headBottom],
5246
+ [center + stemHalfWidth, headBottom],
5247
+ [center + stemHalfWidth, bottom],
5248
+ [center - stemHalfWidth, bottom],
5249
+ [center - stemHalfWidth, headBottom],
5250
+ [center - headHalfWidth, headBottom]
5251
+ ]);
5252
+ paintPath(context, graphic);
5253
+ return;
5254
+ }
3517
5255
  if (graphic.kind === "rectangle") {
3518
5256
  roundedRectanglePath(
3519
5257
  context,
@@ -3521,7 +5259,9 @@ function drawGraphic(context, graphic) {
3521
5259
  padding,
3522
5260
  drawableSize,
3523
5261
  drawableSize,
3524
- graphic.cornerRadius ?? 0
5262
+ graphic.cornerRadius ?? 0,
5263
+ aspectWidth,
5264
+ aspectHeight
3525
5265
  );
3526
5266
  paintPath(context, graphic);
3527
5267
  return;
@@ -3560,7 +5300,7 @@ function drawGraphic(context, graphic) {
3560
5300
  paintPath(context, graphic, path);
3561
5301
  context.restore();
3562
5302
  }
3563
- function createGraphicTexture(graphic) {
5303
+ function createGraphicTexture(graphic, aspectWidth, aspectHeight) {
3564
5304
  const canvas = document.createElement("canvas");
3565
5305
  canvas.width = GRAPHIC_TEXTURE_SIZE;
3566
5306
  canvas.height = GRAPHIC_TEXTURE_SIZE;
@@ -3568,7 +5308,7 @@ function createGraphicTexture(graphic) {
3568
5308
  if (!context) {
3569
5309
  throw new Error("Canvas 2D is unavailable for this graphic hotspot.");
3570
5310
  }
3571
- drawGraphic(context, graphic);
5311
+ drawGraphic(context, graphic, aspectWidth, aspectHeight);
3572
5312
  const texture = new CanvasTexture(canvas);
3573
5313
  texture.colorSpace = SRGBColorSpace5;
3574
5314
  texture.wrapS = ClampToEdgeWrapping4;
@@ -3579,16 +5319,16 @@ function createGraphicTexture(graphic) {
3579
5319
  texture.needsUpdate = true;
3580
5320
  return texture;
3581
5321
  }
3582
- function useGraphicTexture(graphic, onLoad, onError) {
3583
- const [texture, setTexture] = useState7(null);
3584
- const onLoadRef = useRef12(onLoad);
3585
- const onErrorRef = useRef12(onError);
5322
+ function useGraphicTexture(graphic, aspectWidth, aspectHeight, onLoad, onError) {
5323
+ const [texture, setTexture] = useState8(null);
5324
+ const onLoadRef = useRef17(onLoad);
5325
+ const onErrorRef = useRef17(onError);
3586
5326
  onLoadRef.current = onLoad;
3587
5327
  onErrorRef.current = onError;
3588
- useEffect13(() => {
5328
+ useEffect19(() => {
3589
5329
  let nextTexture = null;
3590
5330
  try {
3591
- nextTexture = createGraphicTexture(graphic);
5331
+ nextTexture = createGraphicTexture(graphic, aspectWidth, aspectHeight);
3592
5332
  setTexture(nextTexture);
3593
5333
  onLoadRef.current?.(nextTexture);
3594
5334
  } catch (error) {
@@ -3598,7 +5338,7 @@ function useGraphicTexture(graphic, onLoad, onError) {
3598
5338
  return () => {
3599
5339
  nextTexture?.dispose();
3600
5340
  };
3601
- }, [graphic]);
5341
+ }, [aspectHeight, aspectWidth, graphic]);
3602
5342
  return texture;
3603
5343
  }
3604
5344
  function CanvasGraphicHotspot({
@@ -3610,10 +5350,10 @@ function CanvasGraphicHotspot({
3610
5350
  onError,
3611
5351
  ...anchorProps
3612
5352
  }) {
3613
- const texture = useGraphicTexture(graphic, onLoad, onError);
3614
- return /* @__PURE__ */ jsx12(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs9("mesh", { children: [
3615
- /* @__PURE__ */ jsx12("planeGeometry", { args: [1, 1] }),
3616
- /* @__PURE__ */ jsx12(
5353
+ const texture = useGraphicTexture(graphic, width, height, onLoad, onError);
5354
+ return /* @__PURE__ */ jsx13(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs10("mesh", { children: [
5355
+ /* @__PURE__ */ jsx13("planeGeometry", { args: [1, 1] }),
5356
+ /* @__PURE__ */ jsx13(
3617
5357
  "meshBasicMaterial",
3618
5358
  {
3619
5359
  map: texture,
@@ -3629,14 +5369,14 @@ function GraphicHotspot(props) {
3629
5369
  const { graphic, ...restProps } = props;
3630
5370
  if (graphic.kind === "svg") {
3631
5371
  const imageProps = restProps;
3632
- return /* @__PURE__ */ jsx12(ImageHotspot, { ...imageProps, src: graphic.src });
5372
+ return /* @__PURE__ */ jsx13(ImageHotspot, { ...imageProps, src: graphic.src });
3633
5373
  }
3634
- return /* @__PURE__ */ jsx12(CanvasGraphicHotspot, { ...restProps, graphic });
5374
+ return /* @__PURE__ */ jsx13(CanvasGraphicHotspot, { ...restProps, graphic });
3635
5375
  }
3636
5376
 
3637
5377
  // src/hotspot/sequence-hotspot.tsx
3638
- import { useFrame as useFrame6, useThree as useThree9 } from "@react-three/fiber";
3639
- import { useEffect as useEffect14, useRef as useRef13, useState as useState8 } from "react";
5378
+ import { useFrame as useFrame8, useThree as useThree14 } from "@react-three/fiber";
5379
+ import { useEffect as useEffect20, useRef as useRef18, useState as useState9 } from "react";
3640
5380
  import {
3641
5381
  ClampToEdgeWrapping as ClampToEdgeWrapping5,
3642
5382
  DoubleSide as DoubleSide5,
@@ -3645,7 +5385,7 @@ import {
3645
5385
  SRGBColorSpace as SRGBColorSpace6,
3646
5386
  TextureLoader as TextureLoader5
3647
5387
  } from "three";
3648
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
5388
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3649
5389
  var DEFAULT_SEQUENCE_FPS = 12;
3650
5390
  function clampOpacity3(opacity) {
3651
5391
  if (!Number.isFinite(opacity)) {
@@ -3665,13 +5405,13 @@ function useSpriteTexture({
3665
5405
  onError,
3666
5406
  onLoadProgress
3667
5407
  }) {
3668
- const gl = useThree9((state) => state.gl);
3669
- const [texture, setTexture] = useState8(null);
3670
- const onErrorRef = useRef13(onError);
3671
- const onLoadProgressRef = useRef13(onLoadProgress);
5408
+ const gl = useThree14((state) => state.gl);
5409
+ const [texture, setTexture] = useState9(null);
5410
+ const onErrorRef = useRef18(onError);
5411
+ const onLoadProgressRef = useRef18(onLoadProgress);
3672
5412
  onErrorRef.current = onError;
3673
5413
  onLoadProgressRef.current = onLoadProgress;
3674
- useEffect14(() => {
5414
+ useEffect20(() => {
3675
5415
  let active = true;
3676
5416
  setTexture(null);
3677
5417
  if (!src) {
@@ -3712,7 +5452,7 @@ function useSpriteTexture({
3712
5452
  active = false;
3713
5453
  };
3714
5454
  }, [gl, id, src]);
3715
- useEffect14(
5455
+ useEffect20(
3716
5456
  () => () => {
3717
5457
  texture?.dispose();
3718
5458
  },
@@ -3741,24 +5481,24 @@ function SequenceHotspot({
3741
5481
  const texture = useSpriteTexture({ id, src, onError, onLoadProgress });
3742
5482
  const frameCount = resolveFrameCount(requestedFrameCount);
3743
5483
  const safeFps = resolveFps(fps);
3744
- const [frameIndex, setFrameIndex] = useState8(0);
3745
- const elapsedRef = useRef13(0);
3746
- const frameIndexRef = useRef13(0);
3747
- const endedRef = useRef13(false);
3748
- const lastPlaybackStateRef = useRef13(null);
3749
- const onFrameChangeRef = useRef13(onFrameChange);
3750
- const onPlaybackStateChangeRef = useRef13(onPlaybackStateChange);
3751
- const onEndedRef = useRef13(onEnded);
5484
+ const [frameIndex, setFrameIndex] = useState9(0);
5485
+ const elapsedRef = useRef18(0);
5486
+ const frameIndexRef = useRef18(0);
5487
+ const endedRef = useRef18(false);
5488
+ const lastPlaybackStateRef = useRef18(null);
5489
+ const onFrameChangeRef = useRef18(onFrameChange);
5490
+ const onPlaybackStateChangeRef = useRef18(onPlaybackStateChange);
5491
+ const onEndedRef = useRef18(onEnded);
3752
5492
  onFrameChangeRef.current = onFrameChange;
3753
5493
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3754
5494
  onEndedRef.current = onEnded;
3755
- useEffect14(() => {
5495
+ useEffect20(() => {
3756
5496
  elapsedRef.current = 0;
3757
5497
  frameIndexRef.current = 0;
3758
5498
  endedRef.current = false;
3759
5499
  setFrameIndex(0);
3760
5500
  }, [frameCount, frameDirection, src]);
3761
- useEffect14(() => {
5501
+ useEffect20(() => {
3762
5502
  if (!texture) {
3763
5503
  return;
3764
5504
  }
@@ -3772,13 +5512,13 @@ function SequenceHotspot({
3772
5512
  );
3773
5513
  texture.needsUpdate = true;
3774
5514
  }, [frameCount, frameDirection, frameIndex, texture]);
3775
- useEffect14(() => {
5515
+ useEffect20(() => {
3776
5516
  if (!texture) {
3777
5517
  return;
3778
5518
  }
3779
5519
  onFrameChangeRef.current?.({ id, index: 0, src });
3780
5520
  }, [id, src, texture]);
3781
- useEffect14(() => {
5521
+ useEffect20(() => {
3782
5522
  if (!texture) {
3783
5523
  return;
3784
5524
  }
@@ -3797,7 +5537,7 @@ function SequenceHotspot({
3797
5537
  onPlaybackStateChangeRef.current?.(nextState);
3798
5538
  }
3799
5539
  }, [playing, texture]);
3800
- useFrame6((_, deltaSeconds) => {
5540
+ useFrame8((_, deltaSeconds) => {
3801
5541
  if (!texture || !playing || endedRef.current) {
3802
5542
  return;
3803
5543
  }
@@ -3827,9 +5567,9 @@ function SequenceHotspot({
3827
5567
  onFrameChangeRef.current?.({ id, index: nextIndex, src });
3828
5568
  }
3829
5569
  });
3830
- return /* @__PURE__ */ jsx13(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs10("mesh", { children: [
3831
- /* @__PURE__ */ jsx13("planeGeometry", { args: [1, 1] }),
3832
- /* @__PURE__ */ jsx13(
5570
+ return /* @__PURE__ */ jsx14(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs11("mesh", { children: [
5571
+ /* @__PURE__ */ jsx14("planeGeometry", { args: [1, 1] }),
5572
+ /* @__PURE__ */ jsx14(
3833
5573
  "meshBasicMaterial",
3834
5574
  {
3835
5575
  map: texture,
@@ -3843,8 +5583,8 @@ function SequenceHotspot({
3843
5583
  }
3844
5584
 
3845
5585
  // src/hotspot/video-hotspot.tsx
3846
- import { useThree as useThree10 } from "@react-three/fiber";
3847
- import { useEffect as useEffect15, useRef as useRef14, useState as useState9 } from "react";
5586
+ import { useThree as useThree15 } from "@react-three/fiber";
5587
+ import { useEffect as useEffect21, useRef as useRef19, useState as useState10 } from "react";
3848
5588
  import {
3849
5589
  ClampToEdgeWrapping as ClampToEdgeWrapping6,
3850
5590
  DoubleSide as DoubleSide6,
@@ -3854,7 +5594,7 @@ import {
3854
5594
  TextureLoader as TextureLoader6,
3855
5595
  VideoTexture
3856
5596
  } from "three";
3857
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
5597
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3858
5598
  function clampOpacity4(opacity) {
3859
5599
  if (!Number.isFinite(opacity)) {
3860
5600
  return 1;
@@ -3868,11 +5608,11 @@ function clampVolume(volume) {
3868
5608
  return Math.max(0, Math.min(volume, 1));
3869
5609
  }
3870
5610
  function usePosterTexture(id, poster, onError) {
3871
- const gl = useThree10((state) => state.gl);
3872
- const [texture, setTexture] = useState9(null);
3873
- const onErrorRef = useRef14(onError);
5611
+ const gl = useThree15((state) => state.gl);
5612
+ const [texture, setTexture] = useState10(null);
5613
+ const onErrorRef = useRef19(onError);
3874
5614
  onErrorRef.current = onError;
3875
- useEffect15(() => {
5615
+ useEffect21(() => {
3876
5616
  let active = true;
3877
5617
  setTexture(null);
3878
5618
  if (!poster) {
@@ -3909,7 +5649,7 @@ function usePosterTexture(id, poster, onError) {
3909
5649
  active = false;
3910
5650
  };
3911
5651
  }, [gl, id, poster]);
3912
- useEffect15(
5652
+ useEffect21(
3913
5653
  () => () => {
3914
5654
  texture?.dispose();
3915
5655
  },
@@ -3927,14 +5667,14 @@ function useVideoResource({
3927
5667
  poster,
3928
5668
  src
3929
5669
  }) {
3930
- const [resource, setResource] = useState9(null);
3931
- const onEndedRef = useRef14(onEnded);
3932
- const onErrorRef = useRef14(onError);
3933
- const onPlaybackStateChangeRef = useRef14(onPlaybackStateChange);
5670
+ const [resource, setResource] = useState10(null);
5671
+ const onEndedRef = useRef19(onEnded);
5672
+ const onErrorRef = useRef19(onError);
5673
+ const onPlaybackStateChangeRef = useRef19(onPlaybackStateChange);
3934
5674
  onEndedRef.current = onEnded;
3935
5675
  onErrorRef.current = onError;
3936
5676
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3937
- useEffect15(() => {
5677
+ useEffect21(() => {
3938
5678
  if (!src) {
3939
5679
  setResource(null);
3940
5680
  return;
@@ -4035,11 +5775,11 @@ function VideoHotspot({
4035
5775
  src
4036
5776
  });
4037
5777
  const posterTexture = usePosterTexture(id, poster, onError);
4038
- const onPlaybackErrorRef = useRef14(onPlaybackError);
4039
- const onPlaybackStateChangeRef = useRef14(onPlaybackStateChange);
5778
+ const onPlaybackErrorRef = useRef19(onPlaybackError);
5779
+ const onPlaybackStateChangeRef = useRef19(onPlaybackStateChange);
4040
5780
  onPlaybackErrorRef.current = onPlaybackError;
4041
5781
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
4042
- useEffect15(() => {
5782
+ useEffect21(() => {
4043
5783
  if (!resource) {
4044
5784
  return;
4045
5785
  }
@@ -4049,7 +5789,7 @@ function VideoHotspot({
4049
5789
  resource.video.playsInline = playsInline;
4050
5790
  resource.video.preload = preload;
4051
5791
  }, [loop, muted, playsInline, preload, resource, volume]);
4052
- useEffect15(() => {
5792
+ useEffect21(() => {
4053
5793
  if (!resource) {
4054
5794
  return;
4055
5795
  }
@@ -4072,9 +5812,9 @@ function VideoHotspot({
4072
5812
  };
4073
5813
  }, [playing, resource]);
4074
5814
  const activeTexture = resource?.ready ? resource.texture : posterTexture;
4075
- return /* @__PURE__ */ jsx14(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs11("mesh", { children: [
4076
- /* @__PURE__ */ jsx14("planeGeometry", { args: [1, 1] }),
4077
- /* @__PURE__ */ jsx14(
5815
+ return /* @__PURE__ */ jsx15(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs12("mesh", { children: [
5816
+ /* @__PURE__ */ jsx15("planeGeometry", { args: [1, 1] }),
5817
+ /* @__PURE__ */ jsx15(
4078
5818
  "meshBasicMaterial",
4079
5819
  {
4080
5820
  map: activeTexture,
@@ -4090,7 +5830,12 @@ export {
4090
5830
  AutoRotate,
4091
5831
  GraphicHotspot,
4092
5832
  ImageHotspot,
5833
+ KeyboardControls,
4093
5834
  MAX_HOTSPOT_PITCH,
5835
+ MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY,
5836
+ MouseControls,
5837
+ PanoContextMenu,
5838
+ PanoEvents,
4094
5839
  PanoView,
4095
5840
  PanoramaScenes,
4096
5841
  PolygonHotspot,
@@ -4098,12 +5843,19 @@ export {
4098
5843
  SequenceHotspot,
4099
5844
  Sphere,
4100
5845
  Tile,
5846
+ TouchControls,
4101
5847
  VideoHotspot,
4102
5848
  clampPanoPitch,
5849
+ composePanoContextMenuItems,
5850
+ createDefaultPanoContextMenuItems,
5851
+ createPanoContextMenuPresets,
5852
+ cycleSceneId,
4103
5853
  normalizePanoPosition,
4104
5854
  normalizePanoYaw,
4105
5855
  panoPositionToVector3,
5856
+ resolvePanoContextMenuEntries,
4106
5857
  unwrapPolygonVertices,
5858
+ usePanoEvents,
4107
5859
  validatePolygonVertices,
4108
5860
  validatePolylineVertices,
4109
5861
  vector3ToPanoPosition