@ericchen1990/pano-view 0.1.6 → 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,16 +4,54 @@
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,
@@ -21,13 +59,15 @@ import {
21
59
  } from "react";
22
60
  import { Euler, MathUtils, PerspectiveCamera } from "three";
23
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,116 +576,1454 @@ 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 = 90;
563
-
564
- // src/hotspot/coordinates.ts
565
- var POLE_EPSILON_DEGREES = 1e-6;
566
- function normalizePanoYaw(yaw) {
567
- if (!Number.isFinite(yaw)) {
568
- 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 [];
569
607
  }
570
- return MathUtils2.euclideanModulo(yaw + 180, 360) - 180;
608
+ return (typeof value === "string" ? [value] : [...value]).filter(
609
+ (entry) => entry.length > 0
610
+ );
571
611
  }
572
- function clampPanoPitch(pitch) {
573
- if (!Number.isFinite(pitch)) {
574
- 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
+ }
575
619
  }
576
- const clamped = MathUtils2.clamp(
577
- pitch,
578
- -MAX_HOTSPOT_PITCH,
579
- MAX_HOTSPOT_PITCH
580
- );
581
- return Math.abs(clamped) >= MAX_HOTSPOT_PITCH - POLE_EPSILON_DEGREES ? Math.sign(clamped) * MAX_HOTSPOT_PITCH : clamped;
620
+ return map;
582
621
  }
