@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.cjs CHANGED
@@ -24,7 +24,12 @@ __export(index_exports, {
24
24
  AutoRotate: () => AutoRotate,
25
25
  GraphicHotspot: () => GraphicHotspot,
26
26
  ImageHotspot: () => ImageHotspot,
27
+ KeyboardControls: () => KeyboardControls,
27
28
  MAX_HOTSPOT_PITCH: () => MAX_HOTSPOT_PITCH,
29
+ MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY: () => MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY,
30
+ MouseControls: () => MouseControls,
31
+ PanoContextMenu: () => PanoContextMenu,
32
+ PanoEvents: () => PanoEvents,
28
33
  PanoView: () => PanoView,
29
34
  PanoramaScenes: () => PanoramaScenes,
30
35
  PolygonHotspot: () => PolygonHotspot,
@@ -32,12 +37,19 @@ __export(index_exports, {
32
37
  SequenceHotspot: () => SequenceHotspot,
33
38
  Sphere: () => Sphere,
34
39
  Tile: () => Tile,
40
+ TouchControls: () => TouchControls,
35
41
  VideoHotspot: () => VideoHotspot,
36
42
  clampPanoPitch: () => clampPanoPitch,
43
+ composePanoContextMenuItems: () => composePanoContextMenuItems,
44
+ createDefaultPanoContextMenuItems: () => createDefaultPanoContextMenuItems,
45
+ createPanoContextMenuPresets: () => createPanoContextMenuPresets,
46
+ cycleSceneId: () => cycleSceneId,
37
47
  normalizePanoPosition: () => normalizePanoPosition,
38
48
  normalizePanoYaw: () => normalizePanoYaw,
39
49
  panoPositionToVector3: () => panoPositionToVector3,
50
+ resolvePanoContextMenuEntries: () => resolvePanoContextMenuEntries,
40
51
  unwrapPolygonVertices: () => unwrapPolygonVertices,
52
+ usePanoEvents: () => usePanoEvents,
41
53
  validatePolygonVertices: () => validatePolygonVertices,
42
54
  validatePolylineVertices: () => validatePolylineVertices,
43
55
  vector3ToPanoPosition: () => vector3ToPanoPosition
@@ -45,21 +57,59 @@ __export(index_exports, {
45
57
  module.exports = __toCommonJS(index_exports);
46
58
 
47
59
  // src/pano-view.tsx
48
- var import_fiber3 = require("@react-three/fiber");
49
- var import_react4 = require("react");
60
+ var import_fiber8 = require("@react-three/fiber");
61
+ var import_react11 = require("react");
50
62
 
51
- // src/panorama-controls.tsx
52
- var import_fiber = require("@react-three/fiber");
63
+ // src/auto-rotate.tsx
64
+ var import_fiber2 = require("@react-three/fiber");
65
+ var import_react3 = require("react");
66
+
67
+ // src/pano-event-bus.ts
53
68
  var import_react = require("react");
69
+ function createPanoEventBus() {
70
+ const listeners = /* @__PURE__ */ new Map();
71
+ return {
72
+ subscribe(type, listener) {
73
+ let set = listeners.get(type);
74
+ if (!set) {
75
+ set = /* @__PURE__ */ new Set();
76
+ listeners.set(type, set);
77
+ }
78
+ set.add(listener);
79
+ return () => {
80
+ set?.delete(listener);
81
+ if (set && set.size === 0) {
82
+ listeners.delete(type);
83
+ }
84
+ };
85
+ },
86
+ emit(type, payload) {
87
+ const set = listeners.get(type);
88
+ if (!set || set.size === 0) {
89
+ return;
90
+ }
91
+ for (const listener of [...set]) {
92
+ listener(payload);
93
+ }
94
+ }
95
+ };
96
+ }
97
+ var PanoEventBusContext = (0, import_react.createContext)(null);
98
+
99
+ // src/panorama-view-runtime.tsx
100
+ var import_fiber = require("@react-three/fiber");
101
+ var import_react2 = require("react");
54
102
  var import_three = require("three");
55
103
  var MAX_PITCH = 90;
104
+ var BOUNCE_OVERSHOOT_PITCH = 12;
105
+ var BOUNCE_OVERSHOOT_FOV_RATIO = 0.15;
56
106
  var INERTIA_ROTATION_DAMPING = 8;
57
107
  var INERTIA_ZOOM_DAMPING = 12;
58
108
  var DEFAULT_ROTATE_DAMPING = 14;
59
109
  var DEFAULT_ZOOM_DAMPING = 16;
60
- var MIN_ROTATION_VELOCITY = 0.01;
61
- var MIN_ZOOM_VELOCITY = 0.01;
110
+ var DEFAULT_FRICTION_STOP = 0.01;
62
111
  var VIEW_SETTLE_EPSILON = 1e-3;
112
+ var PanoramaViewContext = (0, import_react2.createContext)(null);
63
113
  function clamp(value, min, max) {
64
114
  return Math.max(min, Math.min(max, value));
65
115
  }
@@ -75,54 +125,104 @@ function dampingFactor(damping, deltaSeconds) {
75
125
  function resolveDamping(value, fallback) {
76
126
  return Number.isFinite(value) && value >= 0 ? value : fallback;
77
127
  }
78
- function pointerDistance(a, b) {
79
- return Math.hypot(a.x - b.x, a.y - b.y);
128
+ function resolveFrictionStop(value) {
129
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_FRICTION_STOP;
80
130
  }
81
131
  function viewsEqual(a, b) {
82
132
  return a.yaw === b.yaw && a.pitch === b.pitch && a.fov === b.fov;
83
133
  }
84
- var PanoramaControls = (0, import_react.forwardRef)(function PanoramaControls2({ enabled, initialView, minFov, maxFov, options, onViewChange }, ref) {
85
- const { camera, gl } = (0, import_fiber.useThree)();
86
- const viewRef = (0, import_react.useRef)({ ...initialView });
87
- const targetViewRef = (0, import_react.useRef)({ ...initialView });
88
- const initialViewRef = (0, import_react.useRef)({ ...initialView });
89
- const pointersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
90
- const pinchDistanceRef = (0, import_react.useRef)(null);
91
- const yawVelocityRef = (0, import_react.useRef)(0);
92
- const pitchVelocityRef = (0, import_react.useRef)(0);
93
- const zoomVelocityRef = (0, import_react.useRef)(0);
94
- const dirtyRef = (0, import_react.useRef)(true);
95
- const interactingRef = (0, import_react.useRef)(false);
96
- const interactionLockCountRef = (0, import_react.useRef)(0);
97
- const optionsRef = (0, import_react.useRef)(options);
98
- const onViewChangeRef = (0, import_react.useRef)(onViewChange);
99
- const lastEmittedViewRef = (0, import_react.useRef)(null);
100
- const eulerRef = (0, import_react.useRef)(new import_three.Euler(0, 0, 0, "YXZ"));
134
+ var PanoramaViewRuntime = (0, import_react2.forwardRef)(function PanoramaViewRuntime2({ initialView, minFov, maxFov, options, eventBus }, ref) {
135
+ const { camera } = (0, import_fiber.useThree)();
136
+ const viewRef = (0, import_react2.useRef)({ ...initialView });
137
+ const targetViewRef = (0, import_react2.useRef)({ ...initialView });
138
+ const initialViewRef = (0, import_react2.useRef)({ ...initialView });
139
+ const yawVelocityRef = (0, import_react2.useRef)(0);
140
+ const pitchVelocityRef = (0, import_react2.useRef)(0);
141
+ const zoomVelocityRef = (0, import_react2.useRef)(0);
142
+ const dirtyRef = (0, import_react2.useRef)(true);
143
+ const settledRef = (0, import_react2.useRef)(false);
144
+ const interactingRef = (0, import_react2.useRef)(false);
145
+ const mouseActiveRef = (0, import_react2.useRef)(false);
146
+ const touchActiveRef = (0, import_react2.useRef)(false);
147
+ const interactionLockCountRef = (0, import_react2.useRef)(0);
148
+ const keyboardActiveRef = (0, import_react2.useRef)(false);
149
+ const lastVelocitySampleAtRef = (0, import_react2.useRef)(null);
150
+ const optionsRef = (0, import_react2.useRef)(options);
151
+ const eventBusRef = (0, import_react2.useRef)(eventBus);
152
+ const lastEmittedViewRef = (0, import_react2.useRef)(null);
153
+ const eulerRef = (0, import_react2.useRef)(new import_three.Euler(0, 0, 0, "YXZ"));
154
+ const minFovRef = (0, import_react2.useRef)(minFov);
155
+ const maxFovRef = (0, import_react2.useRef)(maxFov);
101
156
  optionsRef.current = options;
102
- onViewChangeRef.current = onViewChange;
103
- const constrainView = (view, current = targetViewRef.current) => {
157
+ eventBusRef.current = eventBus;
158
+ minFovRef.current = minFov;
159
+ maxFovRef.current = maxFov;
160
+ const hasInertia = () => yawVelocityRef.current !== 0 || pitchVelocityRef.current !== 0 || zoomVelocityRef.current !== 0;
161
+ const isUserInteracting = () => mouseActiveRef.current || touchActiveRef.current || keyboardActiveRef.current || interactionLockCountRef.current > 0 || hasInertia();
162
+ const syncInteracting = () => {
163
+ interactingRef.current = mouseActiveRef.current || touchActiveRef.current;
164
+ };
165
+ const hardConstrainView = (view, current = targetViewRef.current) => {
104
166
  return {
105
167
  yaw: normalizeYaw(view.yaw ?? current.yaw),
106
168
  pitch: clamp(view.pitch ?? current.pitch, -MAX_PITCH, MAX_PITCH),
107
- fov: clamp(view.fov ?? current.fov, minFov, maxFov)
169
+ fov: clamp(
170
+ view.fov ?? current.fov,
171
+ minFovRef.current,
172
+ maxFovRef.current
173
+ )
174
+ };
175
+ };
176
+ const softConstrainView = (view, current = targetViewRef.current) => {
177
+ const bouncing = optionsRef.current.bouncingLimits === true;
178
+ if (!bouncing || !interactingRef.current) {
179
+ return hardConstrainView(view, current);
180
+ }
181
+ const fovRange = Math.max(maxFovRef.current - minFovRef.current, 1);
182
+ const fovOvershoot = fovRange * BOUNCE_OVERSHOOT_FOV_RATIO;
183
+ return {
184
+ yaw: normalizeYaw(view.yaw ?? current.yaw),
185
+ pitch: clamp(
186
+ view.pitch ?? current.pitch,
187
+ -MAX_PITCH - BOUNCE_OVERSHOOT_PITCH,
188
+ MAX_PITCH + BOUNCE_OVERSHOOT_PITCH
189
+ ),
190
+ fov: clamp(
191
+ view.fov ?? current.fov,
192
+ minFovRef.current - fovOvershoot,
193
+ maxFovRef.current + fovOvershoot
194
+ )
108
195
  };
109
196
  };
110
197
  const stopVelocity = () => {
111
198
  yawVelocityRef.current = 0;
112
199
  pitchVelocityRef.current = 0;
113
200
  zoomVelocityRef.current = 0;
201
+ lastVelocitySampleAtRef.current = null;
202
+ };
203
+ const markDirty = () => {
204
+ dirtyRef.current = true;
205
+ settledRef.current = false;
206
+ };
207
+ const snapTargetToHardLimits = () => {
208
+ const next = hardConstrainView(targetViewRef.current);
209
+ if (next.yaw !== targetViewRef.current.yaw || next.pitch !== targetViewRef.current.pitch || next.fov !== targetViewRef.current.fov) {
210
+ targetViewRef.current = next;
211
+ markDirty();
212
+ }
114
213
  };
115
214
  const acquireInteractionLock = () => {
116
215
  interactionLockCountRef.current += 1;
117
216
  stopVelocity();
118
- interactingRef.current = false;
119
- pinchDistanceRef.current = null;
120
- for (const pointerId of pointersRef.current.keys()) {
121
- if (gl.domElement.hasPointerCapture?.(pointerId)) {
122
- gl.domElement.releasePointerCapture?.(pointerId);
123
- }
217
+ if (mouseActiveRef.current) {
218
+ mouseActiveRef.current = false;
219
+ eventBusRef.current.emit("viewinteractionend", { source: "mouse" });
220
+ }
221
+ if (touchActiveRef.current) {
222
+ touchActiveRef.current = false;
223
+ eventBusRef.current.emit("viewinteractionend", { source: "touch" });
124
224
  }
125
- pointersRef.current.clear();
225
+ syncInteracting();
126
226
  let released = false;
127
227
  return () => {
128
228
  if (released) {
@@ -136,233 +236,145 @@ var PanoramaControls = (0, import_react.forwardRef)(function PanoramaControls2({
136
236
  };
137
237
  };
138
238
  const setView = (view, setOptions) => {
139
- const nextView = constrainView(view);
239
+ const nextView = hardConstrainView(view);
140
240
  targetViewRef.current = nextView;
141
241
  viewRef.current = nextView;
142
242
  if (setOptions?.immediate !== false) {
143
243
  stopVelocity();
144
244
  }
145
- dirtyRef.current = true;
245
+ markDirty();
246
+ };
247
+ const applyViewDelta = (delta, applyOptions) => {
248
+ if (interactionLockCountRef.current > 0) {
249
+ return false;
250
+ }
251
+ const current = targetViewRef.current;
252
+ const nextYaw = delta.yaw !== void 0 && Number.isFinite(delta.yaw) ? current.yaw + delta.yaw : current.yaw;
253
+ const nextPitch = delta.pitch !== void 0 && Number.isFinite(delta.pitch) ? current.pitch + delta.pitch : current.pitch;
254
+ const nextFov = delta.fov !== void 0 && Number.isFinite(delta.fov) ? current.fov + delta.fov : current.fov;
255
+ targetViewRef.current = softConstrainView({
256
+ yaw: nextYaw,
257
+ pitch: nextPitch,
258
+ fov: nextFov
259
+ });
260
+ markDirty();
261
+ if (applyOptions?.recordVelocity) {
262
+ const now = performance.now();
263
+ const previousAt = lastVelocitySampleAtRef.current;
264
+ const elapsed = previousAt === null ? 1 / 60 : Math.max((now - previousAt) / 1e3, 1 / 120);
265
+ lastVelocitySampleAtRef.current = now;
266
+ if (delta.yaw !== void 0 && Number.isFinite(delta.yaw)) {
267
+ yawVelocityRef.current = delta.yaw / elapsed;
268
+ }
269
+ if (delta.pitch !== void 0 && Number.isFinite(delta.pitch)) {
270
+ pitchVelocityRef.current = delta.pitch / elapsed;
271
+ }
272
+ if (delta.fov !== void 0 && Number.isFinite(delta.fov)) {
273
+ zoomVelocityRef.current = clamp(delta.fov / elapsed, -180, 180);
274
+ }
275
+ }
276
+ return true;
146
277
  };
147
- (0, import_react.useImperativeHandle)(
278
+ (0, import_react2.useImperativeHandle)(
148
279
  ref,
149
280
  () => ({
150
281
  getView: () => ({ ...viewRef.current }),
282
+ getTargetView: () => ({ ...targetViewRef.current }),
151
283
  setView,
152
284
  reset: () => setView(initialViewRef.current),
285
+ applyViewDelta,
286
+ setKeyboardActive: (active) => {
287
+ const wasActive = keyboardActiveRef.current;
288
+ keyboardActiveRef.current = active;
289
+ if (active && !wasActive) {
290
+ eventBusRef.current.emit("viewinteractionstart", {
291
+ source: "keyboard"
292
+ });
293
+ } else if (!active && wasActive) {
294
+ eventBusRef.current.emit("viewinteractionend", {
295
+ source: "keyboard"
296
+ });
297
+ }
298
+ },
299
+ setPointerActive: (source, active) => {
300
+ const wasActive = source === "mouse" ? mouseActiveRef.current : touchActiveRef.current;
301
+ if (source === "mouse") {
302
+ mouseActiveRef.current = active;
303
+ } else {
304
+ touchActiveRef.current = active;
305
+ }
306
+ syncInteracting();
307
+ if (active && !wasActive) {
308
+ eventBusRef.current.emit("viewinteractionstart", { source });
309
+ } else if (!active && wasActive) {
310
+ eventBusRef.current.emit("viewinteractionend", { source });
311
+ }
312
+ if (active) {
313
+ stopVelocity();
314
+ } else if (!mouseActiveRef.current && !touchActiveRef.current) {
315
+ lastVelocitySampleAtRef.current = null;
316
+ snapTargetToHardLimits();
317
+ }
318
+ },
319
+ isInteractionLocked: () => interactionLockCountRef.current > 0,
320
+ isUserInteracting,
153
321
  acquireInteractionLock,
154
322
  applyAutoRotation: (yawDelta) => {
155
323
  const inertiaFinished = yawVelocityRef.current === 0 && pitchVelocityRef.current === 0 && zoomVelocityRef.current === 0;
156
- if (!Number.isFinite(yawDelta) || interactionLockCountRef.current > 0 || interactingRef.current || !inertiaFinished) {
324
+ if (!Number.isFinite(yawDelta) || interactionLockCountRef.current > 0 || interactingRef.current || keyboardActiveRef.current || !inertiaFinished) {
157
325
  return false;
158
326
  }
159
327
  targetViewRef.current.yaw += yawDelta;
160
- dirtyRef.current = true;
328
+ markDirty();
161
329
  return true;
162
330
  }
163
331
  }),
164
- [gl, maxFov, minFov]
332
+ []
165
333
  );
166
- (0, import_react.useEffect)(() => {
334
+ (0, import_react2.useEffect)(() => {
167
335
  initialViewRef.current = { ...initialView };
168
336
  setView(initialView);
169
337
  }, [initialView.fov, initialView.pitch, initialView.yaw]);
170
- (0, import_react.useEffect)(() => {
171
- const element = gl.domElement;
172
- if (!enabled) {
173
- pointersRef.current.clear();
174
- interactingRef.current = false;
175
- return;
176
- }
177
- const updatePointer = (event) => {
178
- const previous = pointersRef.current.get(event.pointerId);
179
- const now = performance.now();
180
- const next = { x: event.clientX, y: event.clientY, at: now };
181
- pointersRef.current.set(event.pointerId, next);
182
- return { previous, next };
183
- };
184
- const onPointerDown = (event) => {
185
- if (interactionLockCountRef.current > 0) {
186
- return;
187
- }
188
- event.preventDefault();
189
- interactingRef.current = true;
190
- stopVelocity();
191
- pointersRef.current.set(event.pointerId, {
192
- x: event.clientX,
193
- y: event.clientY,
194
- at: performance.now()
195
- });
196
- element.setPointerCapture?.(event.pointerId);
197
- if (pointersRef.current.size === 2) {
198
- const [first, second] = Array.from(pointersRef.current.values());
199
- pinchDistanceRef.current = pointerDistance(first, second);
200
- }
201
- };
202
- const onPointerMove = (event) => {
203
- if (interactionLockCountRef.current > 0) {
204
- return;
205
- }
206
- if (!pointersRef.current.has(event.pointerId)) {
207
- return;
208
- }
209
- event.preventDefault();
210
- const { previous, next } = updatePointer(event);
211
- if (!previous) {
212
- return;
213
- }
214
- const pointers = Array.from(pointersRef.current.values());
215
- if (pointers.length >= 2) {
216
- const distance = pointerDistance(pointers[0], pointers[1]);
217
- const previousDistance = pinchDistanceRef.current;
218
- pinchDistanceRef.current = distance;
219
- if (previousDistance && distance > 0) {
220
- const currentFov = targetViewRef.current.fov;
221
- const nextFov = clamp(
222
- currentFov * (previousDistance / distance),
223
- minFov,
224
- maxFov
225
- );
226
- const elapsed2 = Math.max((next.at - previous.at) / 1e3, 1 / 120);
227
- zoomVelocityRef.current = (nextFov - currentFov) / elapsed2;
228
- targetViewRef.current = {
229
- ...targetViewRef.current,
230
- fov: nextFov
231
- };
232
- dirtyRef.current = true;
233
- }
234
- return;
235
- }
236
- const rect = element.getBoundingClientRect();
237
- const rotateSpeed = optionsRef.current.rotateSpeed ?? 0.35;
238
- const deltaYaw = -(next.x - previous.x) * 360 * rotateSpeed / Math.max(rect.width, 1);
239
- const deltaPitch = (next.y - previous.y) * 180 * rotateSpeed / Math.max(rect.height, 1);
240
- const elapsed = Math.max((next.at - previous.at) / 1e3, 1 / 120);
241
- targetViewRef.current = constrainView({
242
- yaw: targetViewRef.current.yaw + deltaYaw,
243
- pitch: targetViewRef.current.pitch + deltaPitch
244
- });
245
- yawVelocityRef.current = deltaYaw / elapsed;
246
- pitchVelocityRef.current = deltaPitch / elapsed;
247
- dirtyRef.current = true;
248
- };
249
- const releasePointer = (event) => {
250
- pointersRef.current.delete(event.pointerId);
251
- if (element.hasPointerCapture?.(event.pointerId)) {
252
- element.releasePointerCapture?.(event.pointerId);
253
- }
254
- if (pointersRef.current.size < 2) {
255
- pinchDistanceRef.current = null;
256
- }
257
- interactingRef.current = pointersRef.current.size > 0;
258
- };
259
- const onWheel = (event) => {
260
- if (interactionLockCountRef.current > 0) {
261
- event.preventDefault();
262
- return;
263
- }
264
- event.preventDefault();
265
- const zoomSpeed = optionsRef.current.zoomSpeed ?? 0.08;
266
- const delta = event.deltaY * zoomSpeed;
267
- const nextFov = clamp(
268
- targetViewRef.current.fov + delta,
269
- minFov,
270
- maxFov
271
- );
272
- zoomVelocityRef.current = clamp(
273
- zoomVelocityRef.current + delta * 12,
274
- -180,
275
- 180
276
- );
277
- targetViewRef.current = {
278
- ...targetViewRef.current,
279
- fov: nextFov
280
- };
281
- dirtyRef.current = true;
282
- };
283
- const onKeyDown = (event) => {
284
- if (interactionLockCountRef.current > 0) {
285
- return;
286
- }
287
- if (optionsRef.current.keyboard === false) {
288
- return;
289
- }
290
- const step = event.shiftKey ? 10 : 3;
291
- let next = null;
292
- if (event.key === "ArrowLeft") {
293
- next = { yaw: targetViewRef.current.yaw - step };
294
- } else if (event.key === "ArrowRight") {
295
- next = { yaw: targetViewRef.current.yaw + step };
296
- } else if (event.key === "ArrowUp") {
297
- next = { pitch: targetViewRef.current.pitch + step };
298
- } else if (event.key === "ArrowDown") {
299
- next = { pitch: targetViewRef.current.pitch - step };
300
- } else if (event.key === "+" || event.key === "=") {
301
- next = { fov: targetViewRef.current.fov - step };
302
- } else if (event.key === "-" || event.key === "_") {
303
- next = { fov: targetViewRef.current.fov + step };
304
- } else if (event.key === "0") {
305
- setView(initialViewRef.current);
306
- event.preventDefault();
307
- return;
308
- }
309
- if (next) {
310
- event.preventDefault();
311
- targetViewRef.current = constrainView(next);
312
- dirtyRef.current = true;
313
- }
314
- };
315
- element.addEventListener("pointerdown", onPointerDown);
316
- element.addEventListener("pointermove", onPointerMove);
317
- element.addEventListener("pointerup", releasePointer);
318
- element.addEventListener("pointercancel", releasePointer);
319
- element.addEventListener("wheel", onWheel, { passive: false });
320
- element.addEventListener("keydown", onKeyDown);
321
- return () => {
322
- element.removeEventListener("pointerdown", onPointerDown);
323
- element.removeEventListener("pointermove", onPointerMove);
324
- element.removeEventListener("pointerup", releasePointer);
325
- element.removeEventListener("pointercancel", releasePointer);
326
- element.removeEventListener("wheel", onWheel);
327
- element.removeEventListener("keydown", onKeyDown);
328
- pointersRef.current.clear();
329
- };
330
- }, [enabled, gl, maxFov, minFov]);
331
338
  (0, import_fiber.useFrame)((_, deltaSeconds) => {
332
339
  if (!(camera instanceof import_three.PerspectiveCamera)) {
333
340
  return;
334
341
  }
342
+ const frictionStop = resolveFrictionStop(optionsRef.current.frictionStop);
335
343
  const inertiaEnabled = optionsRef.current.inertia !== false;
336
344
  if (!interactingRef.current && inertiaEnabled) {
337
345
  const rotationDamping = Math.exp(
338
346
  -INERTIA_ROTATION_DAMPING * deltaSeconds
339
347
  );
340
348
  const zoomDamping2 = Math.exp(-INERTIA_ZOOM_DAMPING * deltaSeconds);
341
- if (Math.abs(yawVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
349
+ if (Math.abs(yawVelocityRef.current) >= frictionStop) {
342
350
  targetViewRef.current.yaw += yawVelocityRef.current * deltaSeconds;
343
351
  yawVelocityRef.current *= rotationDamping;
344
- dirtyRef.current = true;
352
+ markDirty();
345
353
  } else {
346
354
  yawVelocityRef.current = 0;
347
355
  }
348
- if (Math.abs(pitchVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
356
+ if (Math.abs(pitchVelocityRef.current) >= frictionStop) {
349
357
  targetViewRef.current.pitch += pitchVelocityRef.current * deltaSeconds;
350
358
  pitchVelocityRef.current *= rotationDamping;
351
- dirtyRef.current = true;
359
+ markDirty();
352
360
  } else {
353
361
  pitchVelocityRef.current = 0;
354
362
  }
355
- if (Math.abs(zoomVelocityRef.current) >= MIN_ZOOM_VELOCITY) {
363
+ if (Math.abs(zoomVelocityRef.current) >= frictionStop) {
356
364
  targetViewRef.current.fov += zoomVelocityRef.current * deltaSeconds;
357
365
  zoomVelocityRef.current *= zoomDamping2;
358
- dirtyRef.current = true;
366
+ markDirty();
359
367
  } else {
360
368
  zoomVelocityRef.current = 0;
361
369
  }
362
370
  } else if (!inertiaEnabled) {
363
371
  stopVelocity();
364
372
  }
365
- targetViewRef.current = constrainView(targetViewRef.current);
373
+ if (!interactingRef.current) {
374
+ targetViewRef.current = hardConstrainView(targetViewRef.current);
375
+ } else {
376
+ targetViewRef.current = softConstrainView(targetViewRef.current);
377
+ }
366
378
  if (!dirtyRef.current) {
367
379
  return;
368
380
  }
@@ -399,7 +411,12 @@ var PanoramaControls = (0, import_react.forwardRef)(function PanoramaControls2({
399
411
  const lastEmitted = lastEmittedViewRef.current;
400
412
  if (!lastEmitted || !viewsEqual(lastEmitted, nextView)) {
401
413
  lastEmittedViewRef.current = nextView;
402
- onViewChangeRef.current?.(nextView);
414
+ eventBusRef.current.emit("viewchange", nextView);
415
+ }
416
+ const wasSettled = settledRef.current;
417
+ settledRef.current = hasSettled;
418
+ if (hasSettled && !wasSettled) {
419
+ eventBusRef.current.emit("viewsettled", nextView);
403
420
  }
404
421
  dirtyRef.current = !hasSettled;
405
422
  });
@@ -407,12 +424,9 @@ var PanoramaControls = (0, import_react.forwardRef)(function PanoramaControls2({
407
424
  });
408
425
 
409
426
  // src/auto-rotate.tsx
410
- var import_fiber2 = require("@react-three/fiber");
411
- var import_react2 = require("react");
412
427
  var DEFAULT_AUTO_ROTATE_SPEED = 18;
413
428
  var DEFAULT_AUTO_ROTATE_ACCELERATION = 18;
414
429
  var DEFAULT_AUTO_ROTATE_START_DELAY = 2;
415
- var PanoramaControlsContext = (0, import_react2.createContext)(null);
416
430
  function resolveStartDelay(value) {
417
431
  return Number.isFinite(value) ? Math.max(value, 0) : 0;
418
432
  }
@@ -425,19 +439,31 @@ function AutoRotate({
425
439
  acceleration = DEFAULT_AUTO_ROTATE_ACCELERATION,
426
440
  startDelay = DEFAULT_AUTO_ROTATE_START_DELAY
427
441
  }) {
428
- const controlsRef = (0, import_react2.useContext)(PanoramaControlsContext);
429
- const elapsedRef = (0, import_react2.useRef)(0);
430
- const rotationSpeedRef = (0, import_react2.useRef)(0);
442
+ const controlsRef = (0, import_react3.useContext)(PanoramaViewContext);
443
+ const eventBus = (0, import_react3.useContext)(PanoEventBusContext);
444
+ const elapsedRef = (0, import_react3.useRef)(0);
445
+ const rotationSpeedRef = (0, import_react3.useRef)(0);
446
+ const rotatingRef = (0, import_react3.useRef)(false);
447
+ const yawAccumulatedRef = (0, import_react3.useRef)(0);
431
448
  if (!controlsRef) {
432
449
  throw new Error("<AutoRotate> must be rendered inside <PanoView>.");
433
450
  }
434
- (0, import_react2.useEffect)(() => {
451
+ (0, import_react3.useEffect)(() => {
435
452
  elapsedRef.current = 0;
436
453
  rotationSpeedRef.current = 0;
437
- }, [enabled, startDelay]);
454
+ yawAccumulatedRef.current = 0;
455
+ if (rotatingRef.current) {
456
+ rotatingRef.current = false;
457
+ eventBus?.emit("autorotatestop", void 0);
458
+ }
459
+ }, [enabled, eventBus, startDelay]);
438
460
  (0, import_fiber2.useFrame)((_, deltaSeconds) => {
439
461
  if (!enabled || !Number.isFinite(speed) || speed === 0) {
440
462
  rotationSpeedRef.current = 0;
463
+ if (rotatingRef.current) {
464
+ rotatingRef.current = false;
465
+ eventBus?.emit("autorotatestop", void 0);
466
+ }
441
467
  return;
442
468
  }
443
469
  const delaySeconds = resolveStartDelay(startDelay) / 1e3;
@@ -456,17 +482,30 @@ function AutoRotate({
456
482
  const yawDelta = Math.sign(speed) * averageSpeed * deltaSeconds;
457
483
  if (!controlsRef.current?.applyAutoRotation(yawDelta)) {
458
484
  rotationSpeedRef.current = 0;
485
+ if (rotatingRef.current) {
486
+ rotatingRef.current = false;
487
+ eventBus?.emit("autorotatestop", void 0);
488
+ }
459
489
  return;
460
490
  }
491
+ if (!rotatingRef.current) {
492
+ rotatingRef.current = true;
493
+ eventBus?.emit("autorotatestart", void 0);
494
+ }
495
+ yawAccumulatedRef.current += Math.abs(yawDelta);
496
+ while (yawAccumulatedRef.current >= 360) {
497
+ yawAccumulatedRef.current -= 360;
498
+ eventBus?.emit("autorotateoneround", void 0);
499
+ }
461
500
  rotationSpeedRef.current = nextSpeed;
462
501
  });
463
502
  return null;
464
503
  }
465
504
 
466
505
  // src/hotspot/accessibility.tsx
467
- var import_react3 = require("react");
506
+ var import_react4 = require("react");
468
507
  var import_jsx_runtime = require("react/jsx-runtime");
469
- var HotspotAccessibilityContext = (0, import_react3.createContext)(null);
508
+ var HotspotAccessibilityContext = (0, import_react4.createContext)(null);
470
509
  var VISUALLY_HIDDEN_STYLE = {
471
510
  border: 0,
472
511
  clip: "rect(0 0 0 0)",
@@ -492,12 +531,12 @@ function useHotspotAccessibility({
492
531
  ariaLabel,
493
532
  onActivate
494
533
  }) {
495
- const registry = (0, import_react3.useContext)(HotspotAccessibilityContext);
496
- const [focused, setFocused] = (0, import_react3.useState)(false);
497
- const activateRef = (0, import_react3.useRef)(onActivate);
534
+ const registry = (0, import_react4.useContext)(HotspotAccessibilityContext);
535
+ const [focused, setFocused] = (0, import_react4.useState)(false);
536
+ const activateRef = (0, import_react4.useRef)(onActivate);
498
537
  const enabled = Boolean(onActivate);
499
538
  activateRef.current = onActivate;
500
- (0, import_react3.useEffect)(() => {
539
+ (0, import_react4.useEffect)(() => {
501
540
  if (!enabled) {
502
541
  setFocused(false);
503
542
  return;
@@ -519,8 +558,8 @@ function useHotspotAccessibility({
519
558
  return focused;
520
559
  }
521
560
  function useHotspotAccessibilityLayer() {
522
- const [entries, setEntries] = (0, import_react3.useState)(() => /* @__PURE__ */ new Map());
523
- const register = (0, import_react3.useCallback)(
561
+ const [entries, setEntries] = (0, import_react4.useState)(() => /* @__PURE__ */ new Map());
562
+ const register = (0, import_react4.useCallback)(
524
563
  (entry) => {
525
564
  const token = Symbol(entry.id);
526
565
  const nextEntry = { ...entry, token };
@@ -542,11 +581,11 @@ function useHotspotAccessibilityLayer() {
542
581
  },
543
582
  []
544
583
  );
545
- const registry = (0, import_react3.useMemo)(
584
+ const registry = (0, import_react4.useMemo)(
546
585
  () => ({ register }),
547
586
  [register]
548
587
  );
549
- const controls = (0, import_react3.useMemo)(
588
+ const controls = (0, import_react4.useMemo)(
550
589
  () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-label": "Panorama hotspots", style: { pointerEvents: "none" }, children: Array.from(entries.values()).map((entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
551
590
  "button",
552
591
  {
@@ -571,111 +610,1439 @@ function useHotspotAccessibilityLayer() {
571
610
  return { controls, registry };
572
611
  }
573
612
 
574
- // src/panorama-event-surface.tsx
575
- var import_three3 = require("three");
576
-
577
- // src/hotspot/coordinates.ts
578
- var import_three2 = require("three");
579
-
580
- // src/hotspot/types.ts
581
- var MAX_HOTSPOT_PITCH = 90;
582
-
583
- // src/hotspot/coordinates.ts
584
- var POLE_EPSILON_DEGREES = 1e-6;
585
- function normalizePanoYaw(yaw) {
586
- if (!Number.isFinite(yaw)) {
587
- return 0;
613
+ // src/keyboard-controls.tsx
614
+ var import_fiber3 = require("@react-three/fiber");
615
+ var import_react5 = require("react");
616
+ var DEFAULT_ROTATE_SPEED = 60;
617
+ var DEFAULT_ZOOM_SPEED = 30;
618
+ var DEFAULT_SHIFT_MULTIPLIER = 3;
619
+ var DEFAULT_KEYS = {
620
+ left: ["ArrowLeft"],
621
+ right: ["ArrowRight"],
622
+ up: ["ArrowUp"],
623
+ down: ["ArrowDown"],
624
+ zoomIn: ["+", "="],
625
+ zoomOut: ["-", "_"],
626
+ previousScene: ["[", "PageUp"],
627
+ nextScene: ["]", "PageDown"],
628
+ reset: ["0"]
629
+ };
630
+ var CONTINUOUS_ACTIONS = /* @__PURE__ */ new Set([
631
+ "left",
632
+ "right",
633
+ "up",
634
+ "down",
635
+ "zoomIn",
636
+ "zoomOut"
637
+ ]);
638
+ function normalizeBinding(value) {
639
+ if (value === void 0) {
640
+ return [];
588
641
  }
589
- return import_three2.MathUtils.euclideanModulo(yaw + 180, 360) - 180;
642
+ return (typeof value === "string" ? [value] : [...value]).filter(
643
+ (entry) => entry.length > 0
644
+ );
590
645
  }
591
- function clampPanoPitch(pitch) {
592
- if (!Number.isFinite(pitch)) {
593
- return 0;
646
+ function resolveKeyMap(overrides) {
647
+ const map = /* @__PURE__ */ new Map();
648
+ for (const action of Object.keys(DEFAULT_KEYS)) {
649
+ const bindings = overrides && action in overrides ? normalizeBinding(overrides[action]) : [...DEFAULT_KEYS[action]];
650
+ for (const binding of bindings) {
651
+ map.set(binding.toLowerCase(), action);
652
+ }
594
653
  }
595
- const clamped = import_three2.MathUtils.clamp(
596
- pitch,
597
- -MAX_HOTSPOT_PITCH,
598
- MAX_HOTSPOT_PITCH
599
- );
600
- return Math.abs(clamped) >= MAX_HOTSPOT_PITCH - POLE_EPSILON_DEGREES ? Math.sign(clamped) * MAX_HOTSPOT_PITCH : clamped;
654
+ return map;
601
655
  }
602
- function normalizePanoPosition(position) {
603
- const pitch = clampPanoPitch(position.pitch);
604
- return {
605
- yaw: Math.abs(pitch) === MAX_HOTSPOT_PITCH ? 0 : normalizePanoYaw(position.yaw),
606
- pitch
607
- };
656
+ function matchAction(event, keyMap) {
657
+ return keyMap.get(event.key.toLowerCase()) ?? keyMap.get(event.code.toLowerCase()) ?? null;
608
658
  }
609
- function panoPositionToVector3(position, radius = 1) {
610
- const normalized = normalizePanoPosition(position);
611
- const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
612
- const yaw = import_three2.MathUtils.degToRad(normalized.yaw);
613
- const pitch = import_three2.MathUtils.degToRad(normalized.pitch);
614
- const cosPitch = Math.cos(pitch);
615
- return new import_three2.Vector3(
616
- cosPitch * Math.sin(yaw) * safeRadius,
617
- Math.sin(pitch) * safeRadius,
618
- -cosPitch * Math.cos(yaw) * safeRadius
619
- );
659
+ function resolveSpeed(value, fallback) {
660
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
620
661
  }
621
- function vector3ToPanoPosition(vector) {
622
- const length = Math.hypot(vector.x, vector.y, vector.z);
623
- if (!Number.isFinite(length) || length === 0) {
624
- return { yaw: 0, pitch: 0 };
662
+ function cycleSceneId(scenes, activeSceneId, step) {
663
+ if (scenes.length === 0) {
664
+ return null;
625
665
  }
626
- return normalizePanoPosition({
627
- yaw: import_three2.MathUtils.radToDeg(Math.atan2(vector.x, -vector.z)),
628
- pitch: import_three2.MathUtils.radToDeg(
629
- Math.asin(import_three2.MathUtils.clamp(vector.y / length, -1, 1))
630
- )
631
- });
632
- }
633
-
634
- // src/panorama-radius.ts
635
- var DEFAULT_PANORAMA_RADIUS = 1e3;
636
- var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
637
- var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
638
-
639
- // src/panorama-event-surface.tsx
640
- var import_jsx_runtime2 = require("react/jsx-runtime");
641
- var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
642
- function makePanoramaPointerEvent(event) {
643
- const nativeEvent = event.nativeEvent;
644
- return {
645
- position: vector3ToPanoPosition(event.ray.direction),
646
- nativeEvent,
647
- pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
648
- button: nativeEvent.button,
649
- buttons: nativeEvent.buttons,
650
- clientX: nativeEvent.clientX,
651
- clientY: nativeEvent.clientY
652
- };
653
- }
654
- function emitPanoramaEvent(event, callback) {
655
- if (!callback) {
656
- return;
666
+ const index = scenes.findIndex((scene) => scene.id === activeSceneId);
667
+ if (index < 0) {
668
+ return null;
657
669
  }
658
- event.stopPropagation();
659
- callback(makePanoramaPointerEvent(event));
670
+ const nextIndex = (index + step + scenes.length) % scenes.length;
671
+ return scenes[nextIndex]?.id ?? null;
660
672
  }
661
- function PanoramaEventSurface({
662
- onClick,
663
- onDoubleClick,
664
- onPointerMove
673
+ function KeyboardControls({
674
+ enabled = true,
675
+ keys,
676
+ rotateSpeed = DEFAULT_ROTATE_SPEED,
677
+ zoomSpeed = DEFAULT_ZOOM_SPEED,
678
+ shiftMultiplier = DEFAULT_SHIFT_MULTIPLIER,
679
+ invert = false,
680
+ fovSpeed,
681
+ onPreviousScene,
682
+ onNextScene
665
683
  }) {
666
- if (!onClick && !onDoubleClick && !onPointerMove) {
667
- return null;
684
+ const controlsRef = (0, import_react5.useContext)(PanoramaViewContext);
685
+ const { gl } = (0, import_fiber3.useThree)();
686
+ const pressedRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
687
+ const shiftHeldRef = (0, import_react5.useRef)(false);
688
+ const invertRef = (0, import_react5.useRef)(invert);
689
+ const fovSpeedRef = (0, import_react5.useRef)(fovSpeed);
690
+ const onPreviousSceneRef = (0, import_react5.useRef)(onPreviousScene);
691
+ const onNextSceneRef = (0, import_react5.useRef)(onNextScene);
692
+ const keyMap = (0, import_react5.useMemo)(() => resolveKeyMap(keys), [keys]);
693
+ if (!controlsRef) {
694
+ throw new Error("<KeyboardControls> must be rendered inside <PanoView>.");
668
695
  }
669
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
696
+ invertRef.current = invert;
697
+ fovSpeedRef.current = fovSpeed;
698
+ onPreviousSceneRef.current = onPreviousScene;
699
+ onNextSceneRef.current = onNextScene;
700
+ (0, import_react5.useEffect)(() => {
701
+ const element = gl.domElement;
702
+ if (!enabled) {
703
+ pressedRef.current.clear();
704
+ shiftHeldRef.current = false;
705
+ controlsRef.current?.setKeyboardActive(false);
706
+ return;
707
+ }
708
+ const syncKeyboardActive = () => {
709
+ let continuous = false;
710
+ for (const action of pressedRef.current) {
711
+ if (CONTINUOUS_ACTIONS.has(action)) {
712
+ continuous = true;
713
+ break;
714
+ }
715
+ }
716
+ controlsRef.current?.setKeyboardActive(continuous);
717
+ };
718
+ const clearPressed = () => {
719
+ pressedRef.current.clear();
720
+ shiftHeldRef.current = false;
721
+ controlsRef.current?.setKeyboardActive(false);
722
+ };
723
+ const onKeyDown = (event) => {
724
+ if (event.key === "Shift") {
725
+ shiftHeldRef.current = true;
726
+ }
727
+ const action = matchAction(event, keyMap);
728
+ if (!action) {
729
+ return;
730
+ }
731
+ if (controlsRef.current?.isInteractionLocked()) {
732
+ return;
733
+ }
734
+ event.preventDefault();
735
+ if (action === "reset") {
736
+ if (!event.repeat) {
737
+ controlsRef.current?.reset();
738
+ }
739
+ return;
740
+ }
741
+ if (action === "previousScene") {
742
+ if (!event.repeat) {
743
+ onPreviousSceneRef.current?.();
744
+ }
745
+ return;
746
+ }
747
+ if (action === "nextScene") {
748
+ if (!event.repeat) {
749
+ onNextSceneRef.current?.();
750
+ }
751
+ return;
752
+ }
753
+ pressedRef.current.add(action);
754
+ syncKeyboardActive();
755
+ };
756
+ const onKeyUp = (event) => {
757
+ if (event.key === "Shift") {
758
+ shiftHeldRef.current = false;
759
+ }
760
+ const action = matchAction(event, keyMap);
761
+ if (!action || !CONTINUOUS_ACTIONS.has(action)) {
762
+ return;
763
+ }
764
+ pressedRef.current.delete(action);
765
+ syncKeyboardActive();
766
+ };
767
+ const onBlur = () => {
768
+ clearPressed();
769
+ };
770
+ element.addEventListener("keydown", onKeyDown);
771
+ element.addEventListener("keyup", onKeyUp);
772
+ element.addEventListener("blur", onBlur);
773
+ window.addEventListener("blur", onBlur);
774
+ return () => {
775
+ element.removeEventListener("keydown", onKeyDown);
776
+ element.removeEventListener("keyup", onKeyUp);
777
+ element.removeEventListener("blur", onBlur);
778
+ window.removeEventListener("blur", onBlur);
779
+ clearPressed();
780
+ };
781
+ }, [controlsRef, enabled, gl, keyMap]);
782
+ (0, import_fiber3.useFrame)((_, deltaSeconds) => {
783
+ if (!enabled || pressedRef.current.size === 0) {
784
+ return;
785
+ }
786
+ const controls = controlsRef.current;
787
+ if (!controls) {
788
+ return;
789
+ }
790
+ const multiplier = shiftHeldRef.current ? resolveSpeed(shiftMultiplier, DEFAULT_SHIFT_MULTIPLIER) : 1;
791
+ const rotation = resolveSpeed(rotateSpeed, DEFAULT_ROTATE_SPEED) * multiplier * deltaSeconds;
792
+ let zoom = resolveSpeed(zoomSpeed, DEFAULT_ZOOM_SPEED) * multiplier * deltaSeconds;
793
+ const fovCap = fovSpeedRef.current;
794
+ if (Number.isFinite(fovCap) && fovCap >= 0) {
795
+ zoom = Math.min(zoom, fovCap * deltaSeconds);
796
+ }
797
+ const pitchSign = invertRef.current ? -1 : 1;
798
+ let yaw = 0;
799
+ let pitch = 0;
800
+ let fov = 0;
801
+ for (const action of pressedRef.current) {
802
+ switch (action) {
803
+ case "left":
804
+ yaw -= rotation;
805
+ break;
806
+ case "right":
807
+ yaw += rotation;
808
+ break;
809
+ case "up":
810
+ pitch += pitchSign * rotation;
811
+ break;
812
+ case "down":
813
+ pitch -= pitchSign * rotation;
814
+ break;
815
+ case "zoomIn":
816
+ fov -= zoom;
817
+ break;
818
+ case "zoomOut":
819
+ fov += zoom;
820
+ break;
821
+ case "previousScene":
822
+ case "nextScene":
823
+ case "reset":
824
+ break;
825
+ default: {
826
+ const _exhaustive = action;
827
+ void _exhaustive;
828
+ break;
829
+ }
830
+ }
831
+ }
832
+ if (yaw !== 0 || pitch !== 0 || fov !== 0) {
833
+ controls.applyViewDelta({ yaw, pitch, fov });
834
+ }
835
+ });
836
+ return null;
837
+ }
838
+
839
+ // src/mouse-controls.tsx
840
+ var import_fiber4 = require("@react-three/fiber");
841
+ var import_react6 = require("react");
842
+ var DEFAULT_ROTATE_SPEED2 = 0.35;
843
+ var DEFAULT_ZOOM_SPEED2 = 0.08;
844
+ var BUTTON_FLAG = {
845
+ left: 0,
846
+ middle: 1,
847
+ right: 2
848
+ };
849
+ function resolveSpeed2(value, fallback) {
850
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
851
+ }
852
+ function resolveButtons(buttons) {
853
+ const list = buttons && buttons.length > 0 ? buttons : ["left"];
854
+ return new Set(list.map((button) => BUTTON_FLAG[button]));
855
+ }
856
+ function MouseControls({
857
+ enabled = true,
858
+ rotateSpeed = DEFAULT_ROTATE_SPEED2,
859
+ zoomSpeed = DEFAULT_ZOOM_SPEED2,
860
+ wheel = true,
861
+ invert = false,
862
+ buttons,
863
+ fovSpeed
864
+ }) {
865
+ const controlsRef = (0, import_react6.useContext)(PanoramaViewContext);
866
+ const { gl } = (0, import_fiber4.useThree)();
867
+ const pointersRef = (0, import_react6.useRef)(
868
+ /* @__PURE__ */ new Map()
869
+ );
870
+ const optionsRef = (0, import_react6.useRef)({
871
+ rotateSpeed,
872
+ zoomSpeed,
873
+ wheel,
874
+ invert,
875
+ fovSpeed
876
+ });
877
+ if (!controlsRef) {
878
+ throw new Error("<MouseControls> must be rendered inside <PanoView>.");
879
+ }
880
+ optionsRef.current = {
881
+ rotateSpeed,
882
+ zoomSpeed,
883
+ wheel,
884
+ invert,
885
+ fovSpeed
886
+ };
887
+ (0, import_react6.useEffect)(() => {
888
+ const element = gl.domElement;
889
+ const allowedButtons = resolveButtons(buttons);
890
+ const clearPointers = () => {
891
+ for (const pointerId of pointersRef.current.keys()) {
892
+ if (element.hasPointerCapture?.(pointerId)) {
893
+ element.releasePointerCapture?.(pointerId);
894
+ }
895
+ }
896
+ pointersRef.current.clear();
897
+ controlsRef.current?.setPointerActive("mouse", false);
898
+ };
899
+ if (!enabled) {
900
+ clearPointers();
901
+ return;
902
+ }
903
+ const isMouseLike = (event) => event.pointerType === "mouse" || event.pointerType === "pen";
904
+ const syncActive = () => {
905
+ controlsRef.current?.setPointerActive(
906
+ "mouse",
907
+ pointersRef.current.size > 0
908
+ );
909
+ };
910
+ const onPointerDown = (event) => {
911
+ if (!isMouseLike(event)) {
912
+ return;
913
+ }
914
+ if (controlsRef.current?.isInteractionLocked()) {
915
+ return;
916
+ }
917
+ if (!allowedButtons.has(event.button)) {
918
+ return;
919
+ }
920
+ event.preventDefault();
921
+ pointersRef.current.set(event.pointerId, {
922
+ x: event.clientX,
923
+ y: event.clientY,
924
+ at: performance.now()
925
+ });
926
+ element.setPointerCapture?.(event.pointerId);
927
+ syncActive();
928
+ };
929
+ const onPointerMove = (event) => {
930
+ if (!isMouseLike(event)) {
931
+ return;
932
+ }
933
+ if (controlsRef.current?.isInteractionLocked()) {
934
+ return;
935
+ }
936
+ const previous = pointersRef.current.get(event.pointerId);
937
+ if (!previous) {
938
+ return;
939
+ }
940
+ event.preventDefault();
941
+ const next = {
942
+ x: event.clientX,
943
+ y: event.clientY,
944
+ at: performance.now()
945
+ };
946
+ pointersRef.current.set(event.pointerId, next);
947
+ const rect = element.getBoundingClientRect();
948
+ const speed = resolveSpeed2(
949
+ optionsRef.current.rotateSpeed,
950
+ DEFAULT_ROTATE_SPEED2
951
+ );
952
+ const sign = optionsRef.current.invert ? -1 : 1;
953
+ const deltaYaw = sign * -(next.x - previous.x) * 360 * speed / Math.max(rect.width, 1);
954
+ const deltaPitch = sign * (next.y - previous.y) * 180 * speed / Math.max(rect.height, 1);
955
+ controlsRef.current?.applyViewDelta(
956
+ { yaw: deltaYaw, pitch: deltaPitch },
957
+ { recordVelocity: true }
958
+ );
959
+ };
960
+ const releasePointer = (event) => {
961
+ if (!pointersRef.current.has(event.pointerId)) {
962
+ return;
963
+ }
964
+ pointersRef.current.delete(event.pointerId);
965
+ if (element.hasPointerCapture?.(event.pointerId)) {
966
+ element.releasePointerCapture?.(event.pointerId);
967
+ }
968
+ syncActive();
969
+ };
970
+ const onWheel = (event) => {
971
+ if (!optionsRef.current.wheel) {
972
+ return;
973
+ }
974
+ if (controlsRef.current?.isInteractionLocked()) {
975
+ event.preventDefault();
976
+ return;
977
+ }
978
+ event.preventDefault();
979
+ const speed = resolveSpeed2(
980
+ optionsRef.current.zoomSpeed,
981
+ DEFAULT_ZOOM_SPEED2
982
+ );
983
+ let delta = event.deltaY * speed;
984
+ const cap = optionsRef.current.fovSpeed;
985
+ if (Number.isFinite(cap) && cap > 0) {
986
+ delta = Math.max(-cap, Math.min(cap, delta));
987
+ }
988
+ controlsRef.current?.applyViewDelta(
989
+ { fov: delta },
990
+ { recordVelocity: true }
991
+ );
992
+ };
993
+ element.addEventListener("pointerdown", onPointerDown);
994
+ element.addEventListener("pointermove", onPointerMove);
995
+ element.addEventListener("pointerup", releasePointer);
996
+ element.addEventListener("pointercancel", releasePointer);
997
+ element.addEventListener("wheel", onWheel, { passive: false });
998
+ return () => {
999
+ element.removeEventListener("pointerdown", onPointerDown);
1000
+ element.removeEventListener("pointermove", onPointerMove);
1001
+ element.removeEventListener("pointerup", releasePointer);
1002
+ element.removeEventListener("pointercancel", releasePointer);
1003
+ element.removeEventListener("wheel", onWheel);
1004
+ clearPointers();
1005
+ };
1006
+ }, [buttons, controlsRef, enabled, gl]);
1007
+ return null;
1008
+ }
1009
+
1010
+ // src/pano-context-menu.tsx
1011
+ var import_react7 = require("react");
1012
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1013
+ var DEFAULT_ICON_SIZE = 16;
1014
+ var MENU_EDGE_PADDING = 8;
1015
+ var MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY = 0.4;
1016
+ var PanoContextMenuOverlayContext = (0, import_react7.createContext)(null);
1017
+ var DEFAULT_APPEARANCE = {
1018
+ background: "#161616",
1019
+ color: "#f4f4f4",
1020
+ border: "1px solid #2e2e2e",
1021
+ borderRadius: 8,
1022
+ shadow: "0 8px 24px rgba(0, 0, 0, 0.45)",
1023
+ opacity: 1,
1024
+ fontSize: 13,
1025
+ minWidth: 180,
1026
+ padding: 4,
1027
+ iconSize: DEFAULT_ICON_SIZE,
1028
+ itemPadding: "6px 10px",
1029
+ itemHoverBackground: "#2a2a2a",
1030
+ itemDisabledColor: "#777777",
1031
+ separatorColor: "#333333",
1032
+ separatorMargin: "4px 6px"
1033
+ };
1034
+ function clampBackgroundOpacity(value) {
1035
+ if (!Number.isFinite(value)) {
1036
+ return DEFAULT_APPEARANCE.opacity;
1037
+ }
1038
+ return Math.min(
1039
+ 1,
1040
+ Math.max(MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY, value)
1041
+ );
1042
+ }
1043
+ function isActionItem(item) {
1044
+ return item.type !== "separator";
1045
+ }
1046
+ function DefaultContextMenuPanel({
1047
+ items,
1048
+ appearance,
1049
+ className,
1050
+ style,
1051
+ itemClassName,
1052
+ separatorClassName,
1053
+ selectContext,
1054
+ onRequestClose
1055
+ }) {
1056
+ const menuRef = (0, import_react7.useRef)(null);
1057
+ const [activeIndex, setActiveIndex] = (0, import_react7.useState)(0);
1058
+ const resolved = { ...DEFAULT_APPEARANCE, ...appearance };
1059
+ const iconSize = Number.isFinite(resolved.iconSize) ? Math.max(0, resolved.iconSize) : DEFAULT_ICON_SIZE;
1060
+ const visibleItems = (0, import_react7.useMemo)(
1061
+ () => items.filter((item) => {
1062
+ if (!isActionItem(item)) {
1063
+ return true;
1064
+ }
1065
+ return !item.hidden;
1066
+ }),
1067
+ [items]
1068
+ );
1069
+ const focusableIndices = (0, import_react7.useMemo)(() => {
1070
+ const indices = [];
1071
+ visibleItems.forEach((item, index) => {
1072
+ if (isActionItem(item) && !item.disabled) {
1073
+ indices.push(index);
1074
+ }
1075
+ });
1076
+ return indices;
1077
+ }, [visibleItems]);
1078
+ (0, import_react7.useEffect)(() => {
1079
+ const first = focusableIndices[0];
1080
+ setActiveIndex(first ?? 0);
1081
+ const frame = requestAnimationFrame(() => {
1082
+ const buttons = menuRef.current?.querySelectorAll(
1083
+ '[role="menuitem"]:not([aria-disabled="true"])'
1084
+ );
1085
+ buttons?.[0]?.focus();
1086
+ });
1087
+ return () => cancelAnimationFrame(frame);
1088
+ }, [focusableIndices]);
1089
+ const activateItem = (0, import_react7.useCallback)(
1090
+ (item) => {
1091
+ if (item.disabled) {
1092
+ return;
1093
+ }
1094
+ item.onSelect(selectContext);
1095
+ onRequestClose();
1096
+ },
1097
+ [onRequestClose, selectContext]
1098
+ );
1099
+ const moveFocus = (direction) => {
1100
+ if (focusableIndices.length === 0) {
1101
+ return;
1102
+ }
1103
+ const currentSlot = focusableIndices.indexOf(activeIndex);
1104
+ const nextSlot = currentSlot === -1 ? 0 : (currentSlot + direction + focusableIndices.length) % focusableIndices.length;
1105
+ const nextIndex = focusableIndices[nextSlot];
1106
+ setActiveIndex(nextIndex);
1107
+ const buttons = menuRef.current?.querySelectorAll(
1108
+ '[role="menuitem"]'
1109
+ );
1110
+ buttons?.[nextIndex]?.focus();
1111
+ };
1112
+ const handleKeyDown = (event) => {
1113
+ switch (event.key) {
1114
+ case "ArrowDown":
1115
+ event.preventDefault();
1116
+ moveFocus(1);
1117
+ break;
1118
+ case "ArrowUp":
1119
+ event.preventDefault();
1120
+ moveFocus(-1);
1121
+ break;
1122
+ case "Home": {
1123
+ event.preventDefault();
1124
+ const first = focusableIndices[0];
1125
+ if (first !== void 0) {
1126
+ setActiveIndex(first);
1127
+ const buttons = menuRef.current?.querySelectorAll(
1128
+ '[role="menuitem"]'
1129
+ );
1130
+ buttons?.[first]?.focus();
1131
+ }
1132
+ break;
1133
+ }
1134
+ case "End": {
1135
+ event.preventDefault();
1136
+ const last = focusableIndices[focusableIndices.length - 1];
1137
+ if (last !== void 0) {
1138
+ setActiveIndex(last);
1139
+ const buttons = menuRef.current?.querySelectorAll(
1140
+ '[role="menuitem"]'
1141
+ );
1142
+ buttons?.[last]?.focus();
1143
+ }
1144
+ break;
1145
+ }
1146
+ case "Escape":
1147
+ event.preventDefault();
1148
+ onRequestClose();
1149
+ break;
1150
+ case "Enter":
1151
+ case " ": {
1152
+ event.preventDefault();
1153
+ const item = visibleItems[activeIndex];
1154
+ if (item && isActionItem(item)) {
1155
+ activateItem(item);
1156
+ }
1157
+ break;
1158
+ }
1159
+ default: {
1160
+ break;
1161
+ }
1162
+ }
1163
+ };
1164
+ const backgroundOpacity = clampBackgroundOpacity(resolved.opacity);
1165
+ const panelStyle = {
1166
+ color: resolved.color,
1167
+ border: resolved.border,
1168
+ borderRadius: resolved.borderRadius,
1169
+ boxShadow: resolved.shadow,
1170
+ fontSize: resolved.fontSize,
1171
+ minWidth: resolved.minWidth,
1172
+ padding: resolved.padding,
1173
+ pointerEvents: "auto",
1174
+ position: "relative",
1175
+ userSelect: "none",
1176
+ ...style
1177
+ };
1178
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1179
+ "div",
1180
+ {
1181
+ ref: menuRef,
1182
+ "aria-label": "Panorama context menu",
1183
+ className,
1184
+ onKeyDown: handleKeyDown,
1185
+ role: "menu",
1186
+ style: panelStyle,
1187
+ tabIndex: -1,
1188
+ children: [
1189
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1190
+ "div",
1191
+ {
1192
+ "aria-hidden": true,
1193
+ style: {
1194
+ background: resolved.background,
1195
+ borderRadius: resolved.borderRadius,
1196
+ inset: 0,
1197
+ opacity: backgroundOpacity,
1198
+ pointerEvents: "none",
1199
+ position: "absolute",
1200
+ zIndex: 0
1201
+ }
1202
+ }
1203
+ ),
1204
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "relative", zIndex: 1 }, children: visibleItems.map((item, index) => {
1205
+ if (!isActionItem(item)) {
1206
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1207
+ "div",
1208
+ {
1209
+ "aria-hidden": true,
1210
+ className: [separatorClassName, item.className].filter(Boolean).join(" ") || void 0,
1211
+ role: "separator",
1212
+ style: {
1213
+ background: resolved.separatorColor,
1214
+ height: 1,
1215
+ margin: resolved.separatorMargin,
1216
+ ...item.style
1217
+ }
1218
+ },
1219
+ item.id ?? `separator-${index}`
1220
+ );
1221
+ }
1222
+ const disabled = item.disabled === true;
1223
+ const iconNode = item.icon ?? (item.image ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1224
+ "img",
1225
+ {
1226
+ alt: item.imageAlt ?? "",
1227
+ draggable: false,
1228
+ src: item.image,
1229
+ style: {
1230
+ display: "block",
1231
+ height: iconSize,
1232
+ objectFit: "contain",
1233
+ width: iconSize
1234
+ }
1235
+ }
1236
+ ) : null);
1237
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1238
+ "button",
1239
+ {
1240
+ "aria-disabled": disabled || void 0,
1241
+ className: [itemClassName, item.className].filter(Boolean).join(" ") || void 0,
1242
+ disabled,
1243
+ onClick: (event) => {
1244
+ event.preventDefault();
1245
+ event.stopPropagation();
1246
+ activateItem(item);
1247
+ },
1248
+ onMouseEnter: (event) => {
1249
+ if (!disabled) {
1250
+ setActiveIndex(index);
1251
+ event.currentTarget.focus();
1252
+ }
1253
+ },
1254
+ role: "menuitem",
1255
+ style: {
1256
+ alignItems: "center",
1257
+ background: !disabled && activeIndex === index ? resolved.itemHoverBackground : "transparent",
1258
+ border: "none",
1259
+ borderRadius: 4,
1260
+ color: disabled ? resolved.itemDisabledColor : "inherit",
1261
+ cursor: disabled ? "default" : "pointer",
1262
+ display: "flex",
1263
+ font: "inherit",
1264
+ gap: 8,
1265
+ padding: resolved.itemPadding,
1266
+ textAlign: "left",
1267
+ width: "100%",
1268
+ ...item.style
1269
+ },
1270
+ tabIndex: disabled ? -1 : activeIndex === index ? 0 : -1,
1271
+ type: "button",
1272
+ children: [
1273
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1274
+ "span",
1275
+ {
1276
+ "aria-hidden": true,
1277
+ style: {
1278
+ alignItems: "center",
1279
+ display: "inline-flex",
1280
+ flexShrink: 0,
1281
+ height: iconSize,
1282
+ justifyContent: "center",
1283
+ width: iconSize
1284
+ },
1285
+ children: iconNode
1286
+ }
1287
+ ),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { flex: 1 }, children: item.label })
1289
+ ]
1290
+ },
1291
+ item.id
1292
+ );
1293
+ }) })
1294
+ ]
1295
+ }
1296
+ );
1297
+ }
1298
+ function PositionedMenuShell({
1299
+ clientX,
1300
+ clientY,
1301
+ overlayElement,
1302
+ children
1303
+ }) {
1304
+ const shellRef = (0, import_react7.useRef)(null);
1305
+ const [offset, setOffset] = (0, import_react7.useState)({ left: 0, top: 0 });
1306
+ (0, import_react7.useLayoutEffect)(() => {
1307
+ const shell = shellRef.current;
1308
+ if (!overlayElement || !shell) {
1309
+ return;
1310
+ }
1311
+ const overlayRect = overlayElement.getBoundingClientRect();
1312
+ const menuRect = shell.getBoundingClientRect();
1313
+ const rawLeft = clientX - overlayRect.left;
1314
+ const rawTop = clientY - overlayRect.top;
1315
+ const maxLeft = Math.max(
1316
+ MENU_EDGE_PADDING,
1317
+ overlayRect.width - menuRect.width - MENU_EDGE_PADDING
1318
+ );
1319
+ const maxTop = Math.max(
1320
+ MENU_EDGE_PADDING,
1321
+ overlayRect.height - menuRect.height - MENU_EDGE_PADDING
1322
+ );
1323
+ setOffset({
1324
+ left: Math.min(Math.max(rawLeft, MENU_EDGE_PADDING), maxLeft),
1325
+ top: Math.min(Math.max(rawTop, MENU_EDGE_PADDING), maxTop)
1326
+ });
1327
+ }, [clientX, clientY, overlayElement, children]);
1328
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1329
+ "div",
1330
+ {
1331
+ ref: shellRef,
1332
+ style: {
1333
+ left: offset.left,
1334
+ position: "absolute",
1335
+ top: offset.top,
1336
+ zIndex: 20
1337
+ },
1338
+ children
1339
+ }
1340
+ );
1341
+ }
1342
+ function usePanoContextMenuOverlay() {
1343
+ const [session, setSession] = (0, import_react7.useState)(null);
1344
+ const overlayRef = (0, import_react7.useRef)(null);
1345
+ const [overlayElement, setOverlayElement] = (0, import_react7.useState)(
1346
+ null
1347
+ );
1348
+ const api = (0, import_react7.useMemo)(
1349
+ () => ({ setSession, overlayRef }),
1350
+ []
1351
+ );
1352
+ const setOverlayNode = (0, import_react7.useCallback)((node) => {
1353
+ overlayRef.current = node;
1354
+ setOverlayElement(node);
1355
+ }, []);
1356
+ (0, import_react7.useEffect)(() => {
1357
+ if (!session) {
1358
+ return;
1359
+ }
1360
+ const handlePointerDown = (event) => {
1361
+ const overlay2 = overlayRef.current;
1362
+ if (!overlay2) {
1363
+ return;
1364
+ }
1365
+ const target = event.target;
1366
+ if (target instanceof Node && overlay2.contains(target)) {
1367
+ const menuRoot = overlay2.querySelector(
1368
+ '[role="menu"], [data-pano-context-menu]'
1369
+ );
1370
+ if (menuRoot && menuRoot.contains(target)) {
1371
+ return;
1372
+ }
1373
+ }
1374
+ setSession(null);
1375
+ };
1376
+ const handleKeyDown = (event) => {
1377
+ if (event.key === "Escape") {
1378
+ setSession(null);
1379
+ }
1380
+ };
1381
+ document.addEventListener("pointerdown", handlePointerDown, true);
1382
+ document.addEventListener("keydown", handleKeyDown);
1383
+ return () => {
1384
+ document.removeEventListener("pointerdown", handlePointerDown, true);
1385
+ document.removeEventListener("keydown", handleKeyDown);
1386
+ };
1387
+ }, [session]);
1388
+ const overlay = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1389
+ "div",
1390
+ {
1391
+ ref: setOverlayNode,
1392
+ "aria-hidden": session ? void 0 : true,
1393
+ style: {
1394
+ inset: 0,
1395
+ pointerEvents: "none",
1396
+ position: "absolute",
1397
+ zIndex: 20
1398
+ },
1399
+ children: session ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1400
+ PositionedMenuShell,
1401
+ {
1402
+ clientX: session.event.clientX,
1403
+ clientY: session.event.clientY,
1404
+ overlayElement,
1405
+ children: session.render()
1406
+ },
1407
+ session.key
1408
+ ) : null
1409
+ }
1410
+ );
1411
+ return { api, overlay };
1412
+ }
1413
+ function PanoContextMenu({
1414
+ items,
1415
+ prepend,
1416
+ append,
1417
+ appearance,
1418
+ className,
1419
+ style,
1420
+ itemClassName,
1421
+ separatorClassName,
1422
+ children
1423
+ }) {
1424
+ const eventBus = (0, import_react7.useContext)(PanoEventBusContext);
1425
+ const controlsRef = (0, import_react7.useContext)(PanoramaViewContext);
1426
+ const overlayApi = (0, import_react7.useContext)(PanoContextMenuOverlayContext);
1427
+ const actions = (0, import_react7.useContext)(PanoContextMenuActionsContext);
1428
+ const instanceId = (0, import_react7.useId)();
1429
+ const sessionKeyRef = (0, import_react7.useRef)(0);
1430
+ const propsRef = (0, import_react7.useRef)({
1431
+ items,
1432
+ prepend,
1433
+ append,
1434
+ appearance,
1435
+ className,
1436
+ style,
1437
+ itemClassName,
1438
+ separatorClassName,
1439
+ children
1440
+ });
1441
+ propsRef.current = {
1442
+ items,
1443
+ prepend,
1444
+ append,
1445
+ appearance,
1446
+ className,
1447
+ style,
1448
+ itemClassName,
1449
+ separatorClassName,
1450
+ children
1451
+ };
1452
+ if (!eventBus || !controlsRef) {
1453
+ throw new Error("<PanoContextMenu> must be rendered inside <PanoView>.");
1454
+ }
1455
+ if (!overlayApi) {
1456
+ throw new Error(
1457
+ "<PanoContextMenu> requires the PanoView context-menu overlay host."
1458
+ );
1459
+ }
1460
+ const close = (0, import_react7.useCallback)(() => {
1461
+ overlayApi.setSession(null);
1462
+ }, [overlayApi]);
1463
+ const openMenu = (0, import_react7.useCallback)(
1464
+ (event) => {
1465
+ event.nativeEvent.preventDefault();
1466
+ const view = controlsRef.current?.getView() ?? {
1467
+ yaw: 0,
1468
+ pitch: 0,
1469
+ fov: 75
1470
+ };
1471
+ const selectContext = {
1472
+ position: event.position,
1473
+ view,
1474
+ nativeEvent: event.nativeEvent,
1475
+ close
1476
+ };
1477
+ const current = propsRef.current;
1478
+ sessionKeyRef.current += 1;
1479
+ const key = `${instanceId}-${sessionKeyRef.current}`;
1480
+ const hasCustomChildren = typeof current.children === "function";
1481
+ const presets = actions ? createPanoContextMenuPresets(actions) : null;
1482
+ const resolvedItems = presets ? composePanoContextMenuItems(current, presets) : current.items?.filter(isActionEntry) ?? [];
1483
+ if (!hasCustomChildren && resolvedItems.length === 0) {
1484
+ return;
1485
+ }
1486
+ overlayApi.setSession({
1487
+ key,
1488
+ event,
1489
+ view,
1490
+ render: () => {
1491
+ if (hasCustomChildren && current.children) {
1492
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-pano-context-menu": true, style: { pointerEvents: "auto" }, children: current.children({ event, view, close }) });
1493
+ }
1494
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1495
+ DefaultContextMenuPanel,
1496
+ {
1497
+ appearance: current.appearance,
1498
+ className: current.className,
1499
+ itemClassName: current.itemClassName,
1500
+ items: resolvedItems,
1501
+ onRequestClose: close,
1502
+ selectContext,
1503
+ separatorClassName: current.separatorClassName,
1504
+ style: current.style
1505
+ }
1506
+ );
1507
+ }
1508
+ });
1509
+ },
1510
+ [actions, close, controlsRef, instanceId, overlayApi]
1511
+ );
1512
+ (0, import_react7.useEffect)(() => {
1513
+ const unsubscribeContextMenu = eventBus.subscribe(
1514
+ "contextmenu",
1515
+ openMenu
1516
+ );
1517
+ const unsubscribeInteraction = eventBus.subscribe(
1518
+ "viewinteractionstart",
1519
+ () => {
1520
+ close();
1521
+ }
1522
+ );
1523
+ return () => {
1524
+ unsubscribeContextMenu();
1525
+ unsubscribeInteraction();
1526
+ close();
1527
+ };
1528
+ }, [close, eventBus, openMenu]);
1529
+ return null;
1530
+ }
1531
+ function ResetViewIcon() {
1532
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1533
+ "svg",
1534
+ {
1535
+ "aria-hidden": true,
1536
+ fill: "none",
1537
+ height: "100%",
1538
+ stroke: "currentColor",
1539
+ strokeLinecap: "round",
1540
+ strokeLinejoin: "round",
1541
+ strokeWidth: "1.75",
1542
+ viewBox: "0 0 16 16",
1543
+ width: "100%",
1544
+ children: [
1545
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3.5 6.5A4.5 4.5 0 1 1 4 10" }),
1546
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3.5 3.5v3h3" })
1547
+ ]
1548
+ }
1549
+ );
1550
+ }
1551
+ function FullscreenIcon() {
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1553
+ "svg",
1554
+ {
1555
+ "aria-hidden": true,
1556
+ fill: "none",
1557
+ height: "100%",
1558
+ stroke: "currentColor",
1559
+ strokeLinecap: "round",
1560
+ strokeLinejoin: "round",
1561
+ strokeWidth: "1.75",
1562
+ viewBox: "0 0 16 16",
1563
+ width: "100%",
1564
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 6V3h3M10 3h3v3M13 10v3h-3M6 13H3v-3" })
1565
+ }
1566
+ );
1567
+ }
1568
+ function ExitFullscreenIcon() {
1569
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1570
+ "svg",
1571
+ {
1572
+ "aria-hidden": true,
1573
+ fill: "none",
1574
+ height: "100%",
1575
+ stroke: "currentColor",
1576
+ strokeLinecap: "round",
1577
+ strokeLinejoin: "round",
1578
+ strokeWidth: "1.75",
1579
+ viewBox: "0 0 16 16",
1580
+ width: "100%",
1581
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M6 3v3H3M10 3v3h3M3 10h3v3M10 10h3v3" })
1582
+ }
1583
+ );
1584
+ }
1585
+ function createPanoContextMenuPresets(actions) {
1586
+ const isFullscreen = actions.isFullscreen === true;
1587
+ const resetView = {
1588
+ id: "pano-reset-view",
1589
+ label: "Reset view",
1590
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ResetViewIcon, {}),
1591
+ onSelect: () => {
1592
+ actions.reset();
1593
+ }
1594
+ };
1595
+ const fullscreen = {
1596
+ id: "pano-fullscreen",
1597
+ label: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
1598
+ icon: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ExitFullscreenIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FullscreenIcon, {}),
1599
+ onSelect: () => {
1600
+ actions.toggleFullscreen();
1601
+ }
1602
+ };
1603
+ const separator = (id = "pano-separator") => ({
1604
+ type: "separator",
1605
+ id
1606
+ });
1607
+ return {
1608
+ resetView,
1609
+ fullscreen,
1610
+ separator,
1611
+ defaults: [resetView, separator("pano-default-separator"), fullscreen]
1612
+ };
1613
+ }
1614
+ function createDefaultPanoContextMenuItems(actions) {
1615
+ return createPanoContextMenuPresets(actions).defaults;
1616
+ }
1617
+ function isPresetId(value) {
1618
+ return value === "resetView" || value === "fullscreen" || value === "separator";
1619
+ }
1620
+ function isPresetRef(value) {
1621
+ return typeof value === "object" && value !== null && "preset" in value && isPresetId(value.preset);
1622
+ }
1623
+ function isActionEntry(value) {
1624
+ if (typeof value === "string") {
1625
+ return false;
1626
+ }
1627
+ if (isPresetRef(value)) {
1628
+ return false;
1629
+ }
1630
+ return true;
1631
+ }
1632
+ function applyPresetOverrides(item, overrides) {
1633
+ if (item.type === "separator") {
1634
+ return {
1635
+ ...item,
1636
+ id: overrides.id ?? item.id,
1637
+ className: overrides.className ?? item.className,
1638
+ style: overrides.style ?? item.style
1639
+ };
1640
+ }
1641
+ return {
1642
+ ...item,
1643
+ id: overrides.id ?? item.id,
1644
+ label: overrides.label ?? item.label,
1645
+ className: overrides.className ?? item.className,
1646
+ style: overrides.style ?? item.style,
1647
+ disabled: overrides.disabled ?? item.disabled,
1648
+ hidden: overrides.hidden ?? item.hidden
1649
+ };
1650
+ }
1651
+ function resolvePresetEntry(entry, presets) {
1652
+ const presetId = typeof entry === "string" ? entry : entry.preset;
1653
+ const overrides = typeof entry === "string" ? {} : entry;
1654
+ switch (presetId) {
1655
+ case "resetView":
1656
+ return applyPresetOverrides(presets.resetView, overrides);
1657
+ case "fullscreen":
1658
+ return applyPresetOverrides(presets.fullscreen, overrides);
1659
+ case "separator":
1660
+ return applyPresetOverrides(
1661
+ presets.separator(overrides.id ?? "pano-separator"),
1662
+ overrides
1663
+ );
1664
+ default: {
1665
+ const _exhaustive = presetId;
1666
+ return _exhaustive;
1667
+ }
1668
+ }
1669
+ }
1670
+ function resolvePanoContextMenuEntries(entries, presets) {
1671
+ const resolved = [];
1672
+ for (const entry of entries) {
1673
+ if (isActionEntry(entry)) {
1674
+ resolved.push(entry);
1675
+ continue;
1676
+ }
1677
+ resolved.push(resolvePresetEntry(entry, presets));
1678
+ }
1679
+ return resolved;
1680
+ }
1681
+ function composePanoContextMenuItems(props, presets) {
1682
+ if (props.items) {
1683
+ return resolvePanoContextMenuEntries(props.items, presets);
1684
+ }
1685
+ return [
1686
+ ...resolvePanoContextMenuEntries(props.prepend ?? [], presets),
1687
+ ...presets.defaults,
1688
+ ...resolvePanoContextMenuEntries(props.append ?? [], presets)
1689
+ ];
1690
+ }
1691
+ var PanoContextMenuActionsContext = (0, import_react7.createContext)(null);
1692
+
1693
+ // src/pano-events.tsx
1694
+ var import_fiber5 = require("@react-three/fiber");
1695
+ var import_react8 = require("react");
1696
+ var DEFAULT_IDLE_TIME_MS = 2e3;
1697
+ function resolveIdleTime(value) {
1698
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_IDLE_TIME_MS;
1699
+ }
1700
+ function usePanoEvents({
1701
+ idleTime = DEFAULT_IDLE_TIME_MS,
1702
+ onViewChange,
1703
+ onViewSettled,
1704
+ onViewInteractionStart,
1705
+ onViewInteractionEnd,
1706
+ onClick,
1707
+ onDoubleClick,
1708
+ onPointerDown,
1709
+ onPointerUp,
1710
+ onPointerMove,
1711
+ onContextMenu,
1712
+ onWheel,
1713
+ onIdle,
1714
+ onIdleEnd,
1715
+ onEnterFullscreen,
1716
+ onExitFullscreen,
1717
+ onResize,
1718
+ onAutoRotateStart,
1719
+ onAutoRotateStop,
1720
+ onAutoRotateOneRound
1721
+ }) {
1722
+ const eventBus = (0, import_react8.useContext)(PanoEventBusContext);
1723
+ const controlsRef = (0, import_react8.useContext)(PanoramaViewContext);
1724
+ const { gl } = (0, import_fiber5.useThree)();
1725
+ if (!eventBus || !controlsRef) {
1726
+ throw new Error("usePanoEvents must be used inside <PanoView>.");
1727
+ }
1728
+ const callbacksRef = (0, import_react8.useRef)({
1729
+ onViewChange,
1730
+ onViewSettled,
1731
+ onViewInteractionStart,
1732
+ onViewInteractionEnd,
1733
+ onClick,
1734
+ onDoubleClick,
1735
+ onPointerDown,
1736
+ onPointerUp,
1737
+ onPointerMove,
1738
+ onContextMenu,
1739
+ onWheel,
1740
+ onIdle,
1741
+ onIdleEnd,
1742
+ onEnterFullscreen,
1743
+ onExitFullscreen,
1744
+ onResize,
1745
+ onAutoRotateStart,
1746
+ onAutoRotateStop,
1747
+ onAutoRotateOneRound
1748
+ });
1749
+ callbacksRef.current = {
1750
+ onViewChange,
1751
+ onViewSettled,
1752
+ onViewInteractionStart,
1753
+ onViewInteractionEnd,
1754
+ onClick,
1755
+ onDoubleClick,
1756
+ onPointerDown,
1757
+ onPointerUp,
1758
+ onPointerMove,
1759
+ onContextMenu,
1760
+ onWheel,
1761
+ onIdle,
1762
+ onIdleEnd,
1763
+ onEnterFullscreen,
1764
+ onExitFullscreen,
1765
+ onResize,
1766
+ onAutoRotateStart,
1767
+ onAutoRotateStop,
1768
+ onAutoRotateOneRound
1769
+ };
1770
+ const idleElapsedRef = (0, import_react8.useRef)(0);
1771
+ const isIdleRef = (0, import_react8.useRef)(false);
1772
+ const idleTimeRef = (0, import_react8.useRef)(resolveIdleTime(idleTime));
1773
+ idleTimeRef.current = resolveIdleTime(idleTime);
1774
+ (0, import_react8.useEffect)(() => {
1775
+ idleElapsedRef.current = 0;
1776
+ isIdleRef.current = false;
1777
+ }, [idleTime]);
1778
+ (0, import_react8.useEffect)(() => {
1779
+ const unsubscribers = [
1780
+ eventBus.subscribe("viewchange", (view) => {
1781
+ callbacksRef.current.onViewChange?.(view);
1782
+ }),
1783
+ eventBus.subscribe("viewsettled", (view) => {
1784
+ callbacksRef.current.onViewSettled?.(view);
1785
+ }),
1786
+ eventBus.subscribe("viewinteractionstart", (event) => {
1787
+ callbacksRef.current.onViewInteractionStart?.(event);
1788
+ }),
1789
+ eventBus.subscribe("viewinteractionend", (event) => {
1790
+ callbacksRef.current.onViewInteractionEnd?.(event);
1791
+ }),
1792
+ eventBus.subscribe("click", (event) => {
1793
+ callbacksRef.current.onClick?.(event);
1794
+ }),
1795
+ eventBus.subscribe("doubleclick", (event) => {
1796
+ callbacksRef.current.onDoubleClick?.(event);
1797
+ }),
1798
+ eventBus.subscribe("pointerdown", (event) => {
1799
+ callbacksRef.current.onPointerDown?.(event);
1800
+ }),
1801
+ eventBus.subscribe("pointerup", (event) => {
1802
+ callbacksRef.current.onPointerUp?.(event);
1803
+ }),
1804
+ eventBus.subscribe("pointermove", (event) => {
1805
+ callbacksRef.current.onPointerMove?.(event);
1806
+ }),
1807
+ eventBus.subscribe("contextmenu", (event) => {
1808
+ callbacksRef.current.onContextMenu?.(event);
1809
+ }),
1810
+ eventBus.subscribe("autorotatestart", () => {
1811
+ callbacksRef.current.onAutoRotateStart?.();
1812
+ }),
1813
+ eventBus.subscribe("autorotatestop", () => {
1814
+ callbacksRef.current.onAutoRotateStop?.();
1815
+ }),
1816
+ eventBus.subscribe("autorotateoneround", () => {
1817
+ callbacksRef.current.onAutoRotateOneRound?.();
1818
+ })
1819
+ ];
1820
+ return () => {
1821
+ for (const unsubscribe of unsubscribers) {
1822
+ unsubscribe();
1823
+ }
1824
+ };
1825
+ }, [eventBus]);
1826
+ (0, import_react8.useEffect)(() => {
1827
+ const element = gl.domElement;
1828
+ const handleWheel = (nativeEvent) => {
1829
+ callbacksRef.current.onWheel?.({
1830
+ delta: nativeEvent.deltaY,
1831
+ nativeEvent
1832
+ });
1833
+ };
1834
+ element.addEventListener("wheel", handleWheel, { passive: true });
1835
+ return () => {
1836
+ element.removeEventListener("wheel", handleWheel);
1837
+ };
1838
+ }, [gl]);
1839
+ (0, import_react8.useEffect)(() => {
1840
+ if (typeof document === "undefined") {
1841
+ return;
1842
+ }
1843
+ const wasFullscreenRef = { current: false };
1844
+ const isViewerFullscreen = () => {
1845
+ const fullscreenElement = document.fullscreenElement;
1846
+ if (!fullscreenElement) {
1847
+ return false;
1848
+ }
1849
+ const canvas = gl.domElement;
1850
+ return fullscreenElement === canvas || fullscreenElement.contains(canvas) || canvas.contains(fullscreenElement);
1851
+ };
1852
+ const handleFullscreenChange = () => {
1853
+ const isOurs = isViewerFullscreen();
1854
+ if (isOurs && !wasFullscreenRef.current) {
1855
+ wasFullscreenRef.current = true;
1856
+ callbacksRef.current.onEnterFullscreen?.();
1857
+ } else if (!isOurs && wasFullscreenRef.current) {
1858
+ wasFullscreenRef.current = false;
1859
+ callbacksRef.current.onExitFullscreen?.();
1860
+ }
1861
+ };
1862
+ wasFullscreenRef.current = isViewerFullscreen();
1863
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
1864
+ return () => {
1865
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
1866
+ };
1867
+ }, [gl]);
1868
+ (0, import_react8.useEffect)(() => {
1869
+ const element = gl.domElement;
1870
+ if (typeof ResizeObserver === "undefined") {
1871
+ return;
1872
+ }
1873
+ const observer = new ResizeObserver((entries) => {
1874
+ const entry = entries[0];
1875
+ if (!entry) {
1876
+ return;
1877
+ }
1878
+ const { width, height } = entry.contentRect;
1879
+ callbacksRef.current.onResize?.({ width, height });
1880
+ });
1881
+ observer.observe(element);
1882
+ return () => {
1883
+ observer.disconnect();
1884
+ };
1885
+ }, [gl]);
1886
+ (0, import_fiber5.useFrame)((_, deltaSeconds) => {
1887
+ const interacting = controlsRef.current?.isUserInteracting() ?? false;
1888
+ if (interacting) {
1889
+ idleElapsedRef.current = 0;
1890
+ if (isIdleRef.current) {
1891
+ isIdleRef.current = false;
1892
+ callbacksRef.current.onIdleEnd?.();
1893
+ }
1894
+ return;
1895
+ }
1896
+ idleElapsedRef.current += deltaSeconds * 1e3;
1897
+ if (!isIdleRef.current && idleElapsedRef.current >= idleTimeRef.current) {
1898
+ isIdleRef.current = true;
1899
+ callbacksRef.current.onIdle?.();
1900
+ }
1901
+ });
1902
+ }
1903
+ function PanoEvents(props) {
1904
+ usePanoEvents(props);
1905
+ return null;
1906
+ }
1907
+
1908
+ // src/panorama-event-surface.tsx
1909
+ var import_fiber6 = require("@react-three/fiber");
1910
+ var import_react9 = require("react");
1911
+ var import_three3 = require("three");
1912
+
1913
+ // src/hotspot/coordinates.ts
1914
+ var import_three2 = require("three");
1915
+
1916
+ // src/hotspot/types.ts
1917
+ var MAX_HOTSPOT_PITCH = 90;
1918
+
1919
+ // src/hotspot/coordinates.ts
1920
+ var POLE_EPSILON_DEGREES = 1e-6;
1921
+ function normalizePanoYaw(yaw) {
1922
+ if (!Number.isFinite(yaw)) {
1923
+ return 0;
1924
+ }
1925
+ return import_three2.MathUtils.euclideanModulo(yaw + 180, 360) - 180;
1926
+ }
1927
+ function clampPanoPitch(pitch) {
1928
+ if (!Number.isFinite(pitch)) {
1929
+ return 0;
1930
+ }
1931
+ const clamped = import_three2.MathUtils.clamp(
1932
+ pitch,
1933
+ -MAX_HOTSPOT_PITCH,
1934
+ MAX_HOTSPOT_PITCH
1935
+ );
1936
+ return Math.abs(clamped) >= MAX_HOTSPOT_PITCH - POLE_EPSILON_DEGREES ? Math.sign(clamped) * MAX_HOTSPOT_PITCH : clamped;
1937
+ }
1938
+ function normalizePanoPosition(position) {
1939
+ const pitch = clampPanoPitch(position.pitch);
1940
+ return {
1941
+ yaw: Math.abs(pitch) === MAX_HOTSPOT_PITCH ? 0 : normalizePanoYaw(position.yaw),
1942
+ pitch
1943
+ };
1944
+ }
1945
+ function panoPositionToVector3(position, radius = 1) {
1946
+ const normalized = normalizePanoPosition(position);
1947
+ const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
1948
+ const yaw = import_three2.MathUtils.degToRad(normalized.yaw);
1949
+ const pitch = import_three2.MathUtils.degToRad(normalized.pitch);
1950
+ const cosPitch = Math.cos(pitch);
1951
+ return new import_three2.Vector3(
1952
+ cosPitch * Math.sin(yaw) * safeRadius,
1953
+ Math.sin(pitch) * safeRadius,
1954
+ -cosPitch * Math.cos(yaw) * safeRadius
1955
+ );
1956
+ }
1957
+ function vector3ToPanoPosition(vector) {
1958
+ const length = Math.hypot(vector.x, vector.y, vector.z);
1959
+ if (!Number.isFinite(length) || length === 0) {
1960
+ return { yaw: 0, pitch: 0 };
1961
+ }
1962
+ return normalizePanoPosition({
1963
+ yaw: import_three2.MathUtils.radToDeg(Math.atan2(vector.x, -vector.z)),
1964
+ pitch: import_three2.MathUtils.radToDeg(
1965
+ Math.asin(import_three2.MathUtils.clamp(vector.y / length, -1, 1))
1966
+ )
1967
+ });
1968
+ }
1969
+
1970
+ // src/panorama-radius.ts
1971
+ var DEFAULT_PANORAMA_RADIUS = 1e3;
1972
+ var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
1973
+ var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
1974
+
1975
+ // src/panorama-event-surface.tsx
1976
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1977
+ var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
1978
+ function makePanoramaPointerEvent(event) {
1979
+ const nativeEvent = event.nativeEvent;
1980
+ return {
1981
+ position: vector3ToPanoPosition(event.ray.direction),
1982
+ nativeEvent,
1983
+ pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
1984
+ button: nativeEvent.button,
1985
+ buttons: nativeEvent.buttons,
1986
+ clientX: nativeEvent.clientX,
1987
+ clientY: nativeEvent.clientY
1988
+ };
1989
+ }
1990
+ function PanoramaEventSurface() {
1991
+ const eventBus = (0, import_react9.useContext)(PanoEventBusContext);
1992
+ const { camera, gl } = (0, import_fiber6.useThree)();
1993
+ (0, import_react9.useEffect)(() => {
1994
+ if (!eventBus) {
1995
+ return;
1996
+ }
1997
+ const element = gl.domElement;
1998
+ const ndc = new import_three3.Vector2();
1999
+ const raycaster = new import_three3.Raycaster();
2000
+ const direction = new import_three3.Vector3();
2001
+ const handleContextMenu = (nativeEvent) => {
2002
+ const rect = element.getBoundingClientRect();
2003
+ if (rect.width <= 0 || rect.height <= 0) {
2004
+ return;
2005
+ }
2006
+ ndc.set(
2007
+ (nativeEvent.clientX - rect.left) / rect.width * 2 - 1,
2008
+ -((nativeEvent.clientY - rect.top) / rect.height) * 2 + 1
2009
+ );
2010
+ raycaster.setFromCamera(ndc, camera);
2011
+ direction.copy(raycaster.ray.direction);
2012
+ eventBus.emit("contextmenu", {
2013
+ position: vector3ToPanoPosition(direction),
2014
+ nativeEvent,
2015
+ pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
2016
+ button: nativeEvent.button,
2017
+ buttons: nativeEvent.buttons,
2018
+ clientX: nativeEvent.clientX,
2019
+ clientY: nativeEvent.clientY
2020
+ });
2021
+ };
2022
+ element.addEventListener("contextmenu", handleContextMenu);
2023
+ return () => {
2024
+ element.removeEventListener("contextmenu", handleContextMenu);
2025
+ };
2026
+ }, [camera, eventBus, gl]);
2027
+ if (!eventBus) {
2028
+ return null;
2029
+ }
2030
+ const emit = (type, event) => {
2031
+ event.stopPropagation();
2032
+ eventBus.emit(type, makePanoramaPointerEvent(event));
2033
+ };
2034
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
670
2035
  "mesh",
671
2036
  {
672
2037
  renderOrder: -200,
673
- onClick: (event) => emitPanoramaEvent(event, onClick),
674
- onDoubleClick: (event) => emitPanoramaEvent(event, onDoubleClick),
675
- onPointerMove: (event) => emitPanoramaEvent(event, onPointerMove),
2038
+ onClick: (event) => emit("click", event),
2039
+ onDoubleClick: (event) => emit("doubleclick", event),
2040
+ onPointerDown: (event) => emit("pointerdown", event),
2041
+ onPointerUp: (event) => emit("pointerup", event),
2042
+ onPointerMove: (event) => emit("pointermove", event),
676
2043
  children: [
677
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
678
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2044
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
2045
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
679
2046
  "meshBasicMaterial",
680
2047
  {
681
2048
  colorWrite: false,
@@ -685,31 +2052,205 @@ function PanoramaEventSurface({
685
2052
  side: import_three3.BackSide,
686
2053
  transparent: true
687
2054
  }
688
- )
689
- ]
690
- }
691
- );
2055
+ )
2056
+ ]
2057
+ }
2058
+ );
2059
+ }
2060
+
2061
+ // src/touch-controls.tsx
2062
+ var import_fiber7 = require("@react-three/fiber");
2063
+ var import_react10 = require("react");
2064
+ var DEFAULT_ROTATE_SPEED3 = 0.35;
2065
+ function resolveSpeed3(value, fallback) {
2066
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
2067
+ }
2068
+ function pointerDistance(a, b) {
2069
+ return Math.hypot(a.x - b.x, a.y - b.y);
2070
+ }
2071
+ function TouchControls({
2072
+ enabled = true,
2073
+ rotateSpeed = DEFAULT_ROTATE_SPEED3,
2074
+ invert = false,
2075
+ pinchZoom = true
2076
+ }) {
2077
+ const controlsRef = (0, import_react10.useContext)(PanoramaViewContext);
2078
+ const { gl } = (0, import_fiber7.useThree)();
2079
+ const pointersRef = (0, import_react10.useRef)(/* @__PURE__ */ new Map());
2080
+ const pinchDistanceRef = (0, import_react10.useRef)(null);
2081
+ const optionsRef = (0, import_react10.useRef)({ rotateSpeed, invert, pinchZoom });
2082
+ if (!controlsRef) {
2083
+ throw new Error("<TouchControls> must be rendered inside <PanoView>.");
2084
+ }
2085
+ optionsRef.current = { rotateSpeed, invert, pinchZoom };
2086
+ (0, import_react10.useEffect)(() => {
2087
+ const element = gl.domElement;
2088
+ const clearPointers = () => {
2089
+ for (const pointerId of pointersRef.current.keys()) {
2090
+ if (element.hasPointerCapture?.(pointerId)) {
2091
+ element.releasePointerCapture?.(pointerId);
2092
+ }
2093
+ }
2094
+ pointersRef.current.clear();
2095
+ pinchDistanceRef.current = null;
2096
+ controlsRef.current?.setPointerActive("touch", false);
2097
+ };
2098
+ if (!enabled) {
2099
+ clearPointers();
2100
+ return;
2101
+ }
2102
+ const syncActive = () => {
2103
+ controlsRef.current?.setPointerActive(
2104
+ "touch",
2105
+ pointersRef.current.size > 0
2106
+ );
2107
+ };
2108
+ const onPointerDown = (event) => {
2109
+ if (event.pointerType !== "touch") {
2110
+ return;
2111
+ }
2112
+ if (controlsRef.current?.isInteractionLocked()) {
2113
+ return;
2114
+ }
2115
+ event.preventDefault();
2116
+ pointersRef.current.set(event.pointerId, {
2117
+ x: event.clientX,
2118
+ y: event.clientY,
2119
+ at: performance.now()
2120
+ });
2121
+ element.setPointerCapture?.(event.pointerId);
2122
+ syncActive();
2123
+ if (pointersRef.current.size === 2) {
2124
+ const [first, second] = Array.from(pointersRef.current.values());
2125
+ pinchDistanceRef.current = pointerDistance(first, second);
2126
+ }
2127
+ };
2128
+ const onPointerMove = (event) => {
2129
+ if (event.pointerType !== "touch") {
2130
+ return;
2131
+ }
2132
+ if (controlsRef.current?.isInteractionLocked()) {
2133
+ return;
2134
+ }
2135
+ const previous = pointersRef.current.get(event.pointerId);
2136
+ if (!previous) {
2137
+ return;
2138
+ }
2139
+ event.preventDefault();
2140
+ const next = {
2141
+ x: event.clientX,
2142
+ y: event.clientY,
2143
+ at: performance.now()
2144
+ };
2145
+ pointersRef.current.set(event.pointerId, next);
2146
+ const pointers = Array.from(pointersRef.current.values());
2147
+ if (pointers.length >= 2) {
2148
+ if (!optionsRef.current.pinchZoom) {
2149
+ return;
2150
+ }
2151
+ const distance = pointerDistance(pointers[0], pointers[1]);
2152
+ const previousDistance = pinchDistanceRef.current;
2153
+ pinchDistanceRef.current = distance;
2154
+ if (previousDistance && distance > 0) {
2155
+ const view = controlsRef.current?.getTargetView();
2156
+ if (view) {
2157
+ const nextFov = view.fov * (previousDistance / distance);
2158
+ controlsRef.current?.applyViewDelta(
2159
+ { fov: nextFov - view.fov },
2160
+ { recordVelocity: true }
2161
+ );
2162
+ }
2163
+ }
2164
+ return;
2165
+ }
2166
+ const rect = element.getBoundingClientRect();
2167
+ const speed = resolveSpeed3(
2168
+ optionsRef.current.rotateSpeed,
2169
+ DEFAULT_ROTATE_SPEED3
2170
+ );
2171
+ const sign = optionsRef.current.invert ? -1 : 1;
2172
+ const deltaYaw = sign * -(next.x - previous.x) * 360 * speed / Math.max(rect.width, 1);
2173
+ const deltaPitch = sign * (next.y - previous.y) * 180 * speed / Math.max(rect.height, 1);
2174
+ controlsRef.current?.applyViewDelta(
2175
+ { yaw: deltaYaw, pitch: deltaPitch },
2176
+ { recordVelocity: true }
2177
+ );
2178
+ };
2179
+ const releasePointer = (event) => {
2180
+ if (!pointersRef.current.has(event.pointerId)) {
2181
+ return;
2182
+ }
2183
+ pointersRef.current.delete(event.pointerId);
2184
+ if (element.hasPointerCapture?.(event.pointerId)) {
2185
+ element.releasePointerCapture?.(event.pointerId);
2186
+ }
2187
+ if (pointersRef.current.size < 2) {
2188
+ pinchDistanceRef.current = null;
2189
+ }
2190
+ syncActive();
2191
+ };
2192
+ element.addEventListener("pointerdown", onPointerDown);
2193
+ element.addEventListener("pointermove", onPointerMove);
2194
+ element.addEventListener("pointerup", releasePointer);
2195
+ element.addEventListener("pointercancel", releasePointer);
2196
+ return () => {
2197
+ element.removeEventListener("pointerdown", onPointerDown);
2198
+ element.removeEventListener("pointermove", onPointerMove);
2199
+ element.removeEventListener("pointerup", releasePointer);
2200
+ element.removeEventListener("pointercancel", releasePointer);
2201
+ clearPointers();
2202
+ };
2203
+ }, [controlsRef, enabled, gl]);
2204
+ return null;
692
2205
  }
693
2206
 
694
2207
  // src/pano-view.tsx
695
- var import_jsx_runtime3 = require("react/jsx-runtime");
2208
+ var import_jsx_runtime4 = require("react/jsx-runtime");
696
2209
  var DEFAULT_VIEW = {
697
2210
  yaw: 0,
698
2211
  pitch: 0,
699
2212
  fov: 75
700
2213
  };
2214
+ var DEFAULT_POINTER_ROTATE_SPEED = 0.35;
2215
+ var DEFAULT_MOUSE_ZOOM_SPEED = 0.08;
701
2216
  var DEFAULT_CANVAS_STYLE = {
702
2217
  display: "block",
703
2218
  height: "100%",
704
2219
  width: "100%"
705
2220
  };
706
- var PanoView = (0, import_react4.forwardRef)(
2221
+ function resolveChannel(value, defaults) {
2222
+ if (value === false) {
2223
+ return { mount: false };
2224
+ }
2225
+ if (value === true || value === void 0) {
2226
+ return { mount: true, props: { ...defaults } };
2227
+ }
2228
+ return {
2229
+ mount: true,
2230
+ props: {
2231
+ ...defaults,
2232
+ ...value,
2233
+ enabled: value.enabled !== false
2234
+ }
2235
+ };
2236
+ }
2237
+ function resolveContextMenuChannel(value) {
2238
+ if (value === false) {
2239
+ return { mount: false };
2240
+ }
2241
+ if (value === true || value === void 0) {
2242
+ return { mount: true, props: {} };
2243
+ }
2244
+ return { mount: true, props: { ...value } };
2245
+ }
2246
+ var PanoView = (0, import_react11.forwardRef)(
707
2247
  function PanoView2({
708
2248
  children,
709
2249
  initialView,
710
2250
  minFov = 30,
711
2251
  maxFov = 100,
712
2252
  controls = true,
2253
+ contextMenu = true,
713
2254
  onViewChange,
714
2255
  onPanoramaClick,
715
2256
  onPanoramaDoubleClick,
@@ -719,16 +2260,18 @@ var PanoView = (0, import_react4.forwardRef)(
719
2260
  "aria-label": ariaLabel = "Panorama viewer",
720
2261
  ...divProps
721
2262
  }, ref) {
722
- const rootRef = (0, import_react4.useRef)(null);
723
- const controlsRef = (0, import_react4.useRef)(null);
2263
+ const rootRef = (0, import_react11.useRef)(null);
2264
+ const controlsRef = (0, import_react11.useRef)(null);
2265
+ const eventBus = (0, import_react11.useMemo)(() => createPanoEventBus(), []);
724
2266
  const { controls: hotspotAccessibilityControls, registry } = useHotspotAccessibilityLayer();
725
- const fallbackViewRef = (0, import_react4.useRef)(DEFAULT_VIEW);
2267
+ const { api: contextMenuOverlayApi, overlay: contextMenuOverlay } = usePanoContextMenuOverlay();
2268
+ const fallbackViewRef = (0, import_react11.useRef)(DEFAULT_VIEW);
726
2269
  const normalizedMinFov = Math.max(1, Math.min(minFov, maxFov - 1));
727
2270
  const normalizedMaxFov = Math.min(
728
2271
  179,
729
2272
  Math.max(maxFov, normalizedMinFov + 1)
730
2273
  );
731
- const normalizedInitialView = (0, import_react4.useMemo)(
2274
+ const normalizedInitialView = (0, import_react11.useMemo)(
732
2275
  () => ({
733
2276
  yaw: initialView?.yaw ?? DEFAULT_VIEW.yaw,
734
2277
  pitch: clampPanoPitch(initialView?.pitch ?? DEFAULT_VIEW.pitch),
@@ -747,18 +2290,101 @@ var PanoView = (0, import_react4.forwardRef)(
747
2290
  );
748
2291
  fallbackViewRef.current = normalizedInitialView;
749
2292
  const controlsEnabled = controls !== false;
750
- const controlOptions = (0, import_react4.useMemo)(
2293
+ const controlOptions = (0, import_react11.useMemo)(
751
2294
  () => typeof controls === "object" ? controls : {},
752
2295
  [controls]
753
2296
  );
2297
+ const userControlsEnabled = controlsEnabled && controlOptions.enabled !== false;
2298
+ const allModeInvert = controlOptions.invert === true;
2299
+ const topRotateSpeed = controlOptions.rotateSpeed ?? DEFAULT_POINTER_ROTATE_SPEED;
2300
+ const topZoomSpeed = controlOptions.zoomSpeed ?? DEFAULT_MOUSE_ZOOM_SPEED;
2301
+ const mouseChannel = (0, import_react11.useMemo)(
2302
+ () => resolveChannel(controlOptions.mouse, {
2303
+ enabled: true,
2304
+ rotateSpeed: topRotateSpeed,
2305
+ zoomSpeed: topZoomSpeed,
2306
+ wheel: true,
2307
+ invert: allModeInvert,
2308
+ buttons: ["left"]
2309
+ }),
2310
+ [
2311
+ allModeInvert,
2312
+ controlOptions.mouse,
2313
+ topRotateSpeed,
2314
+ topZoomSpeed
2315
+ ]
2316
+ );
2317
+ const touchChannel = (0, import_react11.useMemo)(
2318
+ () => resolveChannel(controlOptions.touch, {
2319
+ enabled: true,
2320
+ rotateSpeed: topRotateSpeed,
2321
+ invert: allModeInvert,
2322
+ pinchZoom: true
2323
+ }),
2324
+ [allModeInvert, controlOptions.touch, topRotateSpeed]
2325
+ );
2326
+ const keyboardChannel = (0, import_react11.useMemo)(
2327
+ () => resolveChannel(controlOptions.keyboard, {
2328
+ enabled: true
2329
+ }),
2330
+ [controlOptions.keyboard]
2331
+ );
2332
+ const toggleFullscreen = (0, import_react11.useCallback)(async () => {
2333
+ if (typeof document === "undefined") {
2334
+ return;
2335
+ }
2336
+ if (document.fullscreenElement) {
2337
+ await document.exitFullscreen?.();
2338
+ return;
2339
+ }
2340
+ await rootRef.current?.requestFullscreen?.();
2341
+ }, []);
2342
+ const [isViewerFullscreen, setIsViewerFullscreen] = (0, import_react11.useState)(false);
2343
+ (0, import_react11.useEffect)(() => {
2344
+ if (typeof document === "undefined") {
2345
+ return;
2346
+ }
2347
+ const syncFullscreen = () => {
2348
+ const fullscreenElement = document.fullscreenElement;
2349
+ const root = rootRef.current;
2350
+ if (!fullscreenElement || !root) {
2351
+ setIsViewerFullscreen(false);
2352
+ return;
2353
+ }
2354
+ setIsViewerFullscreen(
2355
+ fullscreenElement === root || fullscreenElement.contains(root) || root.contains(fullscreenElement)
2356
+ );
2357
+ };
2358
+ syncFullscreen();
2359
+ document.addEventListener("fullscreenchange", syncFullscreen);
2360
+ return () => {
2361
+ document.removeEventListener("fullscreenchange", syncFullscreen);
2362
+ };
2363
+ }, []);
2364
+ const contextMenuActions = (0, import_react11.useMemo)(
2365
+ () => ({
2366
+ reset: () => {
2367
+ controlsRef.current?.reset();
2368
+ },
2369
+ toggleFullscreen: () => {
2370
+ void toggleFullscreen();
2371
+ },
2372
+ isFullscreen: isViewerFullscreen
2373
+ }),
2374
+ [isViewerFullscreen, toggleFullscreen]
2375
+ );
2376
+ const contextMenuChannel = (0, import_react11.useMemo)(
2377
+ () => resolveContextMenuChannel(contextMenu),
2378
+ [contextMenu]
2379
+ );
754
2380
  const legacyAutoRotateOptions = controlOptions;
755
- const [legacyAutoRotate, setLegacyAutoRotate] = (0, import_react4.useState)(
2381
+ const [legacyAutoRotate, setLegacyAutoRotate] = (0, import_react11.useState)(
756
2382
  legacyAutoRotateOptions.autoRotate ?? false
757
2383
  );
758
- (0, import_react4.useEffect)(() => {
2384
+ (0, import_react11.useEffect)(() => {
759
2385
  setLegacyAutoRotate(legacyAutoRotateOptions.autoRotate ?? false);
760
2386
  }, [legacyAutoRotateOptions.autoRotate]);
761
- (0, import_react4.useImperativeHandle)(
2387
+ (0, import_react11.useImperativeHandle)(
762
2388
  ref,
763
2389
  () => ({
764
2390
  getView: () => controlsRef.current?.getView() ?? { ...fallbackViewRef.current },
@@ -774,20 +2400,11 @@ var PanoView = (0, import_react4.forwardRef)(
774
2400
  stopAutoRotate: () => {
775
2401
  setLegacyAutoRotate(false);
776
2402
  },
777
- toggleFullscreen: async () => {
778
- if (typeof document === "undefined") {
779
- return;
780
- }
781
- if (document.fullscreenElement) {
782
- await document.exitFullscreen?.();
783
- return;
784
- }
785
- await rootRef.current?.requestFullscreen?.();
786
- }
2403
+ toggleFullscreen
787
2404
  }),
788
- []
2405
+ [toggleFullscreen]
789
2406
  );
790
- const rootStyle = (0, import_react4.useMemo)(
2407
+ const rootStyle = (0, import_react11.useMemo)(
791
2408
  () => ({
792
2409
  overflow: "hidden",
793
2410
  position: "relative",
@@ -795,7 +2412,7 @@ var PanoView = (0, import_react4.forwardRef)(
795
2412
  }),
796
2413
  [style]
797
2414
  );
798
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2415
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
799
2416
  "div",
800
2417
  {
801
2418
  ...divProps,
@@ -803,8 +2420,8 @@ var PanoView = (0, import_react4.forwardRef)(
803
2420
  "aria-label": ariaLabel,
804
2421
  style: rootStyle,
805
2422
  children: [
806
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
807
- import_fiber3.Canvas,
2423
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2424
+ import_fiber8.Canvas,
808
2425
  {
809
2426
  "aria-label": `${ariaLabel} canvas`,
810
2427
  camera: {
@@ -823,41 +2440,71 @@ var PanoView = (0, import_react4.forwardRef)(
823
2440
  },
824
2441
  style: {
825
2442
  ...DEFAULT_CANVAS_STYLE,
826
- touchAction: controlsEnabled ? "none" : "auto"
2443
+ touchAction: userControlsEnabled ? "none" : "auto"
827
2444
  },
828
- tabIndex: controlsEnabled ? 0 : void 0,
829
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanoramaControlsContext.Provider, { value: controlsRef, children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
831
- PanoramaControls,
832
- {
833
- ref: controlsRef,
834
- enabled: controlsEnabled,
835
- initialView: normalizedInitialView,
836
- maxFov: normalizedMaxFov,
837
- minFov: normalizedMinFov,
838
- onViewChange,
839
- options: controlOptions
840
- }
841
- ),
842
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
843
- AutoRotate,
844
- {
845
- enabled: legacyAutoRotate,
846
- speed: legacyAutoRotateOptions.autoRotateSpeed
847
- }
848
- ),
849
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
850
- PanoramaEventSurface,
851
- {
852
- onClick: onPanoramaClick,
853
- onDoubleClick: onPanoramaDoubleClick,
854
- onPointerMove: onPanoramaPointerMove
855
- }
856
- ),
857
- children
858
- ] }) })
2445
+ tabIndex: userControlsEnabled ? 0 : void 0,
2446
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanoEventBusContext.Provider, { value: eventBus, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanoramaViewContext.Provider, { value: controlsRef, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2447
+ PanoContextMenuOverlayContext.Provider,
2448
+ {
2449
+ value: contextMenuOverlayApi,
2450
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2451
+ PanoContextMenuActionsContext.Provider,
2452
+ {
2453
+ value: contextMenuActions,
2454
+ children: [
2455
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2456
+ PanoramaViewRuntime,
2457
+ {
2458
+ ref: controlsRef,
2459
+ eventBus,
2460
+ initialView: normalizedInitialView,
2461
+ maxFov: normalizedMaxFov,
2462
+ minFov: normalizedMinFov,
2463
+ options: controlOptions
2464
+ }
2465
+ ),
2466
+ (onViewChange || onPanoramaClick || onPanoramaDoubleClick || onPanoramaPointerMove) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2467
+ PanoEvents,
2468
+ {
2469
+ onViewChange,
2470
+ onClick: onPanoramaClick,
2471
+ onDoubleClick: onPanoramaDoubleClick,
2472
+ onPointerMove: onPanoramaPointerMove
2473
+ }
2474
+ ),
2475
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2476
+ AutoRotate,
2477
+ {
2478
+ enabled: legacyAutoRotate,
2479
+ speed: legacyAutoRotateOptions.autoRotateSpeed
2480
+ }
2481
+ ),
2482
+ userControlsEnabled && mouseChannel.mount ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2483
+ MouseControls,
2484
+ {
2485
+ ...mouseChannel.props,
2486
+ fovSpeed: controlOptions.fovSpeed
2487
+ }
2488
+ ) : null,
2489
+ userControlsEnabled && touchChannel.mount ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TouchControls, { ...touchChannel.props }) : null,
2490
+ userControlsEnabled && keyboardChannel.mount ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2491
+ KeyboardControls,
2492
+ {
2493
+ ...keyboardChannel.props,
2494
+ fovSpeed: keyboardChannel.props.fovSpeed ?? controlOptions.fovSpeed
2495
+ }
2496
+ ) : null,
2497
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanoramaEventSurface, {}),
2498
+ contextMenuChannel.mount ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PanoContextMenu, { ...contextMenuChannel.props }) : null,
2499
+ children
2500
+ ]
2501
+ }
2502
+ )
2503
+ }
2504
+ ) }) }) })
859
2505
  }
860
2506
  ),
2507
+ contextMenuOverlay,
861
2508
  hotspotAccessibilityControls
862
2509
  ]
863
2510
  }
@@ -866,10 +2513,10 @@ var PanoView = (0, import_react4.forwardRef)(
866
2513
  );
867
2514
 
868
2515
  // src/sphere.tsx
869
- var import_fiber4 = require("@react-three/fiber");
870
- var import_react5 = require("react");
2516
+ var import_fiber9 = require("@react-three/fiber");
2517
+ var import_react12 = require("react");
871
2518
  var import_three4 = require("three");
872
- var import_jsx_runtime4 = require("react/jsx-runtime");
2519
+ var import_jsx_runtime5 = require("react/jsx-runtime");
873
2520
  function Sphere({
874
2521
  src,
875
2522
  yawOffset = 0,
@@ -877,13 +2524,13 @@ function Sphere({
877
2524
  onError,
878
2525
  visible = true
879
2526
  }) {
880
- const gl = (0, import_fiber4.useThree)((state) => state.gl);
881
- const [texture, setTexture] = (0, import_react5.useState)(null);
882
- const onLoadRef = (0, import_react5.useRef)(onLoad);
883
- const onErrorRef = (0, import_react5.useRef)(onError);
2527
+ const gl = (0, import_fiber9.useThree)((state) => state.gl);
2528
+ const [texture, setTexture] = (0, import_react12.useState)(null);
2529
+ const onLoadRef = (0, import_react12.useRef)(onLoad);
2530
+ const onErrorRef = (0, import_react12.useRef)(onError);
884
2531
  onLoadRef.current = onLoad;
885
2532
  onErrorRef.current = onError;
886
- (0, import_react5.useEffect)(() => {
2533
+ (0, import_react12.useEffect)(() => {
887
2534
  let active = true;
888
2535
  const loader = new import_three4.TextureLoader();
889
2536
  setTexture(null);
@@ -917,7 +2564,7 @@ function Sphere({
917
2564
  active = false;
918
2565
  };
919
2566
  }, [gl, src]);
920
- (0, import_react5.useEffect)(
2567
+ (0, import_react12.useEffect)(
921
2568
  () => () => {
922
2569
  texture?.dispose();
923
2570
  },
@@ -926,14 +2573,14 @@ function Sphere({
926
2573
  if (!texture) {
927
2574
  return null;
928
2575
  }
929
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2576
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
930
2577
  "mesh",
931
2578
  {
932
2579
  rotation: [0, import_three4.MathUtils.degToRad(90 + yawOffset), 0],
933
2580
  visible,
934
2581
  children: [
935
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
936
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2582
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
2583
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
937
2584
  "meshBasicMaterial",
938
2585
  {
939
2586
  map: texture,
@@ -947,13 +2594,13 @@ function Sphere({
947
2594
  }
948
2595
 
949
2596
  // src/panorama-scenes.tsx
950
- var import_fiber7 = require("@react-three/fiber");
951
- var import_react8 = require("react");
2597
+ var import_fiber12 = require("@react-three/fiber");
2598
+ var import_react15 = require("react");
952
2599
  var import_three8 = require("three");
953
2600
 
954
2601
  // src/tile/texture-manager-context.tsx
955
- var import_fiber5 = require("@react-three/fiber");
956
- var import_react6 = require("react");
2602
+ var import_fiber10 = require("@react-three/fiber");
2603
+ var import_react13 = require("react");
957
2604
 
958
2605
  // src/tile/texture-manager.ts
959
2606
  var import_three5 = require("three");
@@ -1177,8 +2824,8 @@ var TileTextureManager = class {
1177
2824
  };
1178
2825
 
1179
2826
  // src/tile/texture-manager-context.tsx
1180
- var import_jsx_runtime5 = require("react/jsx-runtime");
1181
- var TileTextureManagerContext = (0, import_react6.createContext)(
2827
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2828
+ var TileTextureManagerContext = (0, import_react13.createContext)(
1182
2829
  null
1183
2830
  );
1184
2831
  function TileTextureManagerProvider({
@@ -1186,9 +2833,9 @@ function TileTextureManagerProvider({
1186
2833
  maxTextureMemoryMb = 128,
1187
2834
  maxConcurrentLoads = 4
1188
2835
  }) {
1189
- const gl = (0, import_fiber5.useThree)((state) => state.gl);
2836
+ const gl = (0, import_fiber10.useThree)((state) => state.gl);
1190
2837
  const anisotropy = gl.capabilities.getMaxAnisotropy();
1191
- const manager = (0, import_react6.useMemo)(
2838
+ const manager = (0, import_react13.useMemo)(
1192
2839
  () => new TileTextureManager({
1193
2840
  anisotropy,
1194
2841
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1197,19 +2844,19 @@ function TileTextureManagerProvider({
1197
2844
  }),
1198
2845
  [anisotropy, maxConcurrentLoads, maxTextureMemoryMb]
1199
2846
  );
1200
- (0, import_react6.useEffect)(() => {
2847
+ (0, import_react13.useEffect)(() => {
1201
2848
  manager.resume();
1202
2849
  return () => manager.dispose();
1203
2850
  }, [manager]);
1204
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TileTextureManagerContext.Provider, { value: manager, children });
2851
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TileTextureManagerContext.Provider, { value: manager, children });
1205
2852
  }
1206
2853
  function useSharedTileTextureManager() {
1207
- return (0, import_react6.useContext)(TileTextureManagerContext);
2854
+ return (0, import_react13.useContext)(TileTextureManagerContext);
1208
2855
  }
1209
2856
 
1210
2857
  // src/tile/tile.tsx
1211
- var import_fiber6 = require("@react-three/fiber");
1212
- var import_react7 = require("react");
2858
+ var import_fiber11 = require("@react-three/fiber");
2859
+ var import_react14 = require("react");
1213
2860
  var import_three7 = require("three");
1214
2861
 
1215
2862
  // src/tile/face-layout.ts
@@ -1390,7 +3037,7 @@ function resolveTemplateUrl(template, address) {
1390
3037
  }
1391
3038
 
1392
3039
  // src/tile/tile.tsx
1393
- var import_jsx_runtime6 = require("react/jsx-runtime");
3040
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1394
3041
  var MAX_LOD_UPDATE_INTERVAL_MS = 100;
1395
3042
  var FAST_ROTATION_THRESHOLD_RADIANS = Math.PI / 180;
1396
3043
  var FAST_FOV_THRESHOLD_DEGREES = 0.5;
@@ -1399,10 +3046,10 @@ var VISIBILITY_FOV_PADDING_DEGREES = 8;
1399
3046
  var PREVIEW_RADIUS = CUBE_RADIUS + 0.05;
1400
3047
  var SHARED_TILE_GEOMETRY = new import_three7.PlaneGeometry(1, 1);
1401
3048
  function usePreviewTexture(url, anisotropy, onError) {
1402
- const [texture, setTexture] = (0, import_react7.useState)(null);
1403
- const onErrorRef = (0, import_react7.useRef)(onError);
3049
+ const [texture, setTexture] = (0, import_react14.useState)(null);
3050
+ const onErrorRef = (0, import_react14.useRef)(onError);
1404
3051
  onErrorRef.current = onError;
1405
- (0, import_react7.useEffect)(() => {
3052
+ (0, import_react14.useEffect)(() => {
1406
3053
  if (!url) {
1407
3054
  setTexture(null);
1408
3055
  return;
@@ -1439,7 +3086,7 @@ function usePreviewTexture(url, anisotropy, onError) {
1439
3086
  active = false;
1440
3087
  };
1441
3088
  }, [anisotropy, url]);
1442
- (0, import_react7.useEffect)(
3089
+ (0, import_react14.useEffect)(
1443
3090
  () => () => {
1444
3091
  texture?.dispose();
1445
3092
  },
@@ -1452,7 +3099,7 @@ function PreviewFace({
1452
3099
  texture,
1453
3100
  faceOrder
1454
3101
  }) {
1455
- const geometry = (0, import_react7.useMemo)(() => {
3102
+ const geometry = (0, import_react14.useMemo)(() => {
1456
3103
  const nextGeometry = new import_three7.PlaneGeometry(
1457
3104
  PREVIEW_RADIUS * 2,
1458
3105
  PREVIEW_RADIUS * 2
@@ -1470,21 +3117,21 @@ function PreviewFace({
1470
3117
  uv.needsUpdate = true;
1471
3118
  return nextGeometry;
1472
3119
  }, [face, faceOrder]);
1473
- (0, import_react7.useEffect)(
3120
+ (0, import_react14.useEffect)(
1474
3121
  () => () => {
1475
3122
  geometry.dispose();
1476
3123
  },
1477
3124
  [geometry]
1478
3125
  );
1479
3126
  const transform = getFaceTransform(face, PREVIEW_RADIUS);
1480
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3127
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1481
3128
  "mesh",
1482
3129
  {
1483
3130
  geometry,
1484
3131
  position: transform.position,
1485
3132
  renderOrder: -100,
1486
3133
  rotation: transform.rotation,
1487
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3134
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1488
3135
  "meshBasicMaterial",
1489
3136
  {
1490
3137
  depthTest: false,
@@ -1500,7 +3147,7 @@ function PreviewCube({
1500
3147
  texture,
1501
3148
  faceOrder
1502
3149
  }) {
1503
- return CUBE_FACES.map((face) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3150
+ return CUBE_FACES.map((face) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1504
3151
  PreviewFace,
1505
3152
  {
1506
3153
  face,
@@ -1511,14 +3158,14 @@ function PreviewCube({
1511
3158
  ));
1512
3159
  }
1513
3160
  function useManagedTexture(manager, key, url, address) {
1514
- const [managedState, setManagedState] = (0, import_react7.useState)(() => ({
3161
+ const [managedState, setManagedState] = (0, import_react14.useState)(() => ({
1515
3162
  manager,
1516
3163
  key,
1517
3164
  url,
1518
3165
  snapshot: manager.getSnapshot(key)
1519
3166
  }));
1520
3167
  const snapshot = managedState.manager === manager && managedState.key === key && managedState.url === url ? managedState.snapshot : manager.getSnapshot(key);
1521
- (0, import_react7.useEffect)(
3168
+ (0, import_react14.useEffect)(
1522
3169
  () => manager.acquire(key, url, address, (nextSnapshot) => {
1523
3170
  setManagedState({
1524
3171
  manager,
@@ -1531,7 +3178,7 @@ function useManagedTexture(manager, key, url, address) {
1531
3178
  );
1532
3179
  return snapshot;
1533
3180
  }
1534
- var TileMesh = (0, import_react7.memo)(function TileMesh2({
3181
+ var TileMesh = (0, import_react14.memo)(function TileMesh2({
1535
3182
  address,
1536
3183
  manager,
1537
3184
  renderOrder,
@@ -1545,14 +3192,14 @@ var TileMesh = (0, import_react7.memo)(function TileMesh2({
1545
3192
  if (!snapshot.texture) {
1546
3193
  return null;
1547
3194
  }
1548
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("group", { position: transform.position, rotation: transform.rotation, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3195
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("group", { position: transform.position, rotation: transform.rotation, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1549
3196
  "mesh",
1550
3197
  {
1551
3198
  geometry: SHARED_TILE_GEOMETRY,
1552
3199
  position: [layout.x, layout.y, 0],
1553
3200
  renderOrder,
1554
3201
  scale: [layout.width, layout.height, 1],
1555
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3202
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1556
3203
  "meshBasicMaterial",
1557
3204
  {
1558
3205
  depthTest: false,
@@ -1564,12 +3211,12 @@ var TileMesh = (0, import_react7.memo)(function TileMesh2({
1564
3211
  }
1565
3212
  ) });
1566
3213
  });
1567
- var TileLayerMeshes = (0, import_react7.memo)(function TileLayerMeshes2({
3214
+ var TileLayerMeshes = (0, import_react14.memo)(function TileLayerMeshes2({
1568
3215
  layer,
1569
3216
  manager,
1570
3217
  resolveUrl
1571
3218
  }) {
1572
- return layer.addresses.map((address) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3219
+ return layer.addresses.map((address) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1573
3220
  TileMesh,
1574
3221
  {
1575
3222
  address,
@@ -1737,11 +3384,11 @@ function Tile({
1737
3384
  onTileError,
1738
3385
  onLevelChange
1739
3386
  }) {
1740
- const { camera, gl, size } = (0, import_fiber6.useThree)();
1741
- const multiresConfig = (0, import_react7.useMemo)(() => parseMultires(multires), [multires]);
3387
+ const { camera, gl, size } = (0, import_fiber11.useThree)();
3388
+ const multiresConfig = (0, import_react14.useMemo)(() => parseMultires(multires), [multires]);
1742
3389
  const { levels, tileSize } = multiresConfig;
1743
3390
  const multiresToken = `${tileSize}:${levels.join(",")}`;
1744
- const candidatesByLevel = (0, import_react7.useMemo)(
3391
+ const candidatesByLevel = (0, import_react14.useMemo)(
1745
3392
  () => buildTileCandidates(levels, tileSize),
1746
3393
  [levels, tileSize]
1747
3394
  );
@@ -1753,13 +3400,13 @@ function Tile({
1753
3400
  onPreviewError
1754
3401
  );
1755
3402
  const sharedManager = useSharedTileTextureManager();
1756
- const progressRef = (0, import_react7.useRef)(onLoadProgress);
1757
- const errorRef = (0, import_react7.useRef)(onTileError);
1758
- const levelChangeRef = (0, import_react7.useRef)(onLevelChange);
3403
+ const progressRef = (0, import_react14.useRef)(onLoadProgress);
3404
+ const errorRef = (0, import_react14.useRef)(onTileError);
3405
+ const levelChangeRef = (0, import_react14.useRef)(onLevelChange);
1759
3406
  progressRef.current = onLoadProgress;
1760
3407
  errorRef.current = onTileError;
1761
3408
  levelChangeRef.current = onLevelChange;
1762
- const ownedManager = (0, import_react7.useMemo)(
3409
+ const ownedManager = (0, import_react14.useMemo)(
1763
3410
  () => new TileTextureManager({
1764
3411
  anisotropy,
1765
3412
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1780,7 +3427,7 @@ function Tile({
1780
3427
  ]
1781
3428
  );
1782
3429
  const manager = sharedManager ?? ownedManager;
1783
- (0, import_react7.useEffect)(() => {
3430
+ (0, import_react14.useEffect)(() => {
1784
3431
  manager.resume();
1785
3432
  return () => {
1786
3433
  if (manager === ownedManager) {
@@ -1790,11 +3437,11 @@ function Tile({
1790
3437
  queueMicrotask(() => manager.releaseUnused());
1791
3438
  };
1792
3439
  }, [manager, ownedManager]);
1793
- const readyRef = (0, import_react7.useRef)(false);
1794
- (0, import_react7.useEffect)(() => {
3440
+ const readyRef = (0, import_react14.useRef)(false);
3441
+ (0, import_react14.useEffect)(() => {
1795
3442
  readyRef.current = false;
1796
3443
  }, [effectivePreviewUrl]);
1797
- (0, import_react7.useEffect)(() => {
3444
+ (0, import_react14.useEffect)(() => {
1798
3445
  if (!previewTexture || readyRef.current) {
1799
3446
  return;
1800
3447
  }
@@ -1802,31 +3449,31 @@ function Tile({
1802
3449
  onReady?.();
1803
3450
  }, [onReady, previewTexture]);
1804
3451
  const template = urlTemplate ?? buildDefaultTileUrlTemplate();
1805
- const resolveUrl = (0, import_react7.useMemo)(
3452
+ const resolveUrl = (0, import_react14.useMemo)(
1806
3453
  () => (address) => {
1807
3454
  const relativePath = resolveTileUrl ? resolveTileUrl(address) : resolveTemplateUrl(template, address);
1808
3455
  return resolveRelativeTileUrl(baseUrl, relativePath);
1809
3456
  },
1810
3457
  [baseUrl, resolveTileUrl, template]
1811
3458
  );
1812
- const [layers, setLayers] = (0, import_react7.useState)([]);
1813
- const layerTokenRef = (0, import_react7.useRef)("");
1814
- const currentLevelRef = (0, import_react7.useRef)(1);
1815
- const lastUpdateAtRef = (0, import_react7.useRef)(-Infinity);
1816
- const visibilityDirtyRef = (0, import_react7.useRef)(true);
1817
- const lastVisibilityQuaternionRef = (0, import_react7.useRef)(camera.quaternion.clone());
1818
- const lastVisibilityFovRef = (0, import_react7.useRef)(Number.NaN);
1819
- const lastViewportRef = (0, import_react7.useRef)({ width: 0, height: 0, dpr: 0 });
1820
- const frustumRef = (0, import_react7.useRef)(new import_three7.Frustum());
1821
- const projectionMatrixRef = (0, import_react7.useRef)(new import_three7.Matrix4());
1822
- const visibilityCameraRef = (0, import_react7.useRef)(new import_three7.PerspectiveCamera());
1823
- (0, import_react7.useEffect)(() => {
3459
+ const [layers, setLayers] = (0, import_react14.useState)([]);
3460
+ const layerTokenRef = (0, import_react14.useRef)("");
3461
+ const currentLevelRef = (0, import_react14.useRef)(1);
3462
+ const lastUpdateAtRef = (0, import_react14.useRef)(-Infinity);
3463
+ const visibilityDirtyRef = (0, import_react14.useRef)(true);
3464
+ const lastVisibilityQuaternionRef = (0, import_react14.useRef)(camera.quaternion.clone());
3465
+ const lastVisibilityFovRef = (0, import_react14.useRef)(Number.NaN);
3466
+ const lastViewportRef = (0, import_react14.useRef)({ width: 0, height: 0, dpr: 0 });
3467
+ const frustumRef = (0, import_react14.useRef)(new import_three7.Frustum());
3468
+ const projectionMatrixRef = (0, import_react14.useRef)(new import_three7.Matrix4());
3469
+ const visibilityCameraRef = (0, import_react14.useRef)(new import_three7.PerspectiveCamera());
3470
+ (0, import_react14.useEffect)(() => {
1824
3471
  currentLevelRef.current = 1;
1825
3472
  layerTokenRef.current = "";
1826
3473
  visibilityDirtyRef.current = true;
1827
3474
  setLayers([]);
1828
3475
  }, [manager, multiresToken]);
1829
- (0, import_react7.useEffect)(() => {
3476
+ (0, import_react14.useEffect)(() => {
1830
3477
  if (loadMode !== "preview") {
1831
3478
  return;
1832
3479
  }
@@ -1840,7 +3487,7 @@ function Tile({
1840
3487
  setLayers(nextLayers);
1841
3488
  }
1842
3489
  };
1843
- (0, import_fiber6.useFrame)(() => {
3490
+ (0, import_fiber11.useFrame)(() => {
1844
3491
  if (loadMode === "preview") {
1845
3492
  return;
1846
3493
  }
@@ -1906,15 +3553,15 @@ function Tile({
1906
3553
  };
1907
3554
  commitLayers(nextLayers);
1908
3555
  });
1909
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("group", { visible, children: [
1910
- previewTexture ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3556
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("group", { visible, children: [
3557
+ previewTexture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1911
3558
  PreviewCube,
1912
3559
  {
1913
3560
  texture: previewTexture,
1914
3561
  faceOrder: previewFaceOrder
1915
3562
  }
1916
3563
  ) : null,
1917
- layers.map((layer) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3564
+ layers.map((layer) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1918
3565
  TileLayerMeshes,
1919
3566
  {
1920
3567
  layer,
@@ -1927,7 +3574,7 @@ function Tile({
1927
3574
  }
1928
3575
 
1929
3576
  // src/panorama-scenes.tsx
1930
- var import_jsx_runtime7 = require("react/jsx-runtime");
3577
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1931
3578
  var TRANSITION_DEFAULTS = {
1932
3579
  none: { duration: 0, krpanoBlend: "NOBLEND" },
1933
3580
  crossfade: { duration: 1, krpanoBlend: "BLEND(1.0, easeInCubic)" },
@@ -1977,7 +3624,7 @@ function SceneSource({
1977
3624
  return null;
1978
3625
  }
1979
3626
  if (scene.type === "sphere") {
1980
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3627
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1981
3628
  Sphere,
1982
3629
  {
1983
3630
  src: scene.src,
@@ -1989,7 +3636,7 @@ function SceneSource({
1989
3636
  );
1990
3637
  }
1991
3638
  const { id: _id, type: _type, ...tileProps } = scene;
1992
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3639
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1993
3640
  Tile,
1994
3641
  {
1995
3642
  ...tileProps,
@@ -2005,17 +3652,17 @@ function SnapshotCapture({
2005
3652
  maxPixels,
2006
3653
  onCaptured
2007
3654
  }) {
2008
- const { camera, gl, scene, size } = (0, import_fiber7.useThree)();
2009
- const capturedRequestRef = (0, import_react8.useRef)(0);
2010
- const renderTargetRef = (0, import_react8.useRef)(null);
2011
- (0, import_react8.useEffect)(
3655
+ const { camera, gl, scene, size } = (0, import_fiber12.useThree)();
3656
+ const capturedRequestRef = (0, import_react15.useRef)(0);
3657
+ const renderTargetRef = (0, import_react15.useRef)(null);
3658
+ (0, import_react15.useEffect)(
2012
3659
  () => () => {
2013
3660
  renderTargetRef.current?.dispose();
2014
3661
  renderTargetRef.current = null;
2015
3662
  },
2016
3663
  []
2017
3664
  );
2018
- (0, import_fiber7.useFrame)(() => {
3665
+ (0, import_fiber12.useFrame)(() => {
2019
3666
  if (requestId === 0 || requestId === capturedRequestRef.current) {
2020
3667
  return;
2021
3668
  }
@@ -2061,12 +3708,12 @@ function SnapshotOverlay({
2061
3708
  runId,
2062
3709
  onFinish
2063
3710
  }) {
2064
- const { camera, size } = (0, import_fiber7.useThree)();
2065
- const meshRef = (0, import_react8.useRef)(null);
2066
- const startRef = (0, import_react8.useRef)(null);
2067
- const finishedRef = (0, import_react8.useRef)(false);
2068
- const directionRef = (0, import_react8.useRef)(new import_three8.Vector3());
2069
- const material = (0, import_react8.useMemo)(
3711
+ const { camera, size } = (0, import_fiber12.useThree)();
3712
+ const meshRef = (0, import_react15.useRef)(null);
3713
+ const startRef = (0, import_react15.useRef)(null);
3714
+ const finishedRef = (0, import_react15.useRef)(false);
3715
+ const directionRef = (0, import_react15.useRef)(new import_three8.Vector3());
3716
+ const material = (0, import_react15.useMemo)(
2070
3717
  () => new import_three8.ShaderMaterial({
2071
3718
  transparent: true,
2072
3719
  depthTest: false,
@@ -2145,13 +3792,13 @@ function SnapshotOverlay({
2145
3792
  }),
2146
3793
  [snapshot.texture, transition.preset]
2147
3794
  );
2148
- (0, import_react8.useEffect)(() => () => material.dispose(), [material]);
2149
- (0, import_react8.useEffect)(() => {
3795
+ (0, import_react15.useEffect)(() => () => material.dispose(), [material]);
3796
+ (0, import_react15.useEffect)(() => {
2150
3797
  startRef.current = null;
2151
3798
  finishedRef.current = false;
2152
3799
  material.uniforms.progress.value = 0;
2153
3800
  }, [material, runId]);
2154
- (0, import_fiber7.useFrame)((state) => {
3801
+ (0, import_fiber12.useFrame)((state) => {
2155
3802
  const mesh = meshRef.current;
2156
3803
  if (!mesh || !camera.isPerspectiveCamera) {
2157
3804
  return;
@@ -2176,9 +3823,9 @@ function SnapshotOverlay({
2176
3823
  onFinish();
2177
3824
  }
2178
3825
  });
2179
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
2180
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
2181
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("primitive", { attach: "material", object: material })
3826
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
3827
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
3828
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("primitive", { attach: "material", object: material })
2182
3829
  ] });
2183
3830
  }
2184
3831
  var FULLSCREEN_PLANE = new import_three8.PlaneGeometry(1, 1);
@@ -2191,47 +3838,47 @@ function PanoramaScenesController({
2191
3838
  onTransitionError,
2192
3839
  snapshotMaxPixels = 3686400
2193
3840
  }) {
2194
- const controlsRef = (0, import_react8.useContext)(PanoramaControlsContext);
3841
+ const controlsRef = (0, import_react15.useContext)(PanoramaViewContext);
2195
3842
  const manager = useSharedTileTextureManager();
2196
- const transition = (0, import_react8.useMemo)(
3843
+ const transition = (0, import_react15.useMemo)(
2197
3844
  () => resolveTransition(transitionInput),
2198
3845
  [transitionInput]
2199
3846
  );
2200
- const sceneMap = (0, import_react8.useMemo)(
3847
+ const sceneMap = (0, import_react15.useMemo)(
2201
3848
  () => new Map(scenes.map((scene) => [scene.id, scene])),
2202
3849
  [scenes]
2203
3850
  );
2204
3851
  const initialScene = sceneMap.get(activeSceneId) ?? null;
2205
- const [slots, setSlots] = (0, import_react8.useState)([
3852
+ const [slots, setSlots] = (0, import_react15.useState)([
2206
3853
  initialScene,
2207
3854
  null
2208
3855
  ]);
2209
- const [activeSlot, setActiveSlot] = (0, import_react8.useState)(0);
2210
- const [liveSlot, setLiveSlot] = (0, import_react8.useState)(0);
2211
- const [pendingSlot, setPendingSlot] = (0, import_react8.useState)(null);
2212
- const [phase, setPhase] = (0, import_react8.useState)(initialScene ? "idle" : "preloading");
2213
- const [captureRequestId, setCaptureRequestId] = (0, import_react8.useState)(0);
2214
- const [transitionRunId, setTransitionRunId] = (0, import_react8.useState)(0);
2215
- const [snapshot, setSnapshot] = (0, import_react8.useState)(null);
2216
- const [recoveryKey, setRecoveryKey] = (0, import_react8.useState)(0);
2217
- const requestedSceneIdRef = (0, import_react8.useRef)(activeSceneId);
2218
- const captureReasonRef = (0, import_react8.useRef)("start");
2219
- const interactionReleaseRef = (0, import_react8.useRef)(null);
2220
- const slotsRef = (0, import_react8.useRef)(slots);
2221
- const phaseRef = (0, import_react8.useRef)(phase);
2222
- const liveSlotRef = (0, import_react8.useRef)(liveSlot);
2223
- const transitionFromRef = (0, import_react8.useRef)(initialScene);
3856
+ const [activeSlot, setActiveSlot] = (0, import_react15.useState)(0);
3857
+ const [liveSlot, setLiveSlot] = (0, import_react15.useState)(0);
3858
+ const [pendingSlot, setPendingSlot] = (0, import_react15.useState)(null);
3859
+ const [phase, setPhase] = (0, import_react15.useState)(initialScene ? "idle" : "preloading");
3860
+ const [captureRequestId, setCaptureRequestId] = (0, import_react15.useState)(0);
3861
+ const [transitionRunId, setTransitionRunId] = (0, import_react15.useState)(0);
3862
+ const [snapshot, setSnapshot] = (0, import_react15.useState)(null);
3863
+ const [recoveryKey, setRecoveryKey] = (0, import_react15.useState)(0);
3864
+ const requestedSceneIdRef = (0, import_react15.useRef)(activeSceneId);
3865
+ const captureReasonRef = (0, import_react15.useRef)("start");
3866
+ const interactionReleaseRef = (0, import_react15.useRef)(null);
3867
+ const slotsRef = (0, import_react15.useRef)(slots);
3868
+ const phaseRef = (0, import_react15.useRef)(phase);
3869
+ const liveSlotRef = (0, import_react15.useRef)(liveSlot);
3870
+ const transitionFromRef = (0, import_react15.useRef)(initialScene);
2224
3871
  slotsRef.current = slots;
2225
3872
  phaseRef.current = phase;
2226
3873
  liveSlotRef.current = liveSlot;
2227
- const releaseInteraction = (0, import_react8.useCallback)(() => {
3874
+ const releaseInteraction = (0, import_react15.useCallback)(() => {
2228
3875
  interactionReleaseRef.current?.();
2229
3876
  interactionReleaseRef.current = null;
2230
3877
  }, []);
2231
- const reportError = (0, import_react8.useCallback)((sceneId, error) => {
3878
+ const reportError = (0, import_react15.useCallback)((sceneId, error) => {
2232
3879
  onTransitionError?.({ sceneId, error });
2233
3880
  }, [onTransitionError]);
2234
- const startPreload = (0, import_react8.useCallback)((scene) => {
3881
+ const startPreload = (0, import_react15.useCallback)((scene) => {
2235
3882
  const nextSlot = 1 - liveSlotRef.current;
2236
3883
  requestedSceneIdRef.current = scene.id;
2237
3884
  setSlots((current) => {
@@ -2242,7 +3889,7 @@ function PanoramaScenesController({
2242
3889
  setPendingSlot(nextSlot);
2243
3890
  setPhase("preloading");
2244
3891
  }, []);
2245
- (0, import_react8.useEffect)(() => {
3892
+ (0, import_react15.useEffect)(() => {
2246
3893
  const requested = sceneMap.get(activeSceneId);
2247
3894
  if (!requested) {
2248
3895
  reportError(activeSceneId, new Error(`Unknown panorama scene: ${activeSceneId}`));
@@ -2266,7 +3913,7 @@ function PanoramaScenesController({
2266
3913
  }
2267
3914
  startPreload(requested);
2268
3915
  }, [activeSceneId, pendingSlot, reportError, sceneMap, startPreload]);
2269
- const handleReady = (0, import_react8.useCallback)((slot) => {
3916
+ const handleReady = (0, import_react15.useCallback)((slot) => {
2270
3917
  if (slot !== pendingSlot || phaseRef.current !== "preloading") {
2271
3918
  return;
2272
3919
  }
@@ -2297,7 +3944,7 @@ function PanoramaScenesController({
2297
3944
  setPhase("capturing");
2298
3945
  setCaptureRequestId((current) => current + 1);
2299
3946
  }, [controlsRef, onTransitionEnd, pendingSlot, transition.preset]);
2300
- const handleCaptured = (0, import_react8.useCallback)((nextSnapshot) => {
3947
+ const handleCaptured = (0, import_react15.useCallback)((nextSnapshot) => {
2301
3948
  setSnapshot((current) => {
2302
3949
  current?.dispose();
2303
3950
  return nextSnapshot;
@@ -2319,7 +3966,7 @@ function PanoramaScenesController({
2319
3966
  setPhase("transitioning");
2320
3967
  setTransitionRunId((current) => current + 1);
2321
3968
  }, [manager, pendingSlot, sceneMap, startPreload]);
2322
- const finishTransition = (0, import_react8.useCallback)(() => {
3969
+ const finishTransition = (0, import_react15.useCallback)(() => {
2323
3970
  const nextSlot = pendingSlot;
2324
3971
  const previousScene = transitionFromRef.current;
2325
3972
  const nextScene = nextSlot === null ? null : slotsRef.current[nextSlot];
@@ -2344,12 +3991,12 @@ function PanoramaScenesController({
2344
3991
  });
2345
3992
  }
2346
3993
  }, [onTransitionEnd, pendingSlot, releaseInteraction, snapshot, transition.preset]);
2347
- (0, import_react8.useEffect)(() => () => {
3994
+ (0, import_react15.useEffect)(() => () => {
2348
3995
  releaseInteraction();
2349
3996
  snapshot?.dispose();
2350
3997
  }, [releaseInteraction, snapshot]);
2351
- const { gl } = (0, import_fiber7.useThree)();
2352
- (0, import_react8.useEffect)(() => {
3998
+ const { gl } = (0, import_fiber12.useThree)();
3999
+ (0, import_react15.useEffect)(() => {
2353
4000
  const canvas = gl.domElement;
2354
4001
  const onContextLost = (event) => {
2355
4002
  event.preventDefault();
@@ -2373,8 +4020,8 @@ function PanoramaScenesController({
2373
4020
  }, [gl, manager, releaseInteraction, snapshot]);
2374
4021
  const renderedScene = slots[liveSlot];
2375
4022
  const snapshotVisible = snapshot !== null && (phase === "transitioning" || phase === "recapturing" || phase === "preloading");
2376
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react8.Fragment, { children: [
2377
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4023
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react15.Fragment, { children: [
4024
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2378
4025
  SnapshotCapture,
2379
4026
  {
2380
4027
  requestId: captureRequestId,
@@ -2386,7 +4033,7 @@ function PanoramaScenesController({
2386
4033
  const source = slots[slot];
2387
4034
  const isLive = slot === liveSlot;
2388
4035
  const isPending = slot === pendingSlot;
2389
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4036
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2390
4037
  SceneSource,
2391
4038
  {
2392
4039
  scene: source,
@@ -2402,7 +4049,7 @@ function PanoramaScenesController({
2402
4049
  `${slot}:${source?.id ?? "empty"}:${recoveryKey}`
2403
4050
  );
2404
4051
  }),
2405
- snapshotVisible ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4052
+ snapshotVisible ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2406
4053
  SnapshotOverlay,
2407
4054
  {
2408
4055
  snapshot,
@@ -2420,26 +4067,26 @@ function PanoramaScenes({
2420
4067
  maxConcurrentTileLoads,
2421
4068
  ...props
2422
4069
  }) {
2423
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2424
4071
  TileTextureManagerProvider,
2425
4072
  {
2426
4073
  maxTextureMemoryMb,
2427
4074
  maxConcurrentLoads: maxConcurrentTileLoads,
2428
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PanoramaScenesController, { ...props })
4075
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PanoramaScenesController, { ...props })
2429
4076
  }
2430
4077
  );
2431
4078
  }
2432
4079
 
2433
4080
  // src/hotspot/image-hotspot.tsx
2434
- var import_fiber9 = require("@react-three/fiber");
2435
- var import_react10 = require("react");
4081
+ var import_fiber14 = require("@react-three/fiber");
4082
+ var import_react17 = require("react");
2436
4083
  var import_three10 = require("three");
2437
4084
 
2438
4085
  // src/hotspot/hotspot-anchor.tsx
2439
- var import_fiber8 = require("@react-three/fiber");
2440
- var import_react9 = require("react");
4086
+ var import_fiber13 = require("@react-three/fiber");
4087
+ var import_react16 = require("react");
2441
4088
  var import_three9 = require("three");
2442
- var import_jsx_runtime8 = require("react/jsx-runtime");
4089
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2443
4090
  var DEFAULT_FLOATING_DISTANCE = 10;
2444
4091
  var PANORAMA_RADIUS = DEFAULT_PANORAMA_RADIUS;
2445
4092
  var PANORAMA_SURFACE_INSET = 4;
@@ -2545,10 +4192,10 @@ function HotspotAnchor({
2545
4192
  onPositionChange,
2546
4193
  onDragEnd
2547
4194
  }) {
2548
- const controlsRef = (0, import_react9.useContext)(PanoramaControlsContext);
2549
- const groupRef = (0, import_react9.useRef)(null);
2550
- const dragStateRef = (0, import_react9.useRef)(null);
2551
- const suppressNextClickRef = (0, import_react9.useRef)(false);
4195
+ const controlsRef = (0, import_react16.useContext)(PanoramaViewContext);
4196
+ const groupRef = (0, import_react16.useRef)(null);
4197
+ const dragStateRef = (0, import_react16.useRef)(null);
4198
+ const suppressNextClickRef = (0, import_react16.useRef)(false);
2552
4199
  const normalizedPosition = normalizePanoPosition(position);
2553
4200
  const rendering = internalOrientation && internalPlacement ? { orientation: internalOrientation, placement: internalPlacement } : HOTSPOT_MODE_RENDERING[mode];
2554
4201
  const { orientation: orientation2, placement } = rendering;
@@ -2561,17 +4208,17 @@ function HotspotAnchor({
2561
4208
  scaledWidth,
2562
4209
  scaledHeight
2563
4210
  );
2564
- const worldPosition = (0, import_react9.useMemo)(
4211
+ const worldPosition = (0, import_react16.useMemo)(
2565
4212
  () => panoPositionToVector3(normalizedPosition, resolvedDistance),
2566
4213
  [normalizedPosition.pitch, normalizedPosition.yaw, resolvedDistance]
2567
4214
  );
2568
- const surfaceQuaternion3 = (0, import_react9.useMemo)(
4215
+ const surfaceQuaternion3 = (0, import_react16.useMemo)(
2569
4216
  () => createSurfaceQuaternion(normalizedPosition),
2570
4217
  [normalizedPosition.pitch, normalizedPosition.yaw]
2571
4218
  );
2572
4219
  const worldWidth = angularSizeToWorldSize(scaledWidth, resolvedDistance);
2573
4220
  const worldHeight = angularSizeToWorldSize(scaledHeight, resolvedDistance);
2574
- const focusGeometry = (0, import_react9.useMemo)(() => {
4221
+ const focusGeometry = (0, import_react16.useMemo)(() => {
2575
4222
  const planeGeometry = new import_three9.PlaneGeometry(1, 1);
2576
4223
  const geometry = new import_three9.EdgesGeometry(planeGeometry);
2577
4224
  planeGeometry.dispose();
@@ -2589,15 +4236,15 @@ function HotspotAnchor({
2589
4236
  });
2590
4237
  } : void 0
2591
4238
  });
2592
- (0, import_react9.useEffect)(
4239
+ (0, import_react16.useEffect)(
2593
4240
  () => () => {
2594
4241
  dragStateRef.current?.releaseInteractionLock();
2595
4242
  dragStateRef.current = null;
2596
4243
  },
2597
4244
  []
2598
4245
  );
2599
- (0, import_react9.useEffect)(() => () => focusGeometry.dispose(), [focusGeometry]);
2600
- (0, import_fiber8.useFrame)(({ camera }) => {
4246
+ (0, import_react16.useEffect)(() => () => focusGeometry.dispose(), [focusGeometry]);
4247
+ (0, import_fiber13.useFrame)(({ camera }) => {
2601
4248
  if (!groupRef.current) {
2602
4249
  return;
2603
4250
  }
@@ -2608,7 +4255,7 @@ function HotspotAnchor({
2608
4255
  }
2609
4256
  groupRef.current.rotateOnAxis(
2610
4257
  LOCAL_FORWARD,
2611
- import_three9.MathUtils.degToRad(rotation)
4258
+ import_three9.MathUtils.degToRad(-rotation)
2612
4259
  );
2613
4260
  const scaleFactor = scaleMode === "fixed" && camera instanceof import_three9.PerspectiveCamera ? fixedScaleFactor(camera.fov, referenceFov) : 1;
2614
4261
  groupRef.current.scale.set(
@@ -2643,7 +4290,7 @@ function HotspotAnchor({
2643
4290
  dragState.releaseInteractionLock();
2644
4291
  dragStateRef.current = null;
2645
4292
  };
2646
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4293
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2647
4294
  "group",
2648
4295
  {
2649
4296
  ref: groupRef,
@@ -2713,14 +4360,14 @@ function HotspotAnchor({
2713
4360
  } : void 0,
2714
4361
  onPointerUp: interactive ? (event) => releasePointer(event, true) : void 0,
2715
4362
  children: [
2716
- focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4363
+ focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2717
4364
  "lineSegments",
2718
4365
  {
2719
4366
  geometry: focusGeometry,
2720
4367
  position: [0, 0, 0.01],
2721
4368
  renderOrder: renderOrder + 1,
2722
4369
  scale: [1.12, 1.12, 1],
2723
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
4370
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
2724
4371
  }
2725
4372
  ) : null,
2726
4373
  children
@@ -2730,7 +4377,7 @@ function HotspotAnchor({
2730
4377
  }
2731
4378
 
2732
4379
  // src/hotspot/image-hotspot.tsx
2733
- var import_jsx_runtime9 = require("react/jsx-runtime");
4380
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2734
4381
  function clampOpacity(opacity) {
2735
4382
  if (!Number.isFinite(opacity)) {
2736
4383
  return 1;
@@ -2738,13 +4385,13 @@ function clampOpacity(opacity) {
2738
4385
  return Math.max(0, Math.min(opacity, 1));
2739
4386
  }
2740
4387
  function useHotspotImageTexture(src, onLoad, onError) {
2741
- const gl = (0, import_fiber9.useThree)((state) => state.gl);
2742
- const [texture, setTexture] = (0, import_react10.useState)(null);
2743
- const onLoadRef = (0, import_react10.useRef)(onLoad);
2744
- const onErrorRef = (0, import_react10.useRef)(onError);
4388
+ const gl = (0, import_fiber14.useThree)((state) => state.gl);
4389
+ const [texture, setTexture] = (0, import_react17.useState)(null);
4390
+ const onLoadRef = (0, import_react17.useRef)(onLoad);
4391
+ const onErrorRef = (0, import_react17.useRef)(onError);
2745
4392
  onLoadRef.current = onLoad;
2746
4393
  onErrorRef.current = onError;
2747
- (0, import_react10.useEffect)(() => {
4394
+ (0, import_react17.useEffect)(() => {
2748
4395
  let active = true;
2749
4396
  setTexture(null);
2750
4397
  if (!src) {
@@ -2782,7 +4429,7 @@ function useHotspotImageTexture(src, onLoad, onError) {
2782
4429
  active = false;
2783
4430
  };
2784
4431
  }, [gl, src]);
2785
- (0, import_react10.useEffect)(
4432
+ (0, import_react17.useEffect)(
2786
4433
  () => () => {
2787
4434
  texture?.dispose();
2788
4435
  },
@@ -2800,9 +4447,9 @@ function ImageHotspot({
2800
4447
  ...anchorProps
2801
4448
  }) {
2802
4449
  const texture = useHotspotImageTexture(src, onLoad, onError);
2803
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("mesh", { children: [
2804
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("planeGeometry", { args: [1, 1] }),
2805
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4450
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("mesh", { children: [
4451
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("planeGeometry", { args: [1, 1] }),
4452
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2806
4453
  "meshBasicMaterial",
2807
4454
  {
2808
4455
  map: texture,
@@ -2816,13 +4463,13 @@ function ImageHotspot({
2816
4463
  }
2817
4464
 
2818
4465
  // src/hotspot/polygon-hotspot.tsx
2819
- var import_fiber10 = require("@react-three/fiber");
2820
- var import_react11 = require("react");
4466
+ var import_fiber15 = require("@react-three/fiber");
4467
+ var import_react18 = require("react");
2821
4468
  var import_three11 = require("three");
2822
4469
  var import_Line2 = require("three/addons/lines/Line2.js");
2823
4470
  var import_LineGeometry = require("three/addons/lines/LineGeometry.js");
2824
4471
  var import_LineMaterial = require("three/addons/lines/LineMaterial.js");
2825
- var import_jsx_runtime10 = require("react/jsx-runtime");
4472
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2826
4473
  var POLYGON_RADIUS = 990;
2827
4474
  var MAX_FILL_SEGMENT_DEGREES = 7;
2828
4475
  function cloneVertices(vertices) {
@@ -3095,38 +4742,38 @@ function PolygonHotspot({
3095
4742
  onInvalid,
3096
4743
  ...anchorProps
3097
4744
  }) {
3098
- const gl = (0, import_fiber10.useThree)((state) => state.gl);
3099
- const size = (0, import_fiber10.useThree)((state) => state.size);
3100
- const issues = (0, import_react11.useMemo)(() => validatePolygonVertices(vertices), [vertices]);
4745
+ const gl = (0, import_fiber15.useThree)((state) => state.gl);
4746
+ const size = (0, import_fiber15.useThree)((state) => state.size);
4747
+ const issues = (0, import_react18.useMemo)(() => validatePolygonVertices(vertices), [vertices]);
3101
4748
  const valid = issues.length === 0;
3102
- const center = (0, import_react11.useMemo)(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
3103
- const fillGeometry = (0, import_react11.useMemo)(
4749
+ const center = (0, import_react18.useMemo)(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
4750
+ const fillGeometry = (0, import_react18.useMemo)(
3104
4751
  () => valid && center ? makeFillGeometry(vertices, center) : null,
3105
4752
  [center, valid, vertices]
3106
4753
  );
3107
- const strokePositions = (0, import_react11.useMemo)(
4754
+ const strokePositions = (0, import_react18.useMemo)(
3108
4755
  () => valid && center ? makeStrokePositions(vertices, center) : null,
3109
4756
  [center, valid, vertices]
3110
4757
  );
3111
4758
  const resolvedStrokeOpacity = Math.max(0, Math.min(strokeOpacity, 1));
3112
4759
  const devicePixelRatio = gl.getPixelRatio();
3113
- const strokeLine = (0, import_react11.useMemo)(
4760
+ const strokeLine = (0, import_react18.useMemo)(
3114
4761
  () => strokePositions ? makeStrokeLine(strokePositions, stroke, strokeWidth * devicePixelRatio, resolvedStrokeOpacity) : null,
3115
4762
  [devicePixelRatio, resolvedStrokeOpacity, stroke, strokePositions, strokeWidth]
3116
4763
  );
3117
- const focusStrokeLine = (0, import_react11.useMemo)(
4764
+ const focusStrokeLine = (0, import_react18.useMemo)(
3118
4765
  () => strokePositions ? makeStrokeLine(strokePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3119
4766
  [devicePixelRatio, strokePositions, strokeWidth]
3120
4767
  );
3121
- const startVerticesRef = (0, import_react11.useRef)(null);
4768
+ const startVerticesRef = (0, import_react18.useRef)(null);
3122
4769
  const issuesKey = issues.map((issue) => issue.code).join(",");
3123
- (0, import_react11.useEffect)(() => {
4770
+ (0, import_react18.useEffect)(() => {
3124
4771
  if (!valid) onInvalid?.(issues);
3125
4772
  }, [issues, issuesKey, onInvalid, valid]);
3126
- (0, import_react11.useEffect)(() => () => fillGeometry?.dispose(), [fillGeometry]);
3127
- (0, import_react11.useEffect)(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
3128
- (0, import_react11.useEffect)(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
3129
- (0, import_react11.useEffect)(() => {
4773
+ (0, import_react18.useEffect)(() => () => fillGeometry?.dispose(), [fillGeometry]);
4774
+ (0, import_react18.useEffect)(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
4775
+ (0, import_react18.useEffect)(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
4776
+ (0, import_react18.useEffect)(() => {
3130
4777
  const width = gl.domElement.width;
3131
4778
  const height = gl.domElement.height;
3132
4779
  for (const line of [strokeLine, focusStrokeLine]) {
@@ -3145,7 +4792,7 @@ function PolygonHotspot({
3145
4792
  startPosition: event.startPosition
3146
4793
  });
3147
4794
  };
3148
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
4795
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3149
4796
  HotspotAnchor,
3150
4797
  {
3151
4798
  ...anchorProps,
@@ -3160,7 +4807,7 @@ function PolygonHotspot({
3160
4807
  draggable,
3161
4808
  interactive,
3162
4809
  useAngularScale: false,
3163
- focusContent: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
4810
+ focusContent: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
3164
4811
  onDragStart: (event) => {
3165
4812
  startVerticesRef.current = cloneVertices(vertices);
3166
4813
  emitVerticesChange(event, onDragStart);
@@ -3171,7 +4818,7 @@ function PolygonHotspot({
3171
4818
  startVerticesRef.current = null;
3172
4819
  },
3173
4820
  children: [
3174
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4821
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3175
4822
  "meshBasicMaterial",
3176
4823
  {
3177
4824
  color: fill,
@@ -3181,20 +4828,20 @@ function PolygonHotspot({
3181
4828
  transparent: true
3182
4829
  }
3183
4830
  ) }),
3184
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
4831
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
3185
4832
  ]
3186
4833
  }
3187
4834
  );
3188
4835
  }
3189
4836
 
3190
4837
  // src/hotspot/polyline-hotspot.tsx
3191
- var import_fiber11 = require("@react-three/fiber");
3192
- var import_react12 = require("react");
4838
+ var import_fiber16 = require("@react-three/fiber");
4839
+ var import_react19 = require("react");
3193
4840
  var import_three12 = require("three");
3194
4841
  var import_Line22 = require("three/addons/lines/Line2.js");
3195
4842
  var import_LineGeometry2 = require("three/addons/lines/LineGeometry.js");
3196
4843
  var import_LineMaterial2 = require("three/addons/lines/LineMaterial.js");
3197
- var import_jsx_runtime11 = require("react/jsx-runtime");
4844
+ var import_jsx_runtime12 = require("react/jsx-runtime");
3198
4845
  var POLYLINE_RADIUS = 990;
3199
4846
  var MAX_SEGMENT_DEGREES = 7;
3200
4847
  function cloneVertices2(vertices) {
@@ -3331,32 +4978,32 @@ function PolylineHotspot({
3331
4978
  onInvalid,
3332
4979
  ...anchorProps
3333
4980
  }) {
3334
- const gl = (0, import_fiber11.useThree)((state) => state.gl);
3335
- const size = (0, import_fiber11.useThree)((state) => state.size);
3336
- const issues = (0, import_react12.useMemo)(() => validatePolylineVertices(vertices), [vertices]);
4981
+ const gl = (0, import_fiber16.useThree)((state) => state.gl);
4982
+ const size = (0, import_fiber16.useThree)((state) => state.size);
4983
+ const issues = (0, import_react19.useMemo)(() => validatePolylineVertices(vertices), [vertices]);
3337
4984
  const valid = issues.length === 0;
3338
- const center = (0, import_react12.useMemo)(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
3339
- const linePositions = (0, import_react12.useMemo)(
4985
+ const center = (0, import_react19.useMemo)(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
4986
+ const linePositions = (0, import_react19.useMemo)(
3340
4987
  () => valid && center ? makeLinePositions(vertices, center) : null,
3341
4988
  [center, valid, vertices]
3342
4989
  );
3343
4990
  const devicePixelRatio = gl.getPixelRatio();
3344
4991
  const opacity = Math.max(0, Math.min(strokeOpacity, 1));
3345
- const line = (0, import_react12.useMemo)(
4992
+ const line = (0, import_react19.useMemo)(
3346
4993
  () => linePositions ? makeLine(linePositions, stroke, strokeWidth * devicePixelRatio, opacity) : null,
3347
4994
  [devicePixelRatio, linePositions, opacity, stroke, strokeWidth]
3348
4995
  );
3349
- const focusLine = (0, import_react12.useMemo)(
4996
+ const focusLine = (0, import_react19.useMemo)(
3350
4997
  () => linePositions ? makeLine(linePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3351
4998
  [devicePixelRatio, linePositions, strokeWidth]
3352
4999
  );
3353
- const startVerticesRef = (0, import_react12.useRef)(null);
3354
- (0, import_react12.useEffect)(() => {
5000
+ const startVerticesRef = (0, import_react19.useRef)(null);
5001
+ (0, import_react19.useEffect)(() => {
3355
5002
  if (!valid) onInvalid?.(issues);
3356
5003
  }, [issues, onInvalid, valid]);
3357
- (0, import_react12.useEffect)(() => () => disposeLine(line), [line]);
3358
- (0, import_react12.useEffect)(() => () => disposeLine(focusLine), [focusLine]);
3359
- (0, import_react12.useEffect)(() => {
5004
+ (0, import_react19.useEffect)(() => () => disposeLine(line), [line]);
5005
+ (0, import_react19.useEffect)(() => () => disposeLine(focusLine), [focusLine]);
5006
+ (0, import_react19.useEffect)(() => {
3360
5007
  const width = gl.domElement.width;
3361
5008
  const height = gl.domElement.height;
3362
5009
  for (const nextLine of [line, focusLine]) {
@@ -3374,7 +5021,7 @@ function PolylineHotspot({
3374
5021
  startPosition: event.startPosition
3375
5022
  });
3376
5023
  };
3377
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5024
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3378
5025
  HotspotAnchor,
3379
5026
  {
3380
5027
  ...anchorProps,
@@ -3388,7 +5035,7 @@ function PolylineHotspot({
3388
5035
  renderOrder,
3389
5036
  draggable,
3390
5037
  useAngularScale: false,
3391
- focusContent: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
5038
+ focusContent: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
3392
5039
  onDragStart: (event) => {
3393
5040
  startVerticesRef.current = cloneVertices2(vertices);
3394
5041
  emitVerticesChange(event, onDragStart);
@@ -3398,15 +5045,15 @@ function PolylineHotspot({
3398
5045
  emitVerticesChange(event, onDragEnd);
3399
5046
  startVerticesRef.current = null;
3400
5047
  },
3401
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("primitive", { object: line, renderOrder: renderOrder + 1 })
5048
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("primitive", { object: line, renderOrder: renderOrder + 1 })
3402
5049
  }
3403
5050
  );
3404
5051
  }
3405
5052
 
3406
5053
  // src/hotspot/graphic-hotspot.tsx
3407
- var import_react13 = require("react");
5054
+ var import_react20 = require("react");
3408
5055
  var import_three13 = require("three");
3409
- var import_jsx_runtime12 = require("react/jsx-runtime");
5056
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3410
5057
  var GRAPHIC_TEXTURE_SIZE = 512;
3411
5058
  function clampOpacity2(opacity) {
3412
5059
  if (!Number.isFinite(opacity)) {
@@ -3417,14 +5064,34 @@ function clampOpacity2(opacity) {
3417
5064
  function resolveStrokeWidth(value) {
3418
5065
  return Number.isFinite(value) ? Math.max(value, 0) : 8;
3419
5066
  }
3420
- function roundedRectanglePath(context, x, y, width, height, radius) {
3421
- const safeRadius = Math.min(Math.max(radius, 0), width / 2, height / 2);
5067
+ function resolveCornerRadius(value) {
5068
+ return Number.isFinite(value) ? Math.max(0, Math.min(value, 0.5)) : 0;
5069
+ }
5070
+ function roundedRectanglePath(context, x, y, width, height, radiusRatio, aspectWidth, aspectHeight) {
5071
+ const safeAspectWidth = Number.isFinite(aspectWidth) && aspectWidth > 0 ? aspectWidth : 1;
5072
+ const safeAspectHeight = Number.isFinite(aspectHeight) && aspectHeight > 0 ? aspectHeight : 1;
5073
+ const shortSide = Math.min(safeAspectWidth, safeAspectHeight);
5074
+ const safeRadiusRatio = resolveCornerRadius(radiusRatio);
5075
+ const radiusX = Math.min(width / 2, width * safeRadiusRatio * shortSide / safeAspectWidth);
5076
+ const radiusY = Math.min(height / 2, height * safeRadiusRatio * shortSide / safeAspectHeight);
5077
+ context.beginPath();
5078
+ context.moveTo(x + radiusX, y);
5079
+ context.lineTo(x + width - radiusX, y);
5080
+ context.ellipse(x + width - radiusX, y + radiusY, radiusX, radiusY, 0, -Math.PI / 2, 0);
5081
+ context.lineTo(x + width, y + height - radiusY);
5082
+ context.ellipse(x + width - radiusX, y + height - radiusY, radiusX, radiusY, 0, 0, Math.PI / 2);
5083
+ context.lineTo(x + radiusX, y + height);
5084
+ context.ellipse(x + radiusX, y + height - radiusY, radiusX, radiusY, 0, Math.PI / 2, Math.PI);
5085
+ context.lineTo(x, y + radiusY);
5086
+ context.ellipse(x + radiusX, y + radiusY, radiusX, radiusY, 0, Math.PI, Math.PI * 1.5);
5087
+ context.closePath();
5088
+ }
5089
+ function polygonPath(context, points) {
3422
5090
  context.beginPath();
3423
- context.moveTo(x + safeRadius, y);
3424
- context.arcTo(x + width, y, x + width, y + height, safeRadius);
3425
- context.arcTo(x + width, y + height, x, y + height, safeRadius);
3426
- context.arcTo(x, y + height, x, y, safeRadius);
3427
- context.arcTo(x, y, x + width, y, safeRadius);
5091
+ context.moveTo(points[0][0], points[0][1]);
5092
+ for (const [x, y] of points.slice(1)) {
5093
+ context.lineTo(x, y);
5094
+ }
3428
5095
  context.closePath();
3429
5096
  }
3430
5097
  function paintPath(context, graphic, path) {
@@ -3438,7 +5105,7 @@ function paintPath(context, graphic, path) {
3438
5105
  path ? context.stroke(path) : context.stroke();
3439
5106
  }
3440
5107
  }
3441
- function drawGraphic(context, graphic) {
5108
+ function drawGraphic(context, graphic, aspectWidth, aspectHeight) {
3442
5109
  const padding = Math.max(resolveStrokeWidth(graphic.strokeWidth) / 2 + 2, 4);
3443
5110
  const drawableSize = GRAPHIC_TEXTURE_SIZE - padding * 2;
3444
5111
  context.clearRect(0, 0, GRAPHIC_TEXTURE_SIZE, GRAPHIC_TEXTURE_SIZE);
@@ -3456,6 +5123,63 @@ function drawGraphic(context, graphic) {
3456
5123
  paintPath(context, graphic);
3457
5124
  return;
3458
5125
  }
5126
+ if (graphic.kind === "triangle") {
5127
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5128
+ polygonPath(context, [
5129
+ [center, padding],
5130
+ [padding + drawableSize, padding + drawableSize],
5131
+ [padding, padding + drawableSize]
5132
+ ]);
5133
+ paintPath(context, graphic);
5134
+ return;
5135
+ }
5136
+ if (graphic.kind === "diamond") {
5137
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5138
+ polygonPath(context, [
5139
+ [center, padding],
5140
+ [padding + drawableSize, center],
5141
+ [center, padding + drawableSize],
5142
+ [padding, center]
5143
+ ]);
5144
+ paintPath(context, graphic);
5145
+ return;
5146
+ }
5147
+ if (graphic.kind === "star") {
5148
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5149
+ const outerRadius = drawableSize / 2;
5150
+ const innerRadius = outerRadius * 0.45;
5151
+ const points = [];
5152
+ for (let index = 0; index < 10; index += 1) {
5153
+ const angle = -Math.PI / 2 + index * Math.PI / 5;
5154
+ const radius = index % 2 === 0 ? outerRadius : innerRadius;
5155
+ points.push([
5156
+ center + Math.cos(angle) * radius,
5157
+ center + Math.sin(angle) * radius
5158
+ ]);
5159
+ }
5160
+ polygonPath(context, points);
5161
+ paintPath(context, graphic);
5162
+ return;
5163
+ }
5164
+ if (graphic.kind === "arrow") {
5165
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
5166
+ const top = padding;
5167
+ const bottom = padding + drawableSize;
5168
+ const headBottom = top + drawableSize * 0.43;
5169
+ const headHalfWidth = drawableSize * 0.38;
5170
+ const stemHalfWidth = drawableSize * 0.14;
5171
+ polygonPath(context, [
5172
+ [center, top],
5173
+ [center + headHalfWidth, headBottom],
5174
+ [center + stemHalfWidth, headBottom],
5175
+ [center + stemHalfWidth, bottom],
5176
+ [center - stemHalfWidth, bottom],
5177
+ [center - stemHalfWidth, headBottom],
5178
+ [center - headHalfWidth, headBottom]
5179
+ ]);
5180
+ paintPath(context, graphic);
5181
+ return;
5182
+ }
3459
5183
  if (graphic.kind === "rectangle") {
3460
5184
  roundedRectanglePath(
3461
5185
  context,
@@ -3463,7 +5187,9 @@ function drawGraphic(context, graphic) {
3463
5187
  padding,
3464
5188
  drawableSize,
3465
5189
  drawableSize,
3466
- graphic.cornerRadius ?? 0
5190
+ graphic.cornerRadius ?? 0,
5191
+ aspectWidth,
5192
+ aspectHeight
3467
5193
  );
3468
5194
  paintPath(context, graphic);
3469
5195
  return;
@@ -3502,7 +5228,7 @@ function drawGraphic(context, graphic) {
3502
5228
  paintPath(context, graphic, path);
3503
5229
  context.restore();
3504
5230
  }
3505
- function createGraphicTexture(graphic) {
5231
+ function createGraphicTexture(graphic, aspectWidth, aspectHeight) {
3506
5232
  const canvas = document.createElement("canvas");
3507
5233
  canvas.width = GRAPHIC_TEXTURE_SIZE;
3508
5234
  canvas.height = GRAPHIC_TEXTURE_SIZE;
@@ -3510,7 +5236,7 @@ function createGraphicTexture(graphic) {
3510
5236
  if (!context) {
3511
5237
  throw new Error("Canvas 2D is unavailable for this graphic hotspot.");
3512
5238
  }
3513
- drawGraphic(context, graphic);
5239
+ drawGraphic(context, graphic, aspectWidth, aspectHeight);
3514
5240
  const texture = new import_three13.CanvasTexture(canvas);
3515
5241
  texture.colorSpace = import_three13.SRGBColorSpace;
3516
5242
  texture.wrapS = import_three13.ClampToEdgeWrapping;
@@ -3521,16 +5247,16 @@ function createGraphicTexture(graphic) {
3521
5247
  texture.needsUpdate = true;
3522
5248
  return texture;
3523
5249
  }
3524
- function useGraphicTexture(graphic, onLoad, onError) {
3525
- const [texture, setTexture] = (0, import_react13.useState)(null);
3526
- const onLoadRef = (0, import_react13.useRef)(onLoad);
3527
- const onErrorRef = (0, import_react13.useRef)(onError);
5250
+ function useGraphicTexture(graphic, aspectWidth, aspectHeight, onLoad, onError) {
5251
+ const [texture, setTexture] = (0, import_react20.useState)(null);
5252
+ const onLoadRef = (0, import_react20.useRef)(onLoad);
5253
+ const onErrorRef = (0, import_react20.useRef)(onError);
3528
5254
  onLoadRef.current = onLoad;
3529
5255
  onErrorRef.current = onError;
3530
- (0, import_react13.useEffect)(() => {
5256
+ (0, import_react20.useEffect)(() => {
3531
5257
  let nextTexture = null;
3532
5258
  try {
3533
- nextTexture = createGraphicTexture(graphic);
5259
+ nextTexture = createGraphicTexture(graphic, aspectWidth, aspectHeight);
3534
5260
  setTexture(nextTexture);
3535
5261
  onLoadRef.current?.(nextTexture);
3536
5262
  } catch (error) {
@@ -3540,7 +5266,7 @@ function useGraphicTexture(graphic, onLoad, onError) {
3540
5266
  return () => {
3541
5267
  nextTexture?.dispose();
3542
5268
  };
3543
- }, [graphic]);
5269
+ }, [aspectHeight, aspectWidth, graphic]);
3544
5270
  return texture;
3545
5271
  }
3546
5272
  function CanvasGraphicHotspot({
@@ -3552,10 +5278,10 @@ function CanvasGraphicHotspot({
3552
5278
  onError,
3553
5279
  ...anchorProps
3554
5280
  }) {
3555
- const texture = useGraphicTexture(graphic, onLoad, onError);
3556
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("mesh", { children: [
3557
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("planeGeometry", { args: [1, 1] }),
3558
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
5281
+ const texture = useGraphicTexture(graphic, width, height, onLoad, onError);
5282
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("mesh", { children: [
5283
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("planeGeometry", { args: [1, 1] }),
5284
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3559
5285
  "meshBasicMaterial",
3560
5286
  {
3561
5287
  map: texture,
@@ -3571,16 +5297,16 @@ function GraphicHotspot(props) {
3571
5297
  const { graphic, ...restProps } = props;
3572
5298
  if (graphic.kind === "svg") {
3573
5299
  const imageProps = restProps;
3574
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ImageHotspot, { ...imageProps, src: graphic.src });
5300
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ImageHotspot, { ...imageProps, src: graphic.src });
3575
5301
  }
3576
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CanvasGraphicHotspot, { ...restProps, graphic });
5302
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CanvasGraphicHotspot, { ...restProps, graphic });
3577
5303
  }
3578
5304
 
3579
5305
  // src/hotspot/sequence-hotspot.tsx
3580
- var import_fiber12 = require("@react-three/fiber");
3581
- var import_react14 = require("react");
5306
+ var import_fiber17 = require("@react-three/fiber");
5307
+ var import_react21 = require("react");
3582
5308
  var import_three14 = require("three");
3583
- var import_jsx_runtime13 = require("react/jsx-runtime");
5309
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3584
5310
  var DEFAULT_SEQUENCE_FPS = 12;
3585
5311
  function clampOpacity3(opacity) {
3586
5312
  if (!Number.isFinite(opacity)) {
@@ -3600,13 +5326,13 @@ function useSpriteTexture({
3600
5326
  onError,
3601
5327
  onLoadProgress
3602
5328
  }) {
3603
- const gl = (0, import_fiber12.useThree)((state) => state.gl);
3604
- const [texture, setTexture] = (0, import_react14.useState)(null);
3605
- const onErrorRef = (0, import_react14.useRef)(onError);
3606
- const onLoadProgressRef = (0, import_react14.useRef)(onLoadProgress);
5329
+ const gl = (0, import_fiber17.useThree)((state) => state.gl);
5330
+ const [texture, setTexture] = (0, import_react21.useState)(null);
5331
+ const onErrorRef = (0, import_react21.useRef)(onError);
5332
+ const onLoadProgressRef = (0, import_react21.useRef)(onLoadProgress);
3607
5333
  onErrorRef.current = onError;
3608
5334
  onLoadProgressRef.current = onLoadProgress;
3609
- (0, import_react14.useEffect)(() => {
5335
+ (0, import_react21.useEffect)(() => {
3610
5336
  let active = true;
3611
5337
  setTexture(null);
3612
5338
  if (!src) {
@@ -3647,7 +5373,7 @@ function useSpriteTexture({
3647
5373
  active = false;
3648
5374
  };
3649
5375
  }, [gl, id, src]);
3650
- (0, import_react14.useEffect)(
5376
+ (0, import_react21.useEffect)(
3651
5377
  () => () => {
3652
5378
  texture?.dispose();
3653
5379
  },
@@ -3676,24 +5402,24 @@ function SequenceHotspot({
3676
5402
  const texture = useSpriteTexture({ id, src, onError, onLoadProgress });
3677
5403
  const frameCount = resolveFrameCount(requestedFrameCount);
3678
5404
  const safeFps = resolveFps(fps);
3679
- const [frameIndex, setFrameIndex] = (0, import_react14.useState)(0);
3680
- const elapsedRef = (0, import_react14.useRef)(0);
3681
- const frameIndexRef = (0, import_react14.useRef)(0);
3682
- const endedRef = (0, import_react14.useRef)(false);
3683
- const lastPlaybackStateRef = (0, import_react14.useRef)(null);
3684
- const onFrameChangeRef = (0, import_react14.useRef)(onFrameChange);
3685
- const onPlaybackStateChangeRef = (0, import_react14.useRef)(onPlaybackStateChange);
3686
- const onEndedRef = (0, import_react14.useRef)(onEnded);
5405
+ const [frameIndex, setFrameIndex] = (0, import_react21.useState)(0);
5406
+ const elapsedRef = (0, import_react21.useRef)(0);
5407
+ const frameIndexRef = (0, import_react21.useRef)(0);
5408
+ const endedRef = (0, import_react21.useRef)(false);
5409
+ const lastPlaybackStateRef = (0, import_react21.useRef)(null);
5410
+ const onFrameChangeRef = (0, import_react21.useRef)(onFrameChange);
5411
+ const onPlaybackStateChangeRef = (0, import_react21.useRef)(onPlaybackStateChange);
5412
+ const onEndedRef = (0, import_react21.useRef)(onEnded);
3687
5413
  onFrameChangeRef.current = onFrameChange;
3688
5414
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3689
5415
  onEndedRef.current = onEnded;
3690
- (0, import_react14.useEffect)(() => {
5416
+ (0, import_react21.useEffect)(() => {
3691
5417
  elapsedRef.current = 0;
3692
5418
  frameIndexRef.current = 0;
3693
5419
  endedRef.current = false;
3694
5420
  setFrameIndex(0);
3695
5421
  }, [frameCount, frameDirection, src]);
3696
- (0, import_react14.useEffect)(() => {
5422
+ (0, import_react21.useEffect)(() => {
3697
5423
  if (!texture) {
3698
5424
  return;
3699
5425
  }
@@ -3707,13 +5433,13 @@ function SequenceHotspot({
3707
5433
  );
3708
5434
  texture.needsUpdate = true;
3709
5435
  }, [frameCount, frameDirection, frameIndex, texture]);
3710
- (0, import_react14.useEffect)(() => {
5436
+ (0, import_react21.useEffect)(() => {
3711
5437
  if (!texture) {
3712
5438
  return;
3713
5439
  }
3714
5440
  onFrameChangeRef.current?.({ id, index: 0, src });
3715
5441
  }, [id, src, texture]);
3716
- (0, import_react14.useEffect)(() => {
5442
+ (0, import_react21.useEffect)(() => {
3717
5443
  if (!texture) {
3718
5444
  return;
3719
5445
  }
@@ -3732,7 +5458,7 @@ function SequenceHotspot({
3732
5458
  onPlaybackStateChangeRef.current?.(nextState);
3733
5459
  }
3734
5460
  }, [playing, texture]);
3735
- (0, import_fiber12.useFrame)((_, deltaSeconds) => {
5461
+ (0, import_fiber17.useFrame)((_, deltaSeconds) => {
3736
5462
  if (!texture || !playing || endedRef.current) {
3737
5463
  return;
3738
5464
  }
@@ -3762,9 +5488,9 @@ function SequenceHotspot({
3762
5488
  onFrameChangeRef.current?.({ id, index: nextIndex, src });
3763
5489
  }
3764
5490
  });
3765
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("mesh", { children: [
3766
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("planeGeometry", { args: [1, 1] }),
3767
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
5491
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("mesh", { children: [
5492
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("planeGeometry", { args: [1, 1] }),
5493
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3768
5494
  "meshBasicMaterial",
3769
5495
  {
3770
5496
  map: texture,
@@ -3778,10 +5504,10 @@ function SequenceHotspot({
3778
5504
  }
3779
5505
 
3780
5506
  // src/hotspot/video-hotspot.tsx
3781
- var import_fiber13 = require("@react-three/fiber");
3782
- var import_react15 = require("react");
5507
+ var import_fiber18 = require("@react-three/fiber");
5508
+ var import_react22 = require("react");
3783
5509
  var import_three15 = require("three");
3784
- var import_jsx_runtime14 = require("react/jsx-runtime");
5510
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3785
5511
  function clampOpacity4(opacity) {
3786
5512
  if (!Number.isFinite(opacity)) {
3787
5513
  return 1;
@@ -3795,11 +5521,11 @@ function clampVolume(volume) {
3795
5521
  return Math.max(0, Math.min(volume, 1));
3796
5522
  }
3797
5523
  function usePosterTexture(id, poster, onError) {
3798
- const gl = (0, import_fiber13.useThree)((state) => state.gl);
3799
- const [texture, setTexture] = (0, import_react15.useState)(null);
3800
- const onErrorRef = (0, import_react15.useRef)(onError);
5524
+ const gl = (0, import_fiber18.useThree)((state) => state.gl);
5525
+ const [texture, setTexture] = (0, import_react22.useState)(null);
5526
+ const onErrorRef = (0, import_react22.useRef)(onError);
3801
5527
  onErrorRef.current = onError;
3802
- (0, import_react15.useEffect)(() => {
5528
+ (0, import_react22.useEffect)(() => {
3803
5529
  let active = true;
3804
5530
  setTexture(null);
3805
5531
  if (!poster) {
@@ -3836,7 +5562,7 @@ function usePosterTexture(id, poster, onError) {
3836
5562
  active = false;
3837
5563
  };
3838
5564
  }, [gl, id, poster]);
3839
- (0, import_react15.useEffect)(
5565
+ (0, import_react22.useEffect)(
3840
5566
  () => () => {
3841
5567
  texture?.dispose();
3842
5568
  },
@@ -3854,14 +5580,14 @@ function useVideoResource({
3854
5580
  poster,
3855
5581
  src
3856
5582
  }) {
3857
- const [resource, setResource] = (0, import_react15.useState)(null);
3858
- const onEndedRef = (0, import_react15.useRef)(onEnded);
3859
- const onErrorRef = (0, import_react15.useRef)(onError);
3860
- const onPlaybackStateChangeRef = (0, import_react15.useRef)(onPlaybackStateChange);
5583
+ const [resource, setResource] = (0, import_react22.useState)(null);
5584
+ const onEndedRef = (0, import_react22.useRef)(onEnded);
5585
+ const onErrorRef = (0, import_react22.useRef)(onError);
5586
+ const onPlaybackStateChangeRef = (0, import_react22.useRef)(onPlaybackStateChange);
3861
5587
  onEndedRef.current = onEnded;
3862
5588
  onErrorRef.current = onError;
3863
5589
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3864
- (0, import_react15.useEffect)(() => {
5590
+ (0, import_react22.useEffect)(() => {
3865
5591
  if (!src) {
3866
5592
  setResource(null);
3867
5593
  return;
@@ -3962,11 +5688,11 @@ function VideoHotspot({
3962
5688
  src
3963
5689
  });
3964
5690
  const posterTexture = usePosterTexture(id, poster, onError);
3965
- const onPlaybackErrorRef = (0, import_react15.useRef)(onPlaybackError);
3966
- const onPlaybackStateChangeRef = (0, import_react15.useRef)(onPlaybackStateChange);
5691
+ const onPlaybackErrorRef = (0, import_react22.useRef)(onPlaybackError);
5692
+ const onPlaybackStateChangeRef = (0, import_react22.useRef)(onPlaybackStateChange);
3967
5693
  onPlaybackErrorRef.current = onPlaybackError;
3968
5694
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3969
- (0, import_react15.useEffect)(() => {
5695
+ (0, import_react22.useEffect)(() => {
3970
5696
  if (!resource) {
3971
5697
  return;
3972
5698
  }
@@ -3976,7 +5702,7 @@ function VideoHotspot({
3976
5702
  resource.video.playsInline = playsInline;
3977
5703
  resource.video.preload = preload;
3978
5704
  }, [loop, muted, playsInline, preload, resource, volume]);
3979
- (0, import_react15.useEffect)(() => {
5705
+ (0, import_react22.useEffect)(() => {
3980
5706
  if (!resource) {
3981
5707
  return;
3982
5708
  }
@@ -3999,9 +5725,9 @@ function VideoHotspot({
3999
5725
  };
4000
5726
  }, [playing, resource]);
4001
5727
  const activeTexture = resource?.ready ? resource.texture : posterTexture;
4002
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("mesh", { children: [
4003
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("planeGeometry", { args: [1, 1] }),
4004
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5728
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("mesh", { children: [
5729
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("planeGeometry", { args: [1, 1] }),
5730
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
4005
5731
  "meshBasicMaterial",
4006
5732
  {
4007
5733
  map: activeTexture,
@@ -4018,7 +5744,12 @@ function VideoHotspot({
4018
5744
  AutoRotate,
4019
5745
  GraphicHotspot,
4020
5746
  ImageHotspot,
5747
+ KeyboardControls,
4021
5748
  MAX_HOTSPOT_PITCH,
5749
+ MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY,
5750
+ MouseControls,
5751
+ PanoContextMenu,
5752
+ PanoEvents,
4022
5753
  PanoView,
4023
5754
  PanoramaScenes,
4024
5755
  PolygonHotspot,
@@ -4026,12 +5757,19 @@ function VideoHotspot({
4026
5757
  SequenceHotspot,
4027
5758
  Sphere,
4028
5759
  Tile,
5760
+ TouchControls,
4029
5761
  VideoHotspot,
4030
5762
  clampPanoPitch,
5763
+ composePanoContextMenuItems,
5764
+ createDefaultPanoContextMenuItems,
5765
+ createPanoContextMenuPresets,
5766
+ cycleSceneId,
4031
5767
  normalizePanoPosition,
4032
5768
  normalizePanoYaw,
4033
5769
  panoPositionToVector3,
5770
+ resolvePanoContextMenuEntries,
4034
5771
  unwrapPolygonVertices,
5772
+ usePanoEvents,
4035
5773
  validatePolygonVertices,
4036
5774
  validatePolylineVertices,
4037
5775
  vector3ToPanoPosition