583
- function normalizePanoPosition(position) {
584
- const pitch = clampPanoPitch(position.pitch);
585
- return {
586
- yaw: Math.abs(pitch) === MAX_HOTSPOT_PITCH ? 0 : normalizePanoYaw(position.yaw),
587
- pitch
588
- };
622
+ function matchAction(event, keyMap) {
623
+ return keyMap.get(event.key.toLowerCase()) ?? keyMap.get(event.code.toLowerCase()) ?? null;
589
624
  }
590
- function panoPositionToVector3(position, radius = 1) {
591
- const normalized = normalizePanoPosition(position);
592
- const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
593
- const yaw = MathUtils2.degToRad(normalized.yaw);
594
- const pitch = MathUtils2.degToRad(normalized.pitch);
595
- const cosPitch = Math.cos(pitch);
596
- return new Vector3(
597
- cosPitch * Math.sin(yaw) * safeRadius,
598
- Math.sin(pitch) * safeRadius,
599
- -cosPitch * Math.cos(yaw) * safeRadius
600
- );
625
+ function resolveSpeed(value, fallback) {
626
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
601
627
  }
602
- function vector3ToPanoPosition(vector) {
603
- const length = Math.hypot(vector.x, vector.y, vector.z);
604
- if (!Number.isFinite(length) || length === 0) {
605
- return { yaw: 0, pitch: 0 };
628
+ function cycleSceneId(scenes, activeSceneId, step) {
629
+ if (scenes.length === 0) {
630
+ return null;
606
631
  }
607
- return normalizePanoPosition({
608
- yaw: MathUtils2.radToDeg(Math.atan2(vector.x, -vector.z)),
609
- pitch: MathUtils2.radToDeg(
610
- Math.asin(MathUtils2.clamp(vector.y / length, -1, 1))
611
- )
612
- });
613
- }
614
-
615
- // src/panorama-radius.ts
616
- var DEFAULT_PANORAMA_RADIUS = 1e3;
617
- var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
618
- var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
619
-
620
- // src/panorama-event-surface.tsx
621
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
622
- var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
623
- function makePanoramaPointerEvent(event) {
624
- const nativeEvent = event.nativeEvent;
625
- return {
626
- position: vector3ToPanoPosition(event.ray.direction),
627
- nativeEvent,
628
- pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
629
- button: nativeEvent.button,
630
- buttons: nativeEvent.buttons,
631
- clientX: nativeEvent.clientX,
632
- clientY: nativeEvent.clientY
633
- };
634
- }
635
- function emitPanoramaEvent(event, callback) {
636
- if (!callback) {
637
- return;
632
+ const index = scenes.findIndex((scene) => scene.id === activeSceneId);
633
+ if (index < 0) {
634
+ return null;
638
635
  }
639
- event.stopPropagation();
640
- callback(makePanoramaPointerEvent(event));
636
+ const nextIndex = (index + step + scenes.length) % scenes.length;
637
+ return scenes[nextIndex]?.id ?? null;
641
638
  }
642
- function PanoramaEventSurface({
643
- onClick,
644
- onDoubleClick,
645
- 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
646
649
  }) {
647
- if (!onClick && !onDoubleClick && !onPointerMove) {
648
- 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>.");
649
661
  }
650
- return /* @__PURE__ */ jsxs(
651
- "mesh",
652
- {
653
- renderOrder: -200,
654
- onClick: (event) => emitPanoramaEvent(event, onClick),
655
- onDoubleClick: (event) => emitPanoramaEvent(event, onDoubleClick),
656
- onPointerMove: (event) => emitPanoramaEvent(event, onPointerMove),
657
- children: [
658
- /* @__PURE__ */ jsx2("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
659
- /* @__PURE__ */ jsx2(
660
- "meshBasicMaterial",
661
- {
662
- colorWrite: false,
663
- depthTest: false,
664
- depthWrite: false,
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,
665
2027
  opacity: 0,
666
2028
  side: BackSide,
667
2029
  transparent: true
@@ -669,21 +2031,194 @@ function PanoramaEventSurface({
669
2031
  )
670
2032
  ]
671
2033
  }
672
- );
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;
2077
+ }
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;
673
2181
  }
674
2182
 
675
2183
  // src/pano-view.tsx
676
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2184
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
677
2185
  var DEFAULT_VIEW = {
678
2186
  yaw: 0,
679
2187
  pitch: 0,
680
2188
  fov: 75
681
2189
  };
2190
+ var DEFAULT_POINTER_ROTATE_SPEED = 0.35;
2191
+ var DEFAULT_MOUSE_ZOOM_SPEED = 0.08;
682
2192
  var DEFAULT_CANVAS_STYLE = {
683
2193
  display: "block",
684
2194
  height: "100%",
685
2195
  width: "100%"
686
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
+ }
687
2222
  var PanoView = forwardRef2(
688
2223
  function PanoView2({
689
2224
  children,
@@ -691,6 +2226,7 @@ var PanoView = forwardRef2(
691
2226
  minFov = 30,
692
2227
  maxFov = 100,
693
2228
  controls = true,
2229
+ contextMenu = true,
694
2230
  onViewChange,
695
2231
  onPanoramaClick,
696
2232
  onPanoramaDoubleClick,
@@ -700,16 +2236,18 @@ var PanoView = forwardRef2(
700
2236
  "aria-label": ariaLabel = "Panorama viewer",
701
2237
  ...divProps
702
2238
  }, ref) {
703
- const rootRef = useRef4(null);
704
- const controlsRef = useRef4(null);
2239
+ const rootRef = useRef9(null);
2240
+ const controlsRef = useRef9(null);
2241
+ const eventBus = useMemo4(() => createPanoEventBus(), []);
705
2242
  const { controls: hotspotAccessibilityControls, registry } = useHotspotAccessibilityLayer();
706
- const fallbackViewRef = useRef4(DEFAULT_VIEW);
2243
+ const { api: contextMenuOverlayApi, overlay: contextMenuOverlay } = usePanoContextMenuOverlay();
2244
+ const fallbackViewRef = useRef9(DEFAULT_VIEW);
707
2245
  const normalizedMinFov = Math.max(1, Math.min(minFov, maxFov - 1));
708
2246
  const normalizedMaxFov = Math.min(
709
2247
  179,
710
2248
  Math.max(maxFov, normalizedMinFov + 1)
711
2249
  );
712
- const normalizedInitialView = useMemo2(
2250
+ const normalizedInitialView = useMemo4(
713
2251
  () => ({
714
2252
  yaw: initialView?.yaw ?? DEFAULT_VIEW.yaw,
715
2253
  pitch: clampPanoPitch(initialView?.pitch ?? DEFAULT_VIEW.pitch),
@@ -728,15 +2266,98 @@ var PanoView = forwardRef2(
728
2266
  );
729
2267
  fallbackViewRef.current = normalizedInitialView;
730
2268
  const controlsEnabled = controls !== false;
731
- const controlOptions = useMemo2(
2269
+ const controlOptions = useMemo4(
732
2270
  () => typeof controls === "object" ? controls : {},
733
2271
  [controls]
734
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
+ );
735
2356
  const legacyAutoRotateOptions = controlOptions;
736
- const [legacyAutoRotate, setLegacyAutoRotate] = useState2(
2357
+ const [legacyAutoRotate, setLegacyAutoRotate] = useState3(
737
2358
  legacyAutoRotateOptions.autoRotate ?? false
738
2359
  );
739
- useEffect4(() => {
2360
+ useEffect10(() => {
740
2361
  setLegacyAutoRotate(legacyAutoRotateOptions.autoRotate ?? false);
741
2362
  }, [legacyAutoRotateOptions.autoRotate]);
742
2363
  useImperativeHandle2(
@@ -755,20 +2376,11 @@ var PanoView = forwardRef2(
755
2376
  stopAutoRotate: () => {
756
2377
  setLegacyAutoRotate(false);
757
2378
  },
758
- toggleFullscreen: async () => {
759
- if (typeof document === "undefined") {
760
- return;
761
- }
762
- if (document.fullscreenElement) {
763
- await document.exitFullscreen?.();
764
- return;
765
- }
766
- await rootRef.current?.requestFullscreen?.();
767
- }
2379
+ toggleFullscreen
768
2380
  }),
769
- []
2381
+ [toggleFullscreen]
770
2382
  );
771
- const rootStyle = useMemo2(
2383
+ const rootStyle = useMemo4(
772
2384
  () => ({
773
2385
  overflow: "hidden",
774
2386
  position: "relative",
@@ -776,7 +2388,7 @@ var PanoView = forwardRef2(
776
2388
  }),
777
2389
  [style]
778
2390
  );
779
- return /* @__PURE__ */ jsxs2(
2391
+ return /* @__PURE__ */ jsxs3(
780
2392
  "div",
781
2393
  {
782
2394
  ...divProps,
@@ -784,7 +2396,7 @@ var PanoView = forwardRef2(
784
2396
  "aria-label": ariaLabel,
785
2397
  style: rootStyle,
786
2398
  children: [
787
- /* @__PURE__ */ jsx3(
2399
+ /* @__PURE__ */ jsx4(
788
2400
  Canvas,
789
2401
  {
790
2402
  "aria-label": `${ariaLabel} canvas`,
@@ -804,41 +2416,71 @@ var PanoView = forwardRef2(
804
2416
  },
805
2417
  style: {
806
2418
  ...DEFAULT_CANVAS_STYLE,
807
- touchAction: controlsEnabled ? "none" : "auto"
2419
+ touchAction: userControlsEnabled ? "none" : "auto"
808
2420
  },
809
- tabIndex: controlsEnabled ? 0 : void 0,
810
- children: /* @__PURE__ */ jsx3(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ jsxs2(PanoramaControlsContext.Provider, { value: controlsRef, children: [
811
- /* @__PURE__ */ jsx3(
812
- PanoramaControls,
813
- {
814
- ref: controlsRef,
815
- enabled: controlsEnabled,
816
- initialView: normalizedInitialView,
817
- maxFov: normalizedMaxFov,
818
- minFov: normalizedMinFov,
819
- onViewChange,
820
- options: controlOptions
821
- }
822
- ),
823
- /* @__PURE__ */ jsx3(
824
- AutoRotate,
825
- {
826
- enabled: legacyAutoRotate,
827
- speed: legacyAutoRotateOptions.autoRotateSpeed
828
- }
829
- ),
830
- /* @__PURE__ */ jsx3(
831
- PanoramaEventSurface,
832
- {
833
- onClick: onPanoramaClick,
834
- onDoubleClick: onPanoramaDoubleClick,
835
- onPointerMove: onPanoramaPointerMove
836
- }
837
- ),
838
- children
839
- ] }) })
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
+ ) }) }) })
840
2481
  }
841
2482
  ),
2483
+ contextMenuOverlay,
842
2484
  hotspotAccessibilityControls
843
2485
  ]
844
2486
  }
@@ -847,8 +2489,8 @@ var PanoView = forwardRef2(
847
2489
  );
848
2490
 
849
2491
  // src/sphere.tsx
850
- import { useThree as useThree2 } from "@react-three/fiber";
851
- 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";
852
2494
  import {
853
2495
  BackSide as BackSide2,
854
2496
  LinearFilter,
@@ -858,7 +2500,7 @@ import {
858
2500
  SRGBColorSpace,
859
2501
  TextureLoader
860
2502
  } from "three";
861
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2503
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
862
2504
  function Sphere({
863
2505
  src,
864
2506
  yawOffset = 0,
@@ -866,13 +2508,13 @@ function Sphere({
866
2508
  onError,
867
2509
  visible = true
868
2510
  }) {
869
- const gl = useThree2((state) => state.gl);
870
- const [texture, setTexture] = useState3(null);
871
- const onLoadRef = useRef5(onLoad);
872
- 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);
873
2515
  onLoadRef.current = onLoad;
874
2516
  onErrorRef.current = onError;
875
- useEffect5(() => {
2517
+ useEffect11(() => {
876
2518
  let active = true;
877
2519
  const loader = new TextureLoader();
878
2520
  setTexture(null);
@@ -906,7 +2548,7 @@ function Sphere({
906
2548
  active = false;
907
2549
  };
908
2550
  }, [gl, src]);
909
- useEffect5(
2551
+ useEffect11(
910
2552
  () => () => {
911
2553
  texture?.dispose();
912
2554
  },
@@ -915,14 +2557,14 @@ function Sphere({
915
2557
  if (!texture) {
916
2558
  return null;
917
2559
  }
918
- return /* @__PURE__ */ jsxs3(
2560
+ return /* @__PURE__ */ jsxs4(
919
2561
  "mesh",
920
2562
  {
921
2563
  rotation: [0, MathUtils3.degToRad(90 + yawOffset), 0],
922
2564
  visible,
923
2565
  children: [
924
- /* @__PURE__ */ jsx4("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
925
- /* @__PURE__ */ jsx4(
2566
+ /* @__PURE__ */ jsx5("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
2567
+ /* @__PURE__ */ jsx5(
926
2568
  "meshBasicMaterial",
927
2569
  {
928
2570
  map: texture,
@@ -936,15 +2578,15 @@ function Sphere({
936
2578
  }
937
2579
 
938
2580
  // src/panorama-scenes.tsx
939
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
2581
+ import { useFrame as useFrame6, useThree as useThree10 } from "@react-three/fiber";
940
2582
  import {
941
2583
  Fragment,
942
- useCallback as useCallback2,
943
- useContext as useContext4,
944
- useEffect as useEffect8,
945
- useMemo as useMemo5,
946
- useRef as useRef7,
947
- 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
948
2590
  } from "react";
949
2591
  import {
950
2592
  DoubleSide,
@@ -952,17 +2594,17 @@ import {
952
2594
  LinearFilter as LinearFilter4,
953
2595
  PlaneGeometry as PlaneGeometry2,
954
2596
  ShaderMaterial,
955
- Vector3 as Vector34,
2597
+ Vector3 as Vector35,
956
2598
  WebGLRenderTarget
957
2599
  } from "three";
958
2600
 
959
2601
  // src/tile/texture-manager-context.tsx
960
- import { useThree as useThree3 } from "@react-three/fiber";
2602
+ import { useThree as useThree8 } from "@react-three/fiber";
961
2603
  import {
962
- createContext as createContext3,
963
- useContext as useContext3,
964
- useEffect as useEffect6,
965
- useMemo as useMemo3
2604
+ createContext as createContext5,
2605
+ useContext as useContext9,
2606
+ useEffect as useEffect12,
2607
+ useMemo as useMemo5
966
2608
  } from "react";
967
2609
 
968
2610
  // src/tile/texture-manager.ts
@@ -1192,8 +2834,8 @@ var TileTextureManager = class {
1192
2834
  };
1193
2835
 
1194
2836
  // src/tile/texture-manager-context.tsx
1195
- import { jsx as jsx5 } from "react/jsx-runtime";
1196
- var TileTextureManagerContext = createContext3(
2837
+ import { jsx as jsx6 } from "react/jsx-runtime";
2838
+ var TileTextureManagerContext = createContext5(
1197
2839
  null
1198
2840
  );
1199
2841
  function TileTextureManagerProvider({
@@ -1201,9 +2843,9 @@ function TileTextureManagerProvider({
1201
2843
  maxTextureMemoryMb = 128,
1202
2844
  maxConcurrentLoads = 4
1203
2845
  }) {
1204
- const gl = useThree3((state) => state.gl);
2846
+ const gl = useThree8((state) => state.gl);
1205
2847
  const anisotropy = gl.capabilities.getMaxAnisotropy();
1206
- const manager = useMemo3(
2848
+ const manager = useMemo5(
1207
2849
  () => new TileTextureManager({
1208
2850
  anisotropy,
1209
2851
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1212,24 +2854,24 @@ function TileTextureManagerProvider({
1212
2854
  }),
1213
2855
  [anisotropy, maxConcurrentLoads, maxTextureMemoryMb]
1214
2856
  );
1215
- useEffect6(() => {
2857
+ useEffect12(() => {
1216
2858
  manager.resume();
1217
2859
  return () => manager.dispose();
1218
2860
  }, [manager]);
1219
- return /* @__PURE__ */ jsx5(TileTextureManagerContext.Provider, { value: manager, children });
2861
+ return /* @__PURE__ */ jsx6(TileTextureManagerContext.Provider, { value: manager, children });
1220
2862
  }
1221
2863
  function useSharedTileTextureManager() {
1222
- return useContext3(TileTextureManagerContext);
2864
+ return useContext9(TileTextureManagerContext);
1223
2865
  }
1224
2866
 
1225
2867
  // src/tile/tile.tsx
1226
- import { useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
2868
+ import { useFrame as useFrame5, useThree as useThree9 } from "@react-three/fiber";
1227
2869
  import {
1228
2870
  memo,
1229
- useEffect as useEffect7,
1230
- useMemo as useMemo4,
1231
- useRef as useRef6,
1232
- useState as useState4
2871
+ useEffect as useEffect13,
2872
+ useMemo as useMemo6,
2873
+ useRef as useRef11,
2874
+ useState as useState5
1233
2875
  } from "react";
1234
2876
  import {
1235
2877
  ClampToEdgeWrapping as ClampToEdgeWrapping2,
@@ -1240,7 +2882,7 @@ import {
1240
2882
  PerspectiveCamera as PerspectiveCamera2,
1241
2883
  SRGBColorSpace as SRGBColorSpace3,
1242
2884
  TextureLoader as TextureLoader3,
1243
- Vector3 as Vector33
2885
+ Vector3 as Vector34
1244
2886
  } from "three";
1245
2887
 
1246
2888
  // src/tile/face-layout.ts
@@ -1248,7 +2890,7 @@ import {
1248
2890
  Box3,
1249
2891
  Euler as Euler2,
1250
2892
  Quaternion,
1251
- Vector3 as Vector32
2893
+ Vector3 as Vector33
1252
2894
  } from "three";
1253
2895
  var CUBE_FACES = ["f", "r", "b", "l", "u", "d"];
1254
2896
  var PREVIEW_FACE_ORDER = ["l", "f", "r", "b", "u", "d"];
@@ -1312,12 +2954,12 @@ function getTileBoundingBox(address, radius = CUBE_RADIUS) {
1312
2954
  const halfWidth = local.width / 2;
1313
2955
  const halfHeight = local.height / 2;
1314
2956
  const quaternion = getFaceQuaternion(address.face);
1315
- const position = new Vector32(...transform.position);
2957
+ const position = new Vector33(...transform.position);
1316
2958
  const corners = [
1317
- new Vector32(local.x - halfWidth, local.y - halfHeight, 0),
1318
- new Vector32(local.x + halfWidth, local.y - halfHeight, 0),
1319
- new Vector32(local.x - halfWidth, local.y + halfHeight, 0),
1320
- 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)
1321
2963
  ];
1322
2964
  for (const corner of corners) {
1323
2965
  corner.applyQuaternion(quaternion).add(position);
@@ -1426,7 +3068,7 @@ function resolveTemplateUrl(template, address) {
1426
3068
  }
1427
3069
 
1428
3070
  // src/tile/tile.tsx
1429
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
3071
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1430
3072
  var MAX_LOD_UPDATE_INTERVAL_MS = 100;
1431
3073
  var FAST_ROTATION_THRESHOLD_RADIANS = Math.PI / 180;
1432
3074
  var FAST_FOV_THRESHOLD_DEGREES = 0.5;
@@ -1435,10 +3077,10 @@ var VISIBILITY_FOV_PADDING_DEGREES = 8;
1435
3077
  var PREVIEW_RADIUS = CUBE_RADIUS + 0.05;
1436
3078
  var SHARED_TILE_GEOMETRY = new PlaneGeometry(1, 1);
1437
3079
  function usePreviewTexture(url, anisotropy, onError) {
1438
- const [texture, setTexture] = useState4(null);
1439
- const onErrorRef = useRef6(onError);
3080
+ const [texture, setTexture] = useState5(null);
3081
+ const onErrorRef = useRef11(onError);
1440
3082
  onErrorRef.current = onError;
1441
- useEffect7(() => {
3083
+ useEffect13(() => {
1442
3084
  if (!url) {
1443
3085
  setTexture(null);
1444
3086
  return;
@@ -1475,7 +3117,7 @@ function usePreviewTexture(url, anisotropy, onError) {
1475
3117
  active = false;
1476
3118
  };
1477
3119
  }, [anisotropy, url]);
1478
- useEffect7(
3120
+ useEffect13(
1479
3121
  () => () => {
1480
3122
  texture?.dispose();
1481
3123
  },
@@ -1488,7 +3130,7 @@ function PreviewFace({
1488
3130
  texture,
1489
3131
  faceOrder
1490
3132
  }) {
1491
- const geometry = useMemo4(() => {
3133
+ const geometry = useMemo6(() => {
1492
3134
  const nextGeometry = new PlaneGeometry(
1493
3135
  PREVIEW_RADIUS * 2,
1494
3136
  PREVIEW_RADIUS * 2
@@ -1506,21 +3148,21 @@ function PreviewFace({
1506
3148
  uv.needsUpdate = true;
1507
3149
  return nextGeometry;
1508
3150
  }, [face, faceOrder]);
1509
- useEffect7(
3151
+ useEffect13(
1510
3152
  () => () => {
1511
3153
  geometry.dispose();
1512
3154
  },
1513
3155
  [geometry]
1514
3156
  );
1515
3157
  const transform = getFaceTransform(face, PREVIEW_RADIUS);
1516
- return /* @__PURE__ */ jsx6(
3158
+ return /* @__PURE__ */ jsx7(
1517
3159
  "mesh",
1518
3160
  {
1519
3161
  geometry,
1520
3162
  position: transform.position,
1521
3163
  renderOrder: -100,
1522
3164
  rotation: transform.rotation,
1523
- children: /* @__PURE__ */ jsx6(
3165
+ children: /* @__PURE__ */ jsx7(
1524
3166
  "meshBasicMaterial",
1525
3167
  {
1526
3168
  depthTest: false,
@@ -1536,7 +3178,7 @@ function PreviewCube({
1536
3178
  texture,
1537
3179
  faceOrder
1538
3180
  }) {
1539
- return CUBE_FACES.map((face) => /* @__PURE__ */ jsx6(
3181
+ return CUBE_FACES.map((face) => /* @__PURE__ */ jsx7(
1540
3182
  PreviewFace,
1541
3183
  {
1542
3184
  face,
@@ -1547,14 +3189,14 @@ function PreviewCube({
1547
3189
  ));
1548
3190
  }
1549
3191
  function useManagedTexture(manager, key, url, address) {
1550
- const [managedState, setManagedState] = useState4(() => ({
3192
+ const [managedState, setManagedState] = useState5(() => ({
1551
3193
  manager,
1552
3194
  key,
1553
3195
  url,
1554
3196
  snapshot: manager.getSnapshot(key)
1555
3197
  }));
1556
3198
  const snapshot = managedState.manager === manager && managedState.key === key && managedState.url === url ? managedState.snapshot : manager.getSnapshot(key);
1557
- useEffect7(
3199
+ useEffect13(
1558
3200
  () => manager.acquire(key, url, address, (nextSnapshot) => {
1559
3201
  setManagedState({
1560
3202
  manager,
@@ -1581,14 +3223,14 @@ var TileMesh = memo(function TileMesh2({
1581
3223
  if (!snapshot.texture) {
1582
3224
  return null;
1583
3225
  }
1584
- 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(
1585
3227
  "mesh",
1586
3228
  {
1587
3229
  geometry: SHARED_TILE_GEOMETRY,
1588
3230
  position: [layout.x, layout.y, 0],
1589
3231
  renderOrder,
1590
3232
  scale: [layout.width, layout.height, 1],
1591
- children: /* @__PURE__ */ jsx6(
3233
+ children: /* @__PURE__ */ jsx7(
1592
3234
  "meshBasicMaterial",
1593
3235
  {
1594
3236
  depthTest: false,
@@ -1605,7 +3247,7 @@ var TileLayerMeshes = memo(function TileLayerMeshes2({
1605
3247
  manager,
1606
3248
  resolveUrl
1607
3249
  }) {
1608
- return layer.addresses.map((address) => /* @__PURE__ */ jsx6(
3250
+ return layer.addresses.map((address) => /* @__PURE__ */ jsx7(
1609
3251
  TileMesh,
1610
3252
  {
1611
3253
  address,
@@ -1705,8 +3347,8 @@ function parentAddresses(child, parentLevel, parentFaceSize, tileSize) {
1705
3347
  return addresses;
1706
3348
  }
1707
3349
  function centerFacingAddress(candidates, camera) {
1708
- const direction = camera.getWorldDirection(new Vector33());
1709
- const center = new Vector33();
3350
+ const direction = camera.getWorldDirection(new Vector34());
3351
+ const center = new Vector34();
1710
3352
  let bestCandidate = candidates[0];
1711
3353
  let bestAlignment = -Infinity;
1712
3354
  for (const candidate of candidates) {
@@ -1773,11 +3415,11 @@ function Tile({
1773
3415
  onTileError,
1774
3416
  onLevelChange
1775
3417
  }) {
1776
- const { camera, gl, size } = useThree4();
1777
- const multiresConfig = useMemo4(() => parseMultires(multires), [multires]);
3418
+ const { camera, gl, size } = useThree9();
3419
+ const multiresConfig = useMemo6(() => parseMultires(multires), [multires]);
1778
3420
  const { levels, tileSize } = multiresConfig;
1779
3421
  const multiresToken = `${tileSize}:${levels.join(",")}`;
1780
- const candidatesByLevel = useMemo4(
3422
+ const candidatesByLevel = useMemo6(
1781
3423
  () => buildTileCandidates(levels, tileSize),
1782
3424
  [levels, tileSize]
1783
3425
  );
@@ -1789,13 +3431,13 @@ function Tile({
1789
3431
  onPreviewError
1790
3432
  );
1791
3433
  const sharedManager = useSharedTileTextureManager();
1792
- const progressRef = useRef6(onLoadProgress);
1793
- const errorRef = useRef6(onTileError);
1794
- const levelChangeRef = useRef6(onLevelChange);
3434
+ const progressRef = useRef11(onLoadProgress);
3435
+ const errorRef = useRef11(onTileError);
3436
+ const levelChangeRef = useRef11(onLevelChange);
1795
3437
  progressRef.current = onLoadProgress;
1796
3438
  errorRef.current = onTileError;
1797
3439
  levelChangeRef.current = onLevelChange;
1798
- const ownedManager = useMemo4(
3440
+ const ownedManager = useMemo6(
1799
3441
  () => new TileTextureManager({
1800
3442
  anisotropy,
1801
3443
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1816,7 +3458,7 @@ function Tile({
1816
3458
  ]
1817
3459
  );
1818
3460
  const manager = sharedManager ?? ownedManager;
1819
- useEffect7(() => {
3461
+ useEffect13(() => {
1820
3462
  manager.resume();
1821
3463
  return () => {
1822
3464
  if (manager === ownedManager) {
@@ -1826,11 +3468,11 @@ function Tile({
1826
3468
  queueMicrotask(() => manager.releaseUnused());
1827
3469
  };
1828
3470
  }, [manager, ownedManager]);
1829
- const readyRef = useRef6(false);
1830
- useEffect7(() => {
3471
+ const readyRef = useRef11(false);
3472
+ useEffect13(() => {
1831
3473
  readyRef.current = false;
1832
3474
  }, [effectivePreviewUrl]);
1833
- useEffect7(() => {
3475
+ useEffect13(() => {
1834
3476
  if (!previewTexture || readyRef.current) {
1835
3477
  return;
1836
3478
  }
@@ -1838,31 +3480,31 @@ function Tile({
1838
3480
  onReady?.();
1839
3481
  }, [onReady, previewTexture]);
1840
3482
  const template = urlTemplate ?? buildDefaultTileUrlTemplate();
1841
- const resolveUrl = useMemo4(
3483
+ const resolveUrl = useMemo6(
1842
3484
  () => (address) => {
1843
3485
  const relativePath = resolveTileUrl ? resolveTileUrl(address) : resolveTemplateUrl(template, address);
1844
3486
  return resolveRelativeTileUrl(baseUrl, relativePath);
1845
3487
  },
1846
3488
  [baseUrl, resolveTileUrl, template]
1847
3489
  );
1848
- const [layers, setLayers] = useState4([]);
1849
- const layerTokenRef = useRef6("");
1850
- const currentLevelRef = useRef6(1);
1851
- const lastUpdateAtRef = useRef6(-Infinity);
1852
- const visibilityDirtyRef = useRef6(true);
1853
- const lastVisibilityQuaternionRef = useRef6(camera.quaternion.clone());
1854
- const lastVisibilityFovRef = useRef6(Number.NaN);
1855
- const lastViewportRef = useRef6({ width: 0, height: 0, dpr: 0 });
1856
- const frustumRef = useRef6(new Frustum());
1857
- const projectionMatrixRef = useRef6(new Matrix4());
1858
- const visibilityCameraRef = useRef6(new PerspectiveCamera2());
1859
- 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(() => {
1860
3502
  currentLevelRef.current = 1;
1861
3503
  layerTokenRef.current = "";
1862
3504
  visibilityDirtyRef.current = true;
1863
3505
  setLayers([]);
1864
3506
  }, [manager, multiresToken]);
1865
- useEffect7(() => {
3507
+ useEffect13(() => {
1866
3508
  if (loadMode !== "preview") {
1867
3509
  return;
1868
3510
  }
@@ -1876,7 +3518,7 @@ function Tile({
1876
3518
  setLayers(nextLayers);
1877
3519
  }
1878
3520
  };
1879
- useFrame3(() => {
3521
+ useFrame5(() => {
1880
3522
  if (loadMode === "preview") {
1881
3523
  return;
1882
3524
  }
@@ -1942,15 +3584,15 @@ function Tile({
1942
3584
  };
1943
3585
  commitLayers(nextLayers);
1944
3586
  });
1945
- return /* @__PURE__ */ jsxs4("group", { visible, children: [
1946
- previewTexture ? /* @__PURE__ */ jsx6(
3587
+ return /* @__PURE__ */ jsxs5("group", { visible, children: [
3588
+ previewTexture ? /* @__PURE__ */ jsx7(
1947
3589
  PreviewCube,
1948
3590
  {
1949
3591
  texture: previewTexture,
1950
3592
  faceOrder: previewFaceOrder
1951
3593
  }
1952
3594
  ) : null,
1953
- layers.map((layer) => /* @__PURE__ */ jsx6(
3595
+ layers.map((layer) => /* @__PURE__ */ jsx7(
1954
3596
  TileLayerMeshes,
1955
3597
  {
1956
3598
  layer,
@@ -1963,7 +3605,7 @@ function Tile({
1963
3605
  }
1964
3606
 
1965
3607
  // src/panorama-scenes.tsx
1966
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
3608
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1967
3609
  var TRANSITION_DEFAULTS = {
1968
3610
  none: { duration: 0, krpanoBlend: "NOBLEND" },
1969
3611
  crossfade: { duration: 1, krpanoBlend: "BLEND(1.0, easeInCubic)" },
@@ -2013,7 +3655,7 @@ function SceneSource({
2013
3655
  return null;
2014
3656
  }
2015
3657
  if (scene.type === "sphere") {
2016
- return /* @__PURE__ */ jsx7(
3658
+ return /* @__PURE__ */ jsx8(
2017
3659
  Sphere,
2018
3660
  {
2019
3661
  src: scene.src,
@@ -2025,7 +3667,7 @@ function SceneSource({
2025
3667
  );
2026
3668
  }
2027
3669
  const { id: _id, type: _type, ...tileProps } = scene;
2028
- return /* @__PURE__ */ jsx7(
3670
+ return /* @__PURE__ */ jsx8(
2029
3671
  Tile,
2030
3672
  {
2031
3673
  ...tileProps,
@@ -2041,17 +3683,17 @@ function SnapshotCapture({
2041
3683
  maxPixels,
2042
3684
  onCaptured
2043
3685
  }) {
2044
- const { camera, gl, scene, size } = useThree5();
2045
- const capturedRequestRef = useRef7(0);
2046
- const renderTargetRef = useRef7(null);
2047
- useEffect8(
3686
+ const { camera, gl, scene, size } = useThree10();
3687
+ const capturedRequestRef = useRef12(0);
3688
+ const renderTargetRef = useRef12(null);
3689
+ useEffect14(
2048
3690
  () => () => {
2049
3691
  renderTargetRef.current?.dispose();
2050
3692
  renderTargetRef.current = null;
2051
3693
  },
2052
3694
  []
2053
3695
  );
2054
- useFrame4(() => {
3696
+ useFrame6(() => {
2055
3697
  if (requestId === 0 || requestId === capturedRequestRef.current) {
2056
3698
  return;
2057
3699
  }
@@ -2097,12 +3739,12 @@ function SnapshotOverlay({
2097
3739
  runId,
2098
3740
  onFinish
2099
3741
  }) {
2100
- const { camera, size } = useThree5();
2101
- const meshRef = useRef7(null);
2102
- const startRef = useRef7(null);
2103
- const finishedRef = useRef7(false);
2104
- const directionRef = useRef7(new Vector34());
2105
- 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(
2106
3748
  () => new ShaderMaterial({
2107
3749
  transparent: true,
2108
3750
  depthTest: false,
@@ -2181,13 +3823,13 @@ function SnapshotOverlay({
2181
3823
  }),
2182
3824
  [snapshot.texture, transition.preset]
2183
3825
  );
2184
- useEffect8(() => () => material.dispose(), [material]);
2185
- useEffect8(() => {
3826
+ useEffect14(() => () => material.dispose(), [material]);
3827
+ useEffect14(() => {
2186
3828
  startRef.current = null;
2187
3829
  finishedRef.current = false;
2188
3830
  material.uniforms.progress.value = 0;
2189
3831
  }, [material, runId]);
2190
- useFrame4((state) => {
3832
+ useFrame6((state) => {
2191
3833
  const mesh = meshRef.current;
2192
3834
  if (!mesh || !camera.isPerspectiveCamera) {
2193
3835
  return;
@@ -2212,9 +3854,9 @@ function SnapshotOverlay({
2212
3854
  onFinish();
2213
3855
  }
2214
3856
  });
2215
- return /* @__PURE__ */ jsxs5("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
2216
- /* @__PURE__ */ jsx7("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
2217
- /* @__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 })
2218
3860
  ] });
2219
3861
  }
2220
3862
  var FULLSCREEN_PLANE = new PlaneGeometry2(1, 1);
@@ -2227,47 +3869,47 @@ function PanoramaScenesController({
2227
3869
  onTransitionError,
2228
3870
  snapshotMaxPixels = 3686400
2229
3871
  }) {
2230
- const controlsRef = useContext4(PanoramaControlsContext);
3872
+ const controlsRef = useContext10(PanoramaViewContext);
2231
3873
  const manager = useSharedTileTextureManager();
2232
- const transition = useMemo5(
3874
+ const transition = useMemo7(
2233
3875
  () => resolveTransition(transitionInput),
2234
3876
  [transitionInput]
2235
3877
  );
2236
- const sceneMap = useMemo5(
3878
+ const sceneMap = useMemo7(
2237
3879
  () => new Map(scenes.map((scene) => [scene.id, scene])),
2238
3880
  [scenes]
2239
3881
  );
2240
3882
  const initialScene = sceneMap.get(activeSceneId) ?? null;
2241
- const [slots, setSlots] = useState5([
3883
+ const [slots, setSlots] = useState6([
2242
3884
  initialScene,
2243
3885
  null
2244
3886
  ]);
2245
- const [activeSlot, setActiveSlot] = useState5(0);
2246
- const [liveSlot, setLiveSlot] = useState5(0);
2247
- const [pendingSlot, setPendingSlot] = useState5(null);
2248
- const [phase, setPhase] = useState5(initialScene ? "idle" : "preloading");
2249
- const [captureRequestId, setCaptureRequestId] = useState5(0);
2250
- const [transitionRunId, setTransitionRunId] = useState5(0);
2251
- const [snapshot, setSnapshot] = useState5(null);
2252
- const [recoveryKey, setRecoveryKey] = useState5(0);
2253
- const requestedSceneIdRef = useRef7(activeSceneId);
2254
- const captureReasonRef = useRef7("start");
2255
- const interactionReleaseRef = useRef7(null);
2256
- const slotsRef = useRef7(slots);
2257
- const phaseRef = useRef7(phase);
2258
- const liveSlotRef = useRef7(liveSlot);
2259
- 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);
2260
3902
  slotsRef.current = slots;
2261
3903
  phaseRef.current = phase;
2262
3904
  liveSlotRef.current = liveSlot;
2263
- const releaseInteraction = useCallback2(() => {
3905
+ const releaseInteraction = useCallback4(() => {
2264
3906
  interactionReleaseRef.current?.();
2265
3907
  interactionReleaseRef.current = null;
2266
3908
  }, []);
2267
- const reportError = useCallback2((sceneId, error) => {
3909
+ const reportError = useCallback4((sceneId, error) => {
2268
3910
  onTransitionError?.({ sceneId, error });
2269
3911
  }, [onTransitionError]);
2270
- const startPreload = useCallback2((scene) => {
3912
+ const startPreload = useCallback4((scene) => {
2271
3913
  const nextSlot = 1 - liveSlotRef.current;
2272
3914
  requestedSceneIdRef.current = scene.id;
2273
3915
  setSlots((current) => {
@@ -2278,7 +3920,7 @@ function PanoramaScenesController({
2278
3920
  setPendingSlot(nextSlot);
2279
3921
  setPhase("preloading");
2280
3922
  }, []);
2281
- useEffect8(() => {
3923
+ useEffect14(() => {
2282
3924
  const requested = sceneMap.get(activeSceneId);
2283
3925
  if (!requested) {
2284
3926
  reportError(activeSceneId, new Error(`Unknown panorama scene: ${activeSceneId}`));
@@ -2302,7 +3944,7 @@ function PanoramaScenesController({
2302
3944
  }
2303
3945
  startPreload(requested);
2304
3946
  }, [activeSceneId, pendingSlot, reportError, sceneMap, startPreload]);
2305
- const handleReady = useCallback2((slot) => {
3947
+ const handleReady = useCallback4((slot) => {
2306
3948
  if (slot !== pendingSlot || phaseRef.current !== "preloading") {
2307
3949
  return;
2308
3950
  }
@@ -2333,7 +3975,7 @@ function PanoramaScenesController({
2333
3975
  setPhase("capturing");
2334
3976
  setCaptureRequestId((current) => current + 1);
2335
3977
  }, [controlsRef, onTransitionEnd, pendingSlot, transition.preset]);
2336
- const handleCaptured = useCallback2((nextSnapshot) => {
3978
+ const handleCaptured = useCallback4((nextSnapshot) => {
2337
3979
  setSnapshot((current) => {
2338
3980
  current?.dispose();
2339
3981
  return nextSnapshot;
@@ -2355,7 +3997,7 @@ function PanoramaScenesController({
2355
3997
  setPhase("transitioning");
2356
3998
  setTransitionRunId((current) => current + 1);
2357
3999
  }, [manager, pendingSlot, sceneMap, startPreload]);
2358
- const finishTransition = useCallback2(() => {
4000
+ const finishTransition = useCallback4(() => {
2359
4001
  const nextSlot = pendingSlot;
2360
4002
  const previousScene = transitionFromRef.current;
2361
4003
  const nextScene = nextSlot === null ? null : slotsRef.current[nextSlot];
@@ -2380,12 +4022,12 @@ function PanoramaScenesController({
2380
4022
  });
2381
4023
  }
2382
4024
  }, [onTransitionEnd, pendingSlot, releaseInteraction, snapshot, transition.preset]);
2383
- useEffect8(() => () => {
4025
+ useEffect14(() => () => {
2384
4026
  releaseInteraction();
2385
4027
  snapshot?.dispose();
2386
4028
  }, [releaseInteraction, snapshot]);
2387
- const { gl } = useThree5();
2388
- useEffect8(() => {
4029
+ const { gl } = useThree10();
4030
+ useEffect14(() => {
2389
4031
  const canvas = gl.domElement;
2390
4032
  const onContextLost = (event) => {
2391
4033
  event.preventDefault();
@@ -2409,8 +4051,8 @@ function PanoramaScenesController({
2409
4051
  }, [gl, manager, releaseInteraction, snapshot]);
2410
4052
  const renderedScene = slots[liveSlot];
2411
4053
  const snapshotVisible = snapshot !== null && (phase === "transitioning" || phase === "recapturing" || phase === "preloading");
2412
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
2413
- /* @__PURE__ */ jsx7(
4054
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
4055
+ /* @__PURE__ */ jsx8(
2414
4056
  SnapshotCapture,
2415
4057
  {
2416
4058
  requestId: captureRequestId,
@@ -2422,7 +4064,7 @@ function PanoramaScenesController({
2422
4064
  const source = slots[slot];
2423
4065
  const isLive = slot === liveSlot;
2424
4066
  const isPending = slot === pendingSlot;
2425
- return /* @__PURE__ */ jsx7(
4067
+ return /* @__PURE__ */ jsx8(
2426
4068
  SceneSource,
2427
4069
  {
2428
4070
  scene: source,
@@ -2438,7 +4080,7 @@ function PanoramaScenesController({
2438
4080
  `${slot}:${source?.id ?? "empty"}:${recoveryKey}`
2439
4081
  );
2440
4082
  }),
2441
- snapshotVisible ? /* @__PURE__ */ jsx7(
4083
+ snapshotVisible ? /* @__PURE__ */ jsx8(
2442
4084
  SnapshotOverlay,
2443
4085
  {
2444
4086
  snapshot,
@@ -2456,19 +4098,19 @@ function PanoramaScenes({
2456
4098
  maxConcurrentTileLoads,
2457
4099
  ...props
2458
4100
  }) {
2459
- return /* @__PURE__ */ jsx7(
4101
+ return /* @__PURE__ */ jsx8(
2460
4102
  TileTextureManagerProvider,
2461
4103
  {
2462
4104
  maxTextureMemoryMb,
2463
4105
  maxConcurrentLoads: maxConcurrentTileLoads,
2464
- children: /* @__PURE__ */ jsx7(PanoramaScenesController, { ...props })
4106
+ children: /* @__PURE__ */ jsx8(PanoramaScenesController, { ...props })
2465
4107
  }
2466
4108
  );
2467
4109
  }
2468
4110
 
2469
4111
  // src/hotspot/image-hotspot.tsx
2470
- import { useThree as useThree6 } from "@react-three/fiber";
2471
- 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";
2472
4114
  import {
2473
4115
  ClampToEdgeWrapping as ClampToEdgeWrapping3,
2474
4116
  DoubleSide as DoubleSide2,
@@ -2479,12 +4121,12 @@ import {
2479
4121
  } from "three";
2480
4122
 
2481
4123
  // src/hotspot/hotspot-anchor.tsx
2482
- import { useFrame as useFrame5 } from "@react-three/fiber";
4124
+ import { useFrame as useFrame7 } from "@react-three/fiber";
2483
4125
  import {
2484
- useContext as useContext5,
2485
- useEffect as useEffect9,
2486
- useMemo as useMemo6,
2487
- useRef as useRef8
4126
+ useContext as useContext11,
4127
+ useEffect as useEffect15,
4128
+ useMemo as useMemo8,
4129
+ useRef as useRef13
2488
4130
  } from "react";
2489
4131
  import {
2490
4132
  EdgesGeometry,
@@ -2493,9 +4135,9 @@ import {
2493
4135
  PlaneGeometry as PlaneGeometry3,
2494
4136
  PerspectiveCamera as PerspectiveCamera4,
2495
4137
  Quaternion as Quaternion2,
2496
- Vector3 as Vector35
4138
+ Vector3 as Vector36
2497
4139
  } from "three";
2498
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4140
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2499
4141
  var DEFAULT_FLOATING_DISTANCE = 10;
2500
4142
  var PANORAMA_RADIUS = DEFAULT_PANORAMA_RADIUS;
2501
4143
  var PANORAMA_SURFACE_INSET = 4;
@@ -2503,7 +4145,7 @@ var DEFAULT_REFERENCE_FOV = 75;
2503
4145
  var MIN_ANGULAR_SIZE = 0.01;
2504
4146
  var MAX_ANGULAR_SIZE = 179;
2505
4147
  var DRAG_EPSILON_DEGREES = 1e-3;
2506
- var LOCAL_FORWARD = new Vector35(0, 0, 1);
4148
+ var LOCAL_FORWARD = new Vector36(0, 0, 1);
2507
4149
  var HOTSPOT_MODE_RENDERING = {
2508
4150
  surface: { orientation: "surface", placement: "surface" },
2509
4151
  billboard: { orientation: "billboard", placement: "floating" }
@@ -2552,7 +4194,7 @@ function createSurfaceQuaternion(position) {
2552
4194
  const inward = outward.clone().negate();
2553
4195
  const pitch = MathUtils4.degToRad(position.pitch);
2554
4196
  const yaw = MathUtils4.degToRad(position.yaw);
2555
- const localUp = new Vector35(
4197
+ const localUp = new Vector36(
2556
4198
  -Math.sin(pitch) * Math.sin(yaw),
2557
4199
  Math.cos(pitch),
2558
4200
  Math.sin(pitch) * Math.cos(yaw)
@@ -2601,10 +4243,10 @@ function HotspotAnchor({
2601
4243
  onPositionChange,
2602
4244
  onDragEnd
2603
4245
  }) {
2604
- const controlsRef = useContext5(PanoramaControlsContext);
2605
- const groupRef = useRef8(null);
2606
- const dragStateRef = useRef8(null);
2607
- const suppressNextClickRef = useRef8(false);
4246
+ const controlsRef = useContext11(PanoramaViewContext);
4247
+ const groupRef = useRef13(null);
4248
+ const dragStateRef = useRef13(null);
4249
+ const suppressNextClickRef = useRef13(false);
2608
4250
  const normalizedPosition = normalizePanoPosition(position);
2609
4251
  const rendering = internalOrientation && internalPlacement ? { orientation: internalOrientation, placement: internalPlacement } : HOTSPOT_MODE_RENDERING[mode];
2610
4252
  const { orientation: orientation2, placement } = rendering;
@@ -2617,17 +4259,17 @@ function HotspotAnchor({
2617
4259
  scaledWidth,
2618
4260
  scaledHeight
2619
4261
  );
2620
- const worldPosition = useMemo6(
4262
+ const worldPosition = useMemo8(
2621
4263
  () => panoPositionToVector3(normalizedPosition, resolvedDistance),
2622
4264
  [normalizedPosition.pitch, normalizedPosition.yaw, resolvedDistance]
2623
4265
  );
2624
- const surfaceQuaternion3 = useMemo6(
4266
+ const surfaceQuaternion3 = useMemo8(
2625
4267
  () => createSurfaceQuaternion(normalizedPosition),
2626
4268
  [normalizedPosition.pitch, normalizedPosition.yaw]
2627
4269
  );
2628
4270
  const worldWidth = angularSizeToWorldSize(scaledWidth, resolvedDistance);
2629
4271
  const worldHeight = angularSizeToWorldSize(scaledHeight, resolvedDistance);
2630
- const focusGeometry = useMemo6(() => {
4272
+ const focusGeometry = useMemo8(() => {
2631
4273
  const planeGeometry = new PlaneGeometry3(1, 1);
2632
4274
  const geometry = new EdgesGeometry(planeGeometry);
2633
4275
  planeGeometry.dispose();
@@ -2645,15 +4287,15 @@ function HotspotAnchor({
2645
4287
  });
2646
4288
  } : void 0
2647
4289
  });
2648
- useEffect9(
4290
+ useEffect15(
2649
4291
  () => () => {
2650
4292
  dragStateRef.current?.releaseInteractionLock();
2651
4293
  dragStateRef.current = null;
2652
4294
  },
2653
4295
  []
2654
4296
  );
2655
- useEffect9(() => () => focusGeometry.dispose(), [focusGeometry]);
2656
- useFrame5(({ camera }) => {
4297
+ useEffect15(() => () => focusGeometry.dispose(), [focusGeometry]);
4298
+ useFrame7(({ camera }) => {
2657
4299
  if (!groupRef.current) {
2658
4300
  return;
2659
4301
  }
@@ -2664,7 +4306,7 @@ function HotspotAnchor({
2664
4306
  }
2665
4307
  groupRef.current.rotateOnAxis(
2666
4308
  LOCAL_FORWARD,
2667
- MathUtils4.degToRad(rotation)
4309
+ MathUtils4.degToRad(-rotation)
2668
4310
  );
2669
4311
  const scaleFactor = scaleMode === "fixed" && camera instanceof PerspectiveCamera4 ? fixedScaleFactor(camera.fov, referenceFov) : 1;
2670
4312
  groupRef.current.scale.set(
@@ -2699,7 +4341,7 @@ function HotspotAnchor({
2699
4341
  dragState.releaseInteractionLock();
2700
4342
  dragStateRef.current = null;
2701
4343
  };
2702
- return /* @__PURE__ */ jsxs6(
4344
+ return /* @__PURE__ */ jsxs7(
2703
4345
  "group",
2704
4346
  {
2705
4347
  ref: groupRef,
@@ -2769,14 +4411,14 @@ function HotspotAnchor({
2769
4411
  } : void 0,
2770
4412
  onPointerUp: interactive ? (event) => releasePointer(event, true) : void 0,
2771
4413
  children: [
2772
- focused ? focusContent ?? /* @__PURE__ */ jsx8(
4414
+ focused ? focusContent ?? /* @__PURE__ */ jsx9(
2773
4415
  "lineSegments",
2774
4416
  {
2775
4417
  geometry: focusGeometry,
2776
4418
  position: [0, 0, 0.01],
2777
4419
  renderOrder: renderOrder + 1,
2778
4420
  scale: [1.12, 1.12, 1],
2779
- children: /* @__PURE__ */ jsx8("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
4421
+ children: /* @__PURE__ */ jsx9("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
2780
4422
  }
2781
4423
  ) : null,
2782
4424
  children
@@ -2786,7 +4428,7 @@ function HotspotAnchor({
2786
4428
  }
2787
4429
 
2788
4430
  // src/hotspot/image-hotspot.tsx
2789
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
4431
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2790
4432
  function clampOpacity(opacity) {
2791
4433
  if (!Number.isFinite(opacity)) {
2792
4434
  return 1;
@@ -2794,13 +4436,13 @@ function clampOpacity(opacity) {
2794
4436
  return Math.max(0, Math.min(opacity, 1));
2795
4437
  }
2796
4438
  function useHotspotImageTexture(src, onLoad, onError) {
2797
- const gl = useThree6((state) => state.gl);
2798
- const [texture, setTexture] = useState6(null);
2799
- const onLoadRef = useRef9(onLoad);
2800
- 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);
2801
4443
  onLoadRef.current = onLoad;
2802
4444
  onErrorRef.current = onError;
2803
- useEffect10(() => {
4445
+ useEffect16(() => {
2804
4446
  let active = true;
2805
4447
  setTexture(null);
2806
4448
  if (!src) {
@@ -2838,7 +4480,7 @@ function useHotspotImageTexture(src, onLoad, onError) {
2838
4480
  active = false;
2839
4481
  };
2840
4482
  }, [gl, src]);
2841
- useEffect10(
4483
+ useEffect16(
2842
4484
  () => () => {
2843
4485
  texture?.dispose();
2844
4486
  },
@@ -2856,9 +4498,9 @@ function ImageHotspot({
2856
4498
  ...anchorProps
2857
4499
  }) {
2858
4500
  const texture = useHotspotImageTexture(src, onLoad, onError);
2859
- return /* @__PURE__ */ jsx9(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs7("mesh", { children: [
2860
- /* @__PURE__ */ jsx9("planeGeometry", { args: [1, 1] }),
2861
- /* @__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(
2862
4504
  "meshBasicMaterial",
2863
4505
  {
2864
4506
  map: texture,
@@ -2872,8 +4514,8 @@ function ImageHotspot({
2872
4514
  }
2873
4515
 
2874
4516
  // src/hotspot/polygon-hotspot.tsx
2875
- import { useThree as useThree7 } from "@react-three/fiber";
2876
- 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";
2877
4519
  import {
2878
4520
  BufferGeometry,
2879
4521
  DoubleSide as DoubleSide3,
@@ -2881,13 +4523,13 @@ import {
2881
4523
  Matrix4 as Matrix43,
2882
4524
  Quaternion as Quaternion3,
2883
4525
  ShapeUtils,
2884
- Vector2 as Vector22,
2885
- Vector3 as Vector36
4526
+ Vector2 as Vector23,
4527
+ Vector3 as Vector37
2886
4528
  } from "three";
2887
4529
  import { Line2 } from "three/addons/lines/Line2.js";
2888
4530
  import { LineGeometry } from "three/addons/lines/LineGeometry.js";
2889
4531
  import { LineMaterial } from "three/addons/lines/LineMaterial.js";
2890
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
4532
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2891
4533
  var POLYGON_RADIUS = 990;
2892
4534
  var MAX_FILL_SEGMENT_DEGREES = 7;
2893
4535
  function cloneVertices(vertices) {
@@ -2922,7 +4564,7 @@ function intersects(a, b, c, d) {
2922
4564
  }
2923
4565
  function hasSelfIntersection(vertices) {
2924
4566
  const points = vertices.map(
2925
- (vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch)
4567
+ (vertex) => new Vector23(vertex.unwrappedYaw, vertex.pitch)
2926
4568
  );
2927
4569
  for (let index = 0; index < points.length; index += 1) {
2928
4570
  const nextIndex = (index + 1) % points.length;
@@ -2959,7 +4601,7 @@ function polygonContainsDirection(vertices, direction) {
2959
4601
  }
2960
4602
  function fitsHemisphere(vertices) {
2961
4603
  const directions = vertices.map((vertex) => panoPositionToVector3(vertex).normalize());
2962
- const candidates = [new Vector36()];
4604
+ const candidates = [new Vector37()];
2963
4605
  for (const direction of directions) {
2964
4606
  candidates[0].add(direction);
2965
4607
  candidates.push(direction.clone());
@@ -2983,7 +4625,7 @@ function validatePolygonVertices(vertices) {
2983
4625
  if (hasSelfIntersection(unwrapped)) {
2984
4626
  issues.push({ code: "self_intersection", message: "Polygon edges cannot cross." });
2985
4627
  }
2986
- 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))) {
2987
4629
  issues.push({ code: "contains_pole", message: "Polygons cannot contain a panorama pole." });
2988
4630
  }
2989
4631
  if (!fitsHemisphere(normalized)) {
@@ -2994,7 +4636,7 @@ function validatePolygonVertices(vertices) {
2994
4636
  function polygonCenter(vertices) {
2995
4637
  const sum = vertices.reduce(
2996
4638
  (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
2997
- new Vector36()
4639
+ new Vector37()
2998
4640
  );
2999
4641
  return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
3000
4642
  }
@@ -3003,7 +4645,7 @@ function surfaceQuaternion(position) {
3003
4645
  const inward = outward.clone().negate();
3004
4646
  const pitch = position.pitch * Math.PI / 180;
3005
4647
  const yaw = position.yaw * Math.PI / 180;
3006
- const localUp = new Vector36(
4648
+ const localUp = new Vector37(
3007
4649
  -Math.sin(pitch) * Math.sin(yaw),
3008
4650
  Math.cos(pitch),
3009
4651
  Math.sin(pitch) * Math.cos(yaw)
@@ -3016,7 +4658,7 @@ function makeWorldToLocal(center) {
3016
4658
  return new Matrix43().compose(
3017
4659
  panoPositionToVector3(center, POLYGON_RADIUS),
3018
4660
  surfaceQuaternion(center),
3019
- new Vector36(1, 1, 1)
4661
+ new Vector37(1, 1, 1)
3020
4662
  ).invert();
3021
4663
  }
3022
4664
  function projectedPoint(a, b, c, weightB, weightC, radius, worldToLocal) {
@@ -3071,7 +4713,7 @@ function addSubdividedTriangle(positions, indices, a, b, c, worldToLocal, steps)
3071
4713
  }
3072
4714
  function makeFillGeometry(vertices, center) {
3073
4715
  const unwrapped = unwrapPolygonVertices(vertices);
3074
- const contour = unwrapped.map((vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch));
4716
+ const contour = unwrapped.map((vertex) => new Vector23(vertex.unwrappedYaw, vertex.pitch));
3075
4717
  const triangles = ShapeUtils.triangulateShape(contour, []);
3076
4718
  const positions = [];
3077
4719
  const indices = [];
@@ -3160,38 +4802,38 @@ function PolygonHotspot({
3160
4802
  onInvalid,
3161
4803
  ...anchorProps
3162
4804
  }) {
3163
- const gl = useThree7((state) => state.gl);
3164
- const size = useThree7((state) => state.size);
3165
- 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]);
3166
4808
  const valid = issues.length === 0;
3167
- const center = useMemo7(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
3168
- const fillGeometry = useMemo7(
4809
+ const center = useMemo9(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
4810
+ const fillGeometry = useMemo9(
3169
4811
  () => valid && center ? makeFillGeometry(vertices, center) : null,
3170
4812
  [center, valid, vertices]
3171
4813
  );
3172
- const strokePositions = useMemo7(
4814
+ const strokePositions = useMemo9(
3173
4815
  () => valid && center ? makeStrokePositions(vertices, center) : null,
3174
4816
  [center, valid, vertices]
3175
4817
  );
3176
4818
  const resolvedStrokeOpacity = Math.max(0, Math.min(strokeOpacity, 1));
3177
4819
  const devicePixelRatio = gl.getPixelRatio();
3178
- const strokeLine = useMemo7(
4820
+ const strokeLine = useMemo9(
3179
4821
  () => strokePositions ? makeStrokeLine(strokePositions, stroke, strokeWidth * devicePixelRatio, resolvedStrokeOpacity) : null,
3180
4822
  [devicePixelRatio, resolvedStrokeOpacity, stroke, strokePositions, strokeWidth]
3181
4823
  );
3182
- const focusStrokeLine = useMemo7(
4824
+ const focusStrokeLine = useMemo9(
3183
4825
  () => strokePositions ? makeStrokeLine(strokePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3184
4826
  [devicePixelRatio, strokePositions, strokeWidth]
3185
4827
  );
3186
- const startVerticesRef = useRef10(null);
4828
+ const startVerticesRef = useRef15(null);
3187
4829
  const issuesKey = issues.map((issue) => issue.code).join(",");
3188
- useEffect11(() => {
4830
+ useEffect17(() => {
3189
4831
  if (!valid) onInvalid?.(issues);
3190
4832
  }, [issues, issuesKey, onInvalid, valid]);
3191
- useEffect11(() => () => fillGeometry?.dispose(), [fillGeometry]);
3192
- useEffect11(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
3193
- useEffect11(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
3194
- useEffect11(() => {
4833
+ useEffect17(() => () => fillGeometry?.dispose(), [fillGeometry]);
4834
+ useEffect17(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
4835
+ useEffect17(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
4836
+ useEffect17(() => {
3195
4837
  const width = gl.domElement.width;
3196
4838
  const height = gl.domElement.height;
3197
4839
  for (const line of [strokeLine, focusStrokeLine]) {
@@ -3210,7 +4852,7 @@ function PolygonHotspot({
3210
4852
  startPosition: event.startPosition
3211
4853
  });
3212
4854
  };
3213
- return /* @__PURE__ */ jsxs8(
4855
+ return /* @__PURE__ */ jsxs9(
3214
4856
  HotspotAnchor,
3215
4857
  {
3216
4858
  ...anchorProps,
@@ -3225,7 +4867,7 @@ function PolygonHotspot({
3225
4867
  draggable,
3226
4868
  interactive,
3227
4869
  useAngularScale: false,
3228
- focusContent: /* @__PURE__ */ jsx10("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
4870
+ focusContent: /* @__PURE__ */ jsx11("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
3229
4871
  onDragStart: (event) => {
3230
4872
  startVerticesRef.current = cloneVertices(vertices);
3231
4873
  emitVerticesChange(event, onDragStart);
@@ -3236,7 +4878,7 @@ function PolygonHotspot({
3236
4878
  startVerticesRef.current = null;
3237
4879
  },
3238
4880
  children: [
3239
- /* @__PURE__ */ jsx10("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ jsx10(
4881
+ /* @__PURE__ */ jsx11("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ jsx11(
3240
4882
  "meshBasicMaterial",
3241
4883
  {
3242
4884
  color: fill,
@@ -3246,25 +4888,25 @@ function PolygonHotspot({
3246
4888
  transparent: true
3247
4889
  }
3248
4890
  ) }),
3249
- /* @__PURE__ */ jsx10("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
4891
+ /* @__PURE__ */ jsx11("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
3250
4892
  ]
3251
4893
  }
3252
4894
  );
3253
4895
  }
3254
4896
 
3255
4897
  // src/hotspot/polyline-hotspot.tsx
3256
- import { useThree as useThree8 } from "@react-three/fiber";
3257
- 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";
3258
4900
  import {
3259
4901
  MathUtils as MathUtils5,
3260
4902
  Matrix4 as Matrix44,
3261
4903
  Quaternion as Quaternion4,
3262
- Vector3 as Vector37
4904
+ Vector3 as Vector38
3263
4905
  } from "three";
3264
4906
  import { Line2 as Line22 } from "three/addons/lines/Line2.js";
3265
4907
  import { LineGeometry as LineGeometry2 } from "three/addons/lines/LineGeometry.js";
3266
4908
  import { LineMaterial as LineMaterial2 } from "three/addons/lines/LineMaterial.js";
3267
- import { jsx as jsx11 } from "react/jsx-runtime";
4909
+ import { jsx as jsx12 } from "react/jsx-runtime";
3268
4910
  var POLYLINE_RADIUS = 990;
3269
4911
  var MAX_SEGMENT_DEGREES = 7;
3270
4912
  function cloneVertices2(vertices) {
@@ -3289,7 +4931,7 @@ function unwrapVertices(vertices) {
3289
4931
  function polylineCenter(vertices) {
3290
4932
  const sum = vertices.reduce(
3291
4933
  (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
3292
- new Vector37()
4934
+ new Vector38()
3293
4935
  );
3294
4936
  return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
3295
4937
  }
@@ -3298,7 +4940,7 @@ function surfaceQuaternion2(position) {
3298
4940
  const inward = outward.clone().negate();
3299
4941
  const pitch = MathUtils5.degToRad(position.pitch);
3300
4942
  const yaw = MathUtils5.degToRad(position.yaw);
3301
- const localUp = new Vector37(
4943
+ const localUp = new Vector38(
3302
4944
  -Math.sin(pitch) * Math.sin(yaw),
3303
4945
  Math.cos(pitch),
3304
4946
  Math.sin(pitch) * Math.cos(yaw)
@@ -3311,7 +4953,7 @@ function makeWorldToLocal2(center) {
3311
4953
  return new Matrix44().compose(
3312
4954
  panoPositionToVector3(center, POLYLINE_RADIUS),
3313
4955
  surfaceQuaternion2(center),
3314
- new Vector37(1, 1, 1)
4956
+ new Vector38(1, 1, 1)
3315
4957
  ).invert();
3316
4958
  }
3317
4959
  function subdivisionSteps(vertices) {
@@ -3401,32 +5043,32 @@ function PolylineHotspot({
3401
5043
  onInvalid,
3402
5044
  ...anchorProps
3403
5045
  }) {
3404
- const gl = useThree8((state) => state.gl);
3405
- const size = useThree8((state) => state.size);
3406
- 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]);
3407
5049
  const valid = issues.length === 0;
3408
- const center = useMemo8(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
3409
- const linePositions = useMemo8(
5050
+ const center = useMemo10(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
5051
+ const linePositions = useMemo10(
3410
5052
  () => valid && center ? makeLinePositions(vertices, center) : null,
3411
5053
  [center, valid, vertices]
3412
5054
  );
3413
5055
  const devicePixelRatio = gl.getPixelRatio();
3414
5056
  const opacity = Math.max(0, Math.min(strokeOpacity, 1));
3415
- const line = useMemo8(
5057
+ const line = useMemo10(
3416
5058
  () => linePositions ? makeLine(linePositions, stroke, strokeWidth * devicePixelRatio, opacity) : null,
3417
5059
  [devicePixelRatio, linePositions, opacity, stroke, strokeWidth]
3418
5060
  );
3419
- const focusLine = useMemo8(
5061
+ const focusLine = useMemo10(
3420
5062
  () => linePositions ? makeLine(linePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3421
5063
  [devicePixelRatio, linePositions, strokeWidth]
3422
5064
  );
3423
- const startVerticesRef = useRef11(null);
3424
- useEffect12(() => {
5065
+ const startVerticesRef = useRef16(null);
5066
+ useEffect18(() => {
3425
5067
  if (!valid) onInvalid?.(issues);
3426
5068
  }, [issues, onInvalid, valid]);
3427
- useEffect12(() => () => disposeLine(line), [line]);
3428
- useEffect12(() => () => disposeLine(focusLine), [focusLine]);
3429
- useEffect12(() => {
5069
+ useEffect18(() => () => disposeLine(line), [line]);
5070
+ useEffect18(() => () => disposeLine(focusLine), [focusLine]);
5071
+ useEffect18(() => {
3430
5072
  const width = gl.domElement.width;
3431
5073
  const height = gl.domElement.height;
3432
5074
  for (const nextLine of [line, focusLine]) {
@@ -3444,7 +5086,7 @@ function PolylineHotspot({
3444
5086
  startPosition: event.startPosition
3445
5087
  });
3446
5088
  };
3447
- return /* @__PURE__ */ jsx11(
5089
+ return /* @__PURE__ */ jsx12(
3448
5090
  HotspotAnchor,
3449
5091
  {
3450
5092
  ...anchorProps,
@@ -3458,7 +5100,7 @@ function PolylineHotspot({
3458
5100
  renderOrder,
3459
5101
  draggable,
3460
5102
  useAngularScale: false,
3461
- focusContent: /* @__PURE__ */ jsx11("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
5103
+ focusContent: /* @__PURE__ */ jsx12("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
3462
5104
  onDragStart: (event) => {
3463
5105
  startVerticesRef.current = cloneVertices2(vertices);
3464
5106
  emitVerticesChange(event, onDragStart);
@@ -3468,13 +5110,13 @@ function PolylineHotspot({
3468
5110
  emitVerticesChange(event, onDragEnd);
3469
5111
  startVerticesRef.current = null;
3470
5112
  },
3471
- children: /* @__PURE__ */ jsx11("primitive", { object: line, renderOrder: renderOrder + 1 })
5113
+ children: /* @__PURE__ */ jsx12("primitive", { object: line, renderOrder: renderOrder + 1 })
3472
5114
  }
3473
5115
  );
3474
5116
  }
3475
5117
 
3476
5118
  // src/hotspot/graphic-hotspot.tsx
3477
- 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";
3478
5120
  import {
3479
5121
  CanvasTexture,
3480
5122
  ClampToEdgeWrapping as ClampToEdgeWrapping4,
@@ -3483,7 +5125,7 @@ import {
3483
5125
  LinearMipmapLinearFilter as LinearMipmapLinearFilter3,
3484
5126
  SRGBColorSpace as SRGBColorSpace5
3485
5127
  } from "three";
3486
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
5128
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3487
5129
  var GRAPHIC_TEXTURE_SIZE = 512;
3488
5130
  function clampOpacity2(opacity) {
3489
5131
  if (!Number.isFinite(opacity)) {
@@ -3494,14 +5136,34 @@ function clampOpacity2(opacity) {
3494
5136
  function resolveStrokeWidth(value) {
3495
5137
  return Number.isFinite(value) ? Math.max(value, 0) : 8;
3496
5138
  }
3497
- function roundedRectanglePath(context, x, y, width, height, radius) {
3498
- 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) {
3499
5162
  context.beginPath();
3500
- context.moveTo(x + safeRadius, y);
3501
- context.arcTo(x + width, y, x + width, y + height, safeRadius);
3502
- context.arcTo(x + width, y + height, x, y + height, safeRadius);
3503
- context.arcTo(x, y + height, x, y, safeRadius);
3504
- 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
+ }
3505
5167
  context.closePath();
3506
5168
  }
3507
5169
  function paintPath(context, graphic, path) {
@@ -3515,7 +5177,7 @@ function paintPath(context, graphic, path) {
3515
5177
  path ? context.stroke(path) : context.stroke();
3516
5178
  }
3517
5179
  }
3518
- function drawGraphic(context, graphic) {
5180
+ function drawGraphic(context, graphic, aspectWidth, aspectHeight) {
3519
5181
  const padding = Math.max(resolveStrokeWidth(graphic.strokeWidth) / 2 + 2, 4);
3520
5182
  const drawableSize = GRAPHIC_TEXTURE_SIZE - padding * 2;
3521
5183
  context.clearRect(0, 0, GRAPHIC_TEXTURE_SIZE, GRAPHIC_TEXTURE_SIZE);
@@ -3533,6 +5195,63 @@ function drawGraphic(context, graphic) {
3533
5195
  paintPath(context, graphic);
3534
5196
  return;
3535
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
+ }
3536
5255
  if (graphic.kind === "rectangle") {
3537
5256
  roundedRectanglePath(
3538
5257
  context,
@@ -3540,7 +5259,9 @@ function drawGraphic(context, graphic) {
3540
5259
  padding,
3541
5260
  drawableSize,
3542
5261
  drawableSize,
3543
- graphic.cornerRadius ?? 0
5262
+ graphic.cornerRadius ?? 0,
5263
+ aspectWidth,
5264
+ aspectHeight
3544
5265
  );
3545
5266
  paintPath(context, graphic);
3546
5267
  return;
@@ -3579,7 +5300,7 @@ function drawGraphic(context, graphic) {
3579
5300
  paintPath(context, graphic, path);
3580
5301
  context.restore();
3581
5302
  }
3582
- function createGraphicTexture(graphic) {
5303
+ function createGraphicTexture(graphic, aspectWidth, aspectHeight) {
3583
5304
  const canvas = document.createElement("canvas");
3584
5305
  canvas.width = GRAPHIC_TEXTURE_SIZE;
3585
5306
  canvas.height = GRAPHIC_TEXTURE_SIZE;
@@ -3587,7 +5308,7 @@ function createGraphicTexture(graphic) {
3587
5308
  if (!context) {
3588
5309
  throw new Error("Canvas 2D is unavailable for this graphic hotspot.");
3589
5310
  }
3590
- drawGraphic(context, graphic);
5311
+ drawGraphic(context, graphic, aspectWidth, aspectHeight);
3591
5312
  const texture = new CanvasTexture(canvas);
3592
5313
  texture.colorSpace = SRGBColorSpace5;
3593
5314
  texture.wrapS = ClampToEdgeWrapping4;
@@ -3598,16 +5319,16 @@ function createGraphicTexture(graphic) {
3598
5319
  texture.needsUpdate = true;
3599
5320
  return texture;
3600
5321
  }
3601
- function useGraphicTexture(graphic, onLoad, onError) {
3602
- const [texture, setTexture] = useState7(null);
3603
- const onLoadRef = useRef12(onLoad);
3604
- 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);
3605
5326
  onLoadRef.current = onLoad;
3606
5327
  onErrorRef.current = onError;
3607
- useEffect13(() => {
5328
+ useEffect19(() => {
3608
5329
  let nextTexture = null;
3609
5330
  try {
3610
- nextTexture = createGraphicTexture(graphic);
5331
+ nextTexture = createGraphicTexture(graphic, aspectWidth, aspectHeight);
3611
5332
  setTexture(nextTexture);
3612
5333
  onLoadRef.current?.(nextTexture);
3613
5334
  } catch (error) {
@@ -3617,7 +5338,7 @@ function useGraphicTexture(graphic, onLoad, onError) {
3617
5338
  return () => {
3618
5339
  nextTexture?.dispose();
3619
5340
  };
3620
- }, [graphic]);
5341
+ }, [aspectHeight, aspectWidth, graphic]);
3621
5342
  return texture;
3622
5343
  }
3623
5344
  function CanvasGraphicHotspot({
@@ -3629,10 +5350,10 @@ function CanvasGraphicHotspot({
3629
5350
  onError,
3630
5351
  ...anchorProps
3631
5352
  }) {
3632
- const texture = useGraphicTexture(graphic, onLoad, onError);
3633
- return /* @__PURE__ */ jsx12(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs9("mesh", { children: [
3634
- /* @__PURE__ */ jsx12("planeGeometry", { args: [1, 1] }),
3635
- /* @__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(
3636
5357
  "meshBasicMaterial",
3637
5358
  {
3638
5359
  map: texture,
@@ -3648,14 +5369,14 @@ function GraphicHotspot(props) {
3648
5369
  const { graphic, ...restProps } = props;
3649
5370
  if (graphic.kind === "svg") {
3650
5371
  const imageProps = restProps;
3651
- return /* @__PURE__ */ jsx12(ImageHotspot, { ...imageProps, src: graphic.src });
5372
+ return /* @__PURE__ */ jsx13(ImageHotspot, { ...imageProps, src: graphic.src });
3652
5373
  }
3653
- return /* @__PURE__ */ jsx12(CanvasGraphicHotspot, { ...restProps, graphic });
5374
+ return /* @__PURE__ */ jsx13(CanvasGraphicHotspot, { ...restProps, graphic });
3654
5375
  }
3655
5376
 
3656
5377
  // src/hotspot/sequence-hotspot.tsx
3657
- import { useFrame as useFrame6, useThree as useThree9 } from "@react-three/fiber";
3658
- 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";
3659
5380
  import {
3660
5381
  ClampToEdgeWrapping as ClampToEdgeWrapping5,
3661
5382
  DoubleSide as DoubleSide5,
@@ -3664,7 +5385,7 @@ import {
3664
5385
  SRGBColorSpace as SRGBColorSpace6,
3665
5386
  TextureLoader as TextureLoader5
3666
5387
  } from "three";
3667
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
5388
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3668
5389
  var DEFAULT_SEQUENCE_FPS = 12;
3669
5390
  function clampOpacity3(opacity) {
3670
5391
  if (!Number.isFinite(opacity)) {
@@ -3684,13 +5405,13 @@ function useSpriteTexture({
3684
5405
  onError,
3685
5406
  onLoadProgress
3686
5407
  }) {
3687
- const gl = useThree9((state) => state.gl);
3688
- const [texture, setTexture] = useState8(null);
3689
- const onErrorRef = useRef13(onError);
3690
- 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);
3691
5412
  onErrorRef.current = onError;
3692
5413
  onLoadProgressRef.current = onLoadProgress;
3693
- useEffect14(() => {
5414
+ useEffect20(() => {
3694
5415
  let active = true;
3695
5416
  setTexture(null);
3696
5417
  if (!src) {
@@ -3731,7 +5452,7 @@ function useSpriteTexture({
3731
5452
  active = false;
3732
5453
  };
3733
5454
  }, [gl, id, src]);
3734
- useEffect14(
5455
+ useEffect20(
3735
5456
  () => () => {
3736
5457
  texture?.dispose();
3737
5458
  },
@@ -3760,24 +5481,24 @@ function SequenceHotspot({
3760
5481
  const texture = useSpriteTexture({ id, src, onError, onLoadProgress });
3761
5482
  const frameCount = resolveFrameCount(requestedFrameCount);
3762
5483
  const safeFps = resolveFps(fps);
3763
- const [frameIndex, setFrameIndex] = useState8(0);
3764
- const elapsedRef = useRef13(0);
3765
- const frameIndexRef = useRef13(0);
3766
- const endedRef = useRef13(false);
3767
- const lastPlaybackStateRef = useRef13(null);
3768
- const onFrameChangeRef = useRef13(onFrameChange);
3769
- const onPlaybackStateChangeRef = useRef13(onPlaybackStateChange);
3770
- 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);
3771
5492
  onFrameChangeRef.current = onFrameChange;
3772
5493
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3773
5494
  onEndedRef.current = onEnded;
3774
- useEffect14(() => {
5495
+ useEffect20(() => {
3775
5496
  elapsedRef.current = 0;
3776
5497
  frameIndexRef.current = 0;
3777
5498
  endedRef.current = false;
3778
5499
  setFrameIndex(0);
3779
5500
  }, [frameCount, frameDirection, src]);
3780
- useEffect14(() => {
5501
+ useEffect20(() => {
3781
5502
  if (!texture) {
3782
5503
  return;
3783
5504
  }
@@ -3791,13 +5512,13 @@ function SequenceHotspot({
3791
5512
  );
3792
5513
  texture.needsUpdate = true;
3793
5514
  }, [frameCount, frameDirection, frameIndex, texture]);
3794
- useEffect14(() => {
5515
+ useEffect20(() => {
3795
5516
  if (!texture) {
3796
5517
  return;
3797
5518
  }
3798
5519
  onFrameChangeRef.current?.({ id, index: 0, src });
3799
5520
  }, [id, src, texture]);
3800
- useEffect14(() => {
5521
+ useEffect20(() => {
3801
5522
  if (!texture) {
3802
5523
  return;
3803
5524
  }
@@ -3816,7 +5537,7 @@ function SequenceHotspot({
3816
5537
  onPlaybackStateChangeRef.current?.(nextState);
3817
5538
  }
3818
5539
  }, [playing, texture]);
3819
- useFrame6((_, deltaSeconds) => {
5540
+ useFrame8((_, deltaSeconds) => {
3820
5541
  if (!texture || !playing || endedRef.current) {
3821
5542
  return;
3822
5543
  }
@@ -3846,9 +5567,9 @@ function SequenceHotspot({
3846
5567
  onFrameChangeRef.current?.({ id, index: nextIndex, src });
3847
5568
  }
3848
5569
  });
3849
- return /* @__PURE__ */ jsx13(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs10("mesh", { children: [
3850
- /* @__PURE__ */ jsx13("planeGeometry", { args: [1, 1] }),
3851
- /* @__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(
3852
5573
  "meshBasicMaterial",
3853
5574
  {
3854
5575
  map: texture,
@@ -3862,8 +5583,8 @@ function SequenceHotspot({
3862
5583
  }
3863
5584
 
3864
5585
  // src/hotspot/video-hotspot.tsx
3865
- import { useThree as useThree10 } from "@react-three/fiber";
3866
- 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";
3867
5588
  import {
3868
5589
  ClampToEdgeWrapping as ClampToEdgeWrapping6,
3869
5590
  DoubleSide as DoubleSide6,
@@ -3873,7 +5594,7 @@ import {
3873
5594
  TextureLoader as TextureLoader6,
3874
5595
  VideoTexture
3875
5596
  } from "three";
3876
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
5597
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3877
5598
  function clampOpacity4(opacity) {
3878
5599
  if (!Number.isFinite(opacity)) {
3879
5600
  return 1;
@@ -3887,11 +5608,11 @@ function clampVolume(volume) {
3887
5608
  return Math.max(0, Math.min(volume, 1));
3888
5609
  }
3889
5610
  function usePosterTexture(id, poster, onError) {
3890
- const gl = useThree10((state) => state.gl);
3891
- const [texture, setTexture] = useState9(null);
3892
- const onErrorRef = useRef14(onError);
5611
+ const gl = useThree15((state) => state.gl);
5612
+ const [texture, setTexture] = useState10(null);
5613
+ const onErrorRef = useRef19(onError);
3893
5614
  onErrorRef.current = onError;
3894
- useEffect15(() => {
5615
+ useEffect21(() => {
3895
5616
  let active = true;
3896
5617
  setTexture(null);
3897
5618
  if (!poster) {
@@ -3928,7 +5649,7 @@ function usePosterTexture(id, poster, onError) {
3928
5649
  active = false;
3929
5650
  };
3930
5651
  }, [gl, id, poster]);
3931
- useEffect15(
5652
+ useEffect21(
3932
5653
  () => () => {
3933
5654
  texture?.dispose();
3934
5655
  },
@@ -3946,14 +5667,14 @@ function useVideoResource({
3946
5667
  poster,
3947
5668
  src
3948
5669
  }) {
3949
- const [resource, setResource] = useState9(null);
3950
- const onEndedRef = useRef14(onEnded);
3951
- const onErrorRef = useRef14(onError);
3952
- 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);
3953
5674
  onEndedRef.current = onEnded;
3954
5675
  onErrorRef.current = onError;
3955
5676
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3956
- useEffect15(() => {
5677
+ useEffect21(() => {
3957
5678
  if (!src) {
3958
5679
  setResource(null);
3959
5680
  return;
@@ -4054,11 +5775,11 @@ function VideoHotspot({
4054
5775
  src
4055
5776
  });
4056
5777
  const posterTexture = usePosterTexture(id, poster, onError);
4057
- const onPlaybackErrorRef = useRef14(onPlaybackError);
4058
- const onPlaybackStateChangeRef = useRef14(onPlaybackStateChange);
5778
+ const onPlaybackErrorRef = useRef19(onPlaybackError);
5779
+ const onPlaybackStateChangeRef = useRef19(onPlaybackStateChange);
4059
5780
  onPlaybackErrorRef.current = onPlaybackError;
4060
5781
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
4061
- useEffect15(() => {
5782
+ useEffect21(() => {
4062
5783
  if (!resource) {
4063
5784
  return;
4064
5785
  }
@@ -4068,7 +5789,7 @@ function VideoHotspot({
4068
5789
  resource.video.playsInline = playsInline;
4069
5790
  resource.video.preload = preload;
4070
5791
  }, [loop, muted, playsInline, preload, resource, volume]);
4071
- useEffect15(() => {
5792
+ useEffect21(() => {
4072
5793
  if (!resource) {
4073
5794
  return;
4074
5795
  }
@@ -4091,9 +5812,9 @@ function VideoHotspot({
4091
5812
  };
4092
5813
  }, [playing, resource]);
4093
5814
  const activeTexture = resource?.ready ? resource.texture : posterTexture;
4094
- return /* @__PURE__ */ jsx14(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs11("mesh", { children: [
4095
- /* @__PURE__ */ jsx14("planeGeometry", { args: [1, 1] }),
4096
- /* @__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(
4097
5818
  "meshBasicMaterial",
4098
5819
  {
4099
5820
  map: activeTexture,
@@ -4109,7 +5830,12 @@ export {
4109
5830
  AutoRotate,
4110
5831
  GraphicHotspot,
4111
5832
  ImageHotspot,
5833
+ KeyboardControls,
4112
5834
  MAX_HOTSPOT_PITCH,
5835
+ MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY,
5836
+ MouseControls,
5837
+ PanoContextMenu,
5838
+ PanoEvents,
4113
5839
  PanoView,
4114
5840
  PanoramaScenes,
4115
5841
  PolygonHotspot,
@@ -4117,12 +5843,19 @@ export {
4117
5843
  SequenceHotspot,
4118
5844
  Sphere,
4119
5845
  Tile,
5846
+ TouchControls,
4120
5847
  VideoHotspot,
4121
5848
  clampPanoPitch,
5849
+ composePanoContextMenuItems,
5850
+ createDefaultPanoContextMenuItems,
5851
+ createPanoContextMenuPresets,
5852
+ cycleSceneId,
4122
5853
  normalizePanoPosition,
4123
5854
  normalizePanoYaw,
4124
5855
  panoPositionToVector3,
5856
+ resolvePanoContextMenuEntries,
4125
5857
  unwrapPolygonVertices,
5858
+ usePanoEvents,
4126
5859
  validatePolygonVertices,
4127
5860
  validatePolylineVertices,
4128
5861
  vector3ToPanoPosition