@ericchen1990/pano-view 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
- var MAX_PITCH = 89.9;
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,138 +610,1647 @@ 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 = 89.9;
582
-
583
- // src/hotspot/coordinates.ts
584
- function normalizePanoYaw(yaw) {
585
- if (!Number.isFinite(yaw)) {
586
- 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 [];
587
641
  }
588
- return import_three2.MathUtils.euclideanModulo(yaw + 180, 360) - 180;
642
+ return (typeof value === "string" ? [value] : [...value]).filter(
643
+ (entry) => entry.length > 0
644
+ );
589
645
  }
590
- function clampPanoPitch(pitch) {
591
- if (!Number.isFinite(pitch)) {
592
- 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
+ }
593
653
  }
594
- return import_three2.MathUtils.clamp(pitch, -MAX_HOTSPOT_PITCH, MAX_HOTSPOT_PITCH);
654
+ return map;
595
655
  }
596
- function normalizePanoPosition(position) {
597
- return {
598
- yaw: normalizePanoYaw(position.yaw),
599
- pitch: clampPanoPitch(position.pitch)
600
- };
656
+ function matchAction(event, keyMap) {
657
+ return keyMap.get(event.key.toLowerCase()) ?? keyMap.get(event.code.toLowerCase()) ?? null;
601
658
  }
602
- function panoPositionToVector3(position, radius = 1) {
603
- const normalized = normalizePanoPosition(position);
604
- const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
605
- const yaw = import_three2.MathUtils.degToRad(normalized.yaw);
606
- const pitch = import_three2.MathUtils.degToRad(normalized.pitch);
607
- const cosPitch = Math.cos(pitch);
608
- return new import_three2.Vector3(
609
- cosPitch * Math.sin(yaw) * safeRadius,
610
- Math.sin(pitch) * safeRadius,
611
- -cosPitch * Math.cos(yaw) * safeRadius
612
- );
659
+ function resolveSpeed(value, fallback) {
660
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
613
661
  }
614
- function vector3ToPanoPosition(vector) {
615
- const length = Math.hypot(vector.x, vector.y, vector.z);
616
- if (!Number.isFinite(length) || length === 0) {
617
- return { yaw: 0, pitch: 0 };
662
+ function cycleSceneId(scenes, activeSceneId, step) {
663
+ if (scenes.length === 0) {
664
+ return null;
618
665
  }
619
- return normalizePanoPosition({
620
- yaw: import_three2.MathUtils.radToDeg(Math.atan2(vector.x, -vector.z)),
621
- pitch: import_three2.MathUtils.radToDeg(
622
- Math.asin(import_three2.MathUtils.clamp(vector.y / length, -1, 1))
623
- )
624
- });
625
- }
626
-
627
- // src/panorama-radius.ts
628
- var DEFAULT_PANORAMA_RADIUS = 1e3;
629
- var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
630
- var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
631
-
632
- // src/panorama-event-surface.tsx
633
- var import_jsx_runtime2 = require("react/jsx-runtime");
634
- var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
635
- function makePanoramaPointerEvent(event) {
636
- const nativeEvent = event.nativeEvent;
637
- return {
638
- position: vector3ToPanoPosition(event.ray.direction),
639
- nativeEvent,
640
- pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
641
- button: nativeEvent.button,
642
- buttons: nativeEvent.buttons,
643
- clientX: nativeEvent.clientX,
644
- clientY: nativeEvent.clientY
645
- };
646
- }
647
- function emitPanoramaEvent(event, callback) {
648
- if (!callback) {
649
- return;
666
+ const index = scenes.findIndex((scene) => scene.id === activeSceneId);
667
+ if (index < 0) {
668
+ return null;
650
669
  }
651
- event.stopPropagation();
652
- callback(makePanoramaPointerEvent(event));
670
+ const nextIndex = (index + step + scenes.length) % scenes.length;
671
+ return scenes[nextIndex]?.id ?? null;
653
672
  }
654
- function PanoramaEventSurface({
655
- onClick,
656
- onDoubleClick,
657
- 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
658
683
  }) {
659
- if (!onClick && !onDoubleClick && !onPointerMove) {
660
- 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>.");
661
695
  }
662
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
663
- "mesh",
664
- {
665
- renderOrder: -200,
666
- onClick: (event) => emitPanoramaEvent(event, onClick),
667
- onDoubleClick: (event) => emitPanoramaEvent(event, onDoubleClick),
668
- onPointerMove: (event) => emitPanoramaEvent(event, onPointerMove),
669
- children: [
670
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
671
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
672
- "meshBasicMaterial",
673
- {
674
- colorWrite: false,
675
- depthTest: false,
676
- depthWrite: false,
677
- opacity: 0,
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)(
2035
+ "mesh",
2036
+ {
2037
+ renderOrder: -200,
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),
2043
+ children: [
2044
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
2045
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2046
+ "meshBasicMaterial",
2047
+ {
2048
+ colorWrite: false,
2049
+ depthTest: false,
2050
+ depthWrite: false,
2051
+ opacity: 0,
678
2052
  side: import_three3.BackSide,
679
2053
  transparent: true
680
2054
  }
681
- )
682
- ]
683
- }
684
- );
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;
685
2205
  }
686
2206
 
687
2207
  // src/pano-view.tsx
688
- var import_jsx_runtime3 = require("react/jsx-runtime");
2208
+ var import_jsx_runtime4 = require("react/jsx-runtime");
689
2209
  var DEFAULT_VIEW = {
690
2210
  yaw: 0,
691
2211
  pitch: 0,
692
2212
  fov: 75
693
2213
  };
2214
+ var DEFAULT_POINTER_ROTATE_SPEED = 0.35;
2215
+ var DEFAULT_MOUSE_ZOOM_SPEED = 0.08;
694
2216
  var DEFAULT_CANVAS_STYLE = {
695
2217
  display: "block",
696
2218
  height: "100%",
697
2219
  width: "100%"
698
2220
  };
699
- 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)(
700
2247
  function PanoView2({
701
2248
  children,
702
2249
  initialView,
703
2250
  minFov = 30,
704
2251
  maxFov = 100,
705
2252
  controls = true,
2253
+ contextMenu = true,
706
2254
  onViewChange,
707
2255
  onPanoramaClick,
708
2256
  onPanoramaDoubleClick,
@@ -712,19 +2260,21 @@ var PanoView = (0, import_react4.forwardRef)(
712
2260
  "aria-label": ariaLabel = "Panorama viewer",
713
2261
  ...divProps
714
2262
  }, ref) {
715
- const rootRef = (0, import_react4.useRef)(null);
716
- 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(), []);
717
2266
  const { controls: hotspotAccessibilityControls, registry } = useHotspotAccessibilityLayer();
718
- const fallbackViewRef = (0, import_react4.useRef)(DEFAULT_VIEW);
2267
+ const { api: contextMenuOverlayApi, overlay: contextMenuOverlay } = usePanoContextMenuOverlay();
2268
+ const fallbackViewRef = (0, import_react11.useRef)(DEFAULT_VIEW);
719
2269
  const normalizedMinFov = Math.max(1, Math.min(minFov, maxFov - 1));
720
2270
  const normalizedMaxFov = Math.min(
721
2271
  179,
722
2272
  Math.max(maxFov, normalizedMinFov + 1)
723
2273
  );
724
- const normalizedInitialView = (0, import_react4.useMemo)(
2274
+ const normalizedInitialView = (0, import_react11.useMemo)(
725
2275
  () => ({
726
2276
  yaw: initialView?.yaw ?? DEFAULT_VIEW.yaw,
727
- pitch: initialView?.pitch ?? DEFAULT_VIEW.pitch,
2277
+ pitch: clampPanoPitch(initialView?.pitch ?? DEFAULT_VIEW.pitch),
728
2278
  fov: Math.max(
729
2279
  normalizedMinFov,
730
2280
  Math.min(initialView?.fov ?? DEFAULT_VIEW.fov, normalizedMaxFov)
@@ -740,18 +2290,101 @@ var PanoView = (0, import_react4.forwardRef)(
740
2290
  );
741
2291
  fallbackViewRef.current = normalizedInitialView;
742
2292
  const controlsEnabled = controls !== false;
743
- const controlOptions = (0, import_react4.useMemo)(
2293
+ const controlOptions = (0, import_react11.useMemo)(
744
2294
  () => typeof controls === "object" ? controls : {},
745
2295
  [controls]
746
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
+ );
747
2380
  const legacyAutoRotateOptions = controlOptions;
748
- const [legacyAutoRotate, setLegacyAutoRotate] = (0, import_react4.useState)(
2381
+ const [legacyAutoRotate, setLegacyAutoRotate] = (0, import_react11.useState)(
749
2382
  legacyAutoRotateOptions.autoRotate ?? false
750
2383
  );
751
- (0, import_react4.useEffect)(() => {
2384
+ (0, import_react11.useEffect)(() => {
752
2385
  setLegacyAutoRotate(legacyAutoRotateOptions.autoRotate ?? false);
753
2386
  }, [legacyAutoRotateOptions.autoRotate]);
754
- (0, import_react4.useImperativeHandle)(
2387
+ (0, import_react11.useImperativeHandle)(
755
2388
  ref,
756
2389
  () => ({
757
2390
  getView: () => controlsRef.current?.getView() ?? { ...fallbackViewRef.current },
@@ -767,20 +2400,11 @@ var PanoView = (0, import_react4.forwardRef)(
767
2400
  stopAutoRotate: () => {
768
2401
  setLegacyAutoRotate(false);
769
2402
  },
770
- toggleFullscreen: async () => {
771
- if (typeof document === "undefined") {
772
- return;
773
- }
774
- if (document.fullscreenElement) {
775
- await document.exitFullscreen?.();
776
- return;
777
- }
778
- await rootRef.current?.requestFullscreen?.();
779
- }
2403
+ toggleFullscreen
780
2404
  }),
781
- []
2405
+ [toggleFullscreen]
782
2406
  );
783
- const rootStyle = (0, import_react4.useMemo)(
2407
+ const rootStyle = (0, import_react11.useMemo)(
784
2408
  () => ({
785
2409
  overflow: "hidden",
786
2410
  position: "relative",
@@ -788,7 +2412,7 @@ var PanoView = (0, import_react4.forwardRef)(
788
2412
  }),
789
2413
  [style]
790
2414
  );
791
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2415
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
792
2416
  "div",
793
2417
  {
794
2418
  ...divProps,
@@ -796,8 +2420,8 @@ var PanoView = (0, import_react4.forwardRef)(
796
2420
  "aria-label": ariaLabel,
797
2421
  style: rootStyle,
798
2422
  children: [
799
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
800
- import_fiber3.Canvas,
2423
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2424
+ import_fiber8.Canvas,
801
2425
  {
802
2426
  "aria-label": `${ariaLabel} canvas`,
803
2427
  camera: {
@@ -816,41 +2440,71 @@ var PanoView = (0, import_react4.forwardRef)(
816
2440
  },
817
2441
  style: {
818
2442
  ...DEFAULT_CANVAS_STYLE,
819
- touchAction: controlsEnabled ? "none" : "auto"
2443
+ touchAction: userControlsEnabled ? "none" : "auto"
820
2444
  },
821
- tabIndex: controlsEnabled ? 0 : void 0,
822
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(PanoramaControlsContext.Provider, { value: controlsRef, children: [
823
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
824
- PanoramaControls,
825
- {
826
- ref: controlsRef,
827
- enabled: controlsEnabled,
828
- initialView: normalizedInitialView,
829
- maxFov: normalizedMaxFov,
830
- minFov: normalizedMinFov,
831
- onViewChange,
832
- options: controlOptions
833
- }
834
- ),
835
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
836
- AutoRotate,
837
- {
838
- enabled: legacyAutoRotate,
839
- speed: legacyAutoRotateOptions.autoRotateSpeed
840
- }
841
- ),
842
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
843
- PanoramaEventSurface,
844
- {
845
- onClick: onPanoramaClick,
846
- onDoubleClick: onPanoramaDoubleClick,
847
- onPointerMove: onPanoramaPointerMove
848
- }
849
- ),
850
- children
851
- ] }) })
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
+ ) }) }) })
852
2505
  }
853
2506
  ),
2507
+ contextMenuOverlay,
854
2508
  hotspotAccessibilityControls
855
2509
  ]
856
2510
  }
@@ -859,10 +2513,10 @@ var PanoView = (0, import_react4.forwardRef)(
859
2513
  );
860
2514
 
861
2515
  // src/sphere.tsx
862
- var import_fiber4 = require("@react-three/fiber");
863
- var import_react5 = require("react");
2516
+ var import_fiber9 = require("@react-three/fiber");
2517
+ var import_react12 = require("react");
864
2518
  var import_three4 = require("three");
865
- var import_jsx_runtime4 = require("react/jsx-runtime");
2519
+ var import_jsx_runtime5 = require("react/jsx-runtime");
866
2520
  function Sphere({
867
2521
  src,
868
2522
  yawOffset = 0,
@@ -870,13 +2524,13 @@ function Sphere({
870
2524
  onError,
871
2525
  visible = true
872
2526
  }) {
873
- const gl = (0, import_fiber4.useThree)((state) => state.gl);
874
- const [texture, setTexture] = (0, import_react5.useState)(null);
875
- const onLoadRef = (0, import_react5.useRef)(onLoad);
876
- 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);
877
2531
  onLoadRef.current = onLoad;
878
2532
  onErrorRef.current = onError;
879
- (0, import_react5.useEffect)(() => {
2533
+ (0, import_react12.useEffect)(() => {
880
2534
  let active = true;
881
2535
  const loader = new import_three4.TextureLoader();
882
2536
  setTexture(null);
@@ -910,7 +2564,7 @@ function Sphere({
910
2564
  active = false;
911
2565
  };
912
2566
  }, [gl, src]);
913
- (0, import_react5.useEffect)(
2567
+ (0, import_react12.useEffect)(
914
2568
  () => () => {
915
2569
  texture?.dispose();
916
2570
  },
@@ -919,14 +2573,14 @@ function Sphere({
919
2573
  if (!texture) {
920
2574
  return null;
921
2575
  }
922
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2576
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
923
2577
  "mesh",
924
2578
  {
925
2579
  rotation: [0, import_three4.MathUtils.degToRad(90 + yawOffset), 0],
926
2580
  visible,
927
2581
  children: [
928
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
929
- /* @__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)(
930
2584
  "meshBasicMaterial",
931
2585
  {
932
2586
  map: texture,
@@ -940,13 +2594,13 @@ function Sphere({
940
2594
  }
941
2595
 
942
2596
  // src/panorama-scenes.tsx
943
- var import_fiber7 = require("@react-three/fiber");
944
- var import_react8 = require("react");
2597
+ var import_fiber12 = require("@react-three/fiber");
2598
+ var import_react15 = require("react");
945
2599
  var import_three8 = require("three");
946
2600
 
947
2601
  // src/tile/texture-manager-context.tsx
948
- var import_fiber5 = require("@react-three/fiber");
949
- var import_react6 = require("react");
2602
+ var import_fiber10 = require("@react-three/fiber");
2603
+ var import_react13 = require("react");
950
2604
 
951
2605
  // src/tile/texture-manager.ts
952
2606
  var import_three5 = require("three");
@@ -1170,8 +2824,8 @@ var TileTextureManager = class {
1170
2824
  };
1171
2825
 
1172
2826
  // src/tile/texture-manager-context.tsx
1173
- var import_jsx_runtime5 = require("react/jsx-runtime");
1174
- var TileTextureManagerContext = (0, import_react6.createContext)(
2827
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2828
+ var TileTextureManagerContext = (0, import_react13.createContext)(
1175
2829
  null
1176
2830
  );
1177
2831
  function TileTextureManagerProvider({
@@ -1179,9 +2833,9 @@ function TileTextureManagerProvider({
1179
2833
  maxTextureMemoryMb = 128,
1180
2834
  maxConcurrentLoads = 4
1181
2835
  }) {
1182
- const gl = (0, import_fiber5.useThree)((state) => state.gl);
2836
+ const gl = (0, import_fiber10.useThree)((state) => state.gl);
1183
2837
  const anisotropy = gl.capabilities.getMaxAnisotropy();
1184
- const manager = (0, import_react6.useMemo)(
2838
+ const manager = (0, import_react13.useMemo)(
1185
2839
  () => new TileTextureManager({
1186
2840
  anisotropy,
1187
2841
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1190,19 +2844,19 @@ function TileTextureManagerProvider({
1190
2844
  }),
1191
2845
  [anisotropy, maxConcurrentLoads, maxTextureMemoryMb]
1192
2846
  );
1193
- (0, import_react6.useEffect)(() => {
2847
+ (0, import_react13.useEffect)(() => {
1194
2848
  manager.resume();
1195
2849
  return () => manager.dispose();
1196
2850
  }, [manager]);
1197
- 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 });
1198
2852
  }
1199
2853
  function useSharedTileTextureManager() {
1200
- return (0, import_react6.useContext)(TileTextureManagerContext);
2854
+ return (0, import_react13.useContext)(TileTextureManagerContext);
1201
2855
  }
1202
2856
 
1203
2857
  // src/tile/tile.tsx
1204
- var import_fiber6 = require("@react-three/fiber");
1205
- var import_react7 = require("react");
2858
+ var import_fiber11 = require("@react-three/fiber");
2859
+ var import_react14 = require("react");
1206
2860
  var import_three7 = require("three");
1207
2861
 
1208
2862
  // src/tile/face-layout.ts
@@ -1383,7 +3037,7 @@ function resolveTemplateUrl(template, address) {
1383
3037
  }
1384
3038
 
1385
3039
  // src/tile/tile.tsx
1386
- var import_jsx_runtime6 = require("react/jsx-runtime");
3040
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1387
3041
  var MAX_LOD_UPDATE_INTERVAL_MS = 100;
1388
3042
  var FAST_ROTATION_THRESHOLD_RADIANS = Math.PI / 180;
1389
3043
  var FAST_FOV_THRESHOLD_DEGREES = 0.5;
@@ -1392,10 +3046,10 @@ var VISIBILITY_FOV_PADDING_DEGREES = 8;
1392
3046
  var PREVIEW_RADIUS = CUBE_RADIUS + 0.05;
1393
3047
  var SHARED_TILE_GEOMETRY = new import_three7.PlaneGeometry(1, 1);
1394
3048
  function usePreviewTexture(url, anisotropy, onError) {
1395
- const [texture, setTexture] = (0, import_react7.useState)(null);
1396
- const onErrorRef = (0, import_react7.useRef)(onError);
3049
+ const [texture, setTexture] = (0, import_react14.useState)(null);
3050
+ const onErrorRef = (0, import_react14.useRef)(onError);
1397
3051
  onErrorRef.current = onError;
1398
- (0, import_react7.useEffect)(() => {
3052
+ (0, import_react14.useEffect)(() => {
1399
3053
  if (!url) {
1400
3054
  setTexture(null);
1401
3055
  return;
@@ -1432,7 +3086,7 @@ function usePreviewTexture(url, anisotropy, onError) {
1432
3086
  active = false;
1433
3087
  };
1434
3088
  }, [anisotropy, url]);
1435
- (0, import_react7.useEffect)(
3089
+ (0, import_react14.useEffect)(
1436
3090
  () => () => {
1437
3091
  texture?.dispose();
1438
3092
  },
@@ -1445,7 +3099,7 @@ function PreviewFace({
1445
3099
  texture,
1446
3100
  faceOrder
1447
3101
  }) {
1448
- const geometry = (0, import_react7.useMemo)(() => {
3102
+ const geometry = (0, import_react14.useMemo)(() => {
1449
3103
  const nextGeometry = new import_three7.PlaneGeometry(
1450
3104
  PREVIEW_RADIUS * 2,
1451
3105
  PREVIEW_RADIUS * 2
@@ -1463,21 +3117,21 @@ function PreviewFace({
1463
3117
  uv.needsUpdate = true;
1464
3118
  return nextGeometry;
1465
3119
  }, [face, faceOrder]);
1466
- (0, import_react7.useEffect)(
3120
+ (0, import_react14.useEffect)(
1467
3121
  () => () => {
1468
3122
  geometry.dispose();
1469
3123
  },
1470
3124
  [geometry]
1471
3125
  );
1472
3126
  const transform = getFaceTransform(face, PREVIEW_RADIUS);
1473
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3127
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1474
3128
  "mesh",
1475
3129
  {
1476
3130
  geometry,
1477
3131
  position: transform.position,
1478
3132
  renderOrder: -100,
1479
3133
  rotation: transform.rotation,
1480
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3134
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1481
3135
  "meshBasicMaterial",
1482
3136
  {
1483
3137
  depthTest: false,
@@ -1493,7 +3147,7 @@ function PreviewCube({
1493
3147
  texture,
1494
3148
  faceOrder
1495
3149
  }) {
1496
- return CUBE_FACES.map((face) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3150
+ return CUBE_FACES.map((face) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1497
3151
  PreviewFace,
1498
3152
  {
1499
3153
  face,
@@ -1504,14 +3158,14 @@ function PreviewCube({
1504
3158
  ));
1505
3159
  }
1506
3160
  function useManagedTexture(manager, key, url, address) {
1507
- const [managedState, setManagedState] = (0, import_react7.useState)(() => ({
3161
+ const [managedState, setManagedState] = (0, import_react14.useState)(() => ({
1508
3162
  manager,
1509
3163
  key,
1510
3164
  url,
1511
3165
  snapshot: manager.getSnapshot(key)
1512
3166
  }));
1513
3167
  const snapshot = managedState.manager === manager && managedState.key === key && managedState.url === url ? managedState.snapshot : manager.getSnapshot(key);
1514
- (0, import_react7.useEffect)(
3168
+ (0, import_react14.useEffect)(
1515
3169
  () => manager.acquire(key, url, address, (nextSnapshot) => {
1516
3170
  setManagedState({
1517
3171
  manager,
@@ -1524,7 +3178,7 @@ function useManagedTexture(manager, key, url, address) {
1524
3178
  );
1525
3179
  return snapshot;
1526
3180
  }
1527
- var TileMesh = (0, import_react7.memo)(function TileMesh2({
3181
+ var TileMesh = (0, import_react14.memo)(function TileMesh2({
1528
3182
  address,
1529
3183
  manager,
1530
3184
  renderOrder,
@@ -1538,14 +3192,14 @@ var TileMesh = (0, import_react7.memo)(function TileMesh2({
1538
3192
  if (!snapshot.texture) {
1539
3193
  return null;
1540
3194
  }
1541
- 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)(
1542
3196
  "mesh",
1543
3197
  {
1544
3198
  geometry: SHARED_TILE_GEOMETRY,
1545
3199
  position: [layout.x, layout.y, 0],
1546
3200
  renderOrder,
1547
3201
  scale: [layout.width, layout.height, 1],
1548
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3202
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1549
3203
  "meshBasicMaterial",
1550
3204
  {
1551
3205
  depthTest: false,
@@ -1557,12 +3211,12 @@ var TileMesh = (0, import_react7.memo)(function TileMesh2({
1557
3211
  }
1558
3212
  ) });
1559
3213
  });
1560
- var TileLayerMeshes = (0, import_react7.memo)(function TileLayerMeshes2({
3214
+ var TileLayerMeshes = (0, import_react14.memo)(function TileLayerMeshes2({
1561
3215
  layer,
1562
3216
  manager,
1563
3217
  resolveUrl
1564
3218
  }) {
1565
- return layer.addresses.map((address) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3219
+ return layer.addresses.map((address) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1566
3220
  TileMesh,
1567
3221
  {
1568
3222
  address,
@@ -1730,11 +3384,11 @@ function Tile({
1730
3384
  onTileError,
1731
3385
  onLevelChange
1732
3386
  }) {
1733
- const { camera, gl, size } = (0, import_fiber6.useThree)();
1734
- 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]);
1735
3389
  const { levels, tileSize } = multiresConfig;
1736
3390
  const multiresToken = `${tileSize}:${levels.join(",")}`;
1737
- const candidatesByLevel = (0, import_react7.useMemo)(
3391
+ const candidatesByLevel = (0, import_react14.useMemo)(
1738
3392
  () => buildTileCandidates(levels, tileSize),
1739
3393
  [levels, tileSize]
1740
3394
  );
@@ -1746,13 +3400,13 @@ function Tile({
1746
3400
  onPreviewError
1747
3401
  );
1748
3402
  const sharedManager = useSharedTileTextureManager();
1749
- const progressRef = (0, import_react7.useRef)(onLoadProgress);
1750
- const errorRef = (0, import_react7.useRef)(onTileError);
1751
- 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);
1752
3406
  progressRef.current = onLoadProgress;
1753
3407
  errorRef.current = onTileError;
1754
3408
  levelChangeRef.current = onLevelChange;
1755
- const ownedManager = (0, import_react7.useMemo)(
3409
+ const ownedManager = (0, import_react14.useMemo)(
1756
3410
  () => new TileTextureManager({
1757
3411
  anisotropy,
1758
3412
  maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
@@ -1773,7 +3427,7 @@ function Tile({
1773
3427
  ]
1774
3428
  );
1775
3429
  const manager = sharedManager ?? ownedManager;
1776
- (0, import_react7.useEffect)(() => {
3430
+ (0, import_react14.useEffect)(() => {
1777
3431
  manager.resume();
1778
3432
  return () => {
1779
3433
  if (manager === ownedManager) {
@@ -1783,11 +3437,11 @@ function Tile({
1783
3437
  queueMicrotask(() => manager.releaseUnused());
1784
3438
  };
1785
3439
  }, [manager, ownedManager]);
1786
- const readyRef = (0, import_react7.useRef)(false);
1787
- (0, import_react7.useEffect)(() => {
3440
+ const readyRef = (0, import_react14.useRef)(false);
3441
+ (0, import_react14.useEffect)(() => {
1788
3442
  readyRef.current = false;
1789
3443
  }, [effectivePreviewUrl]);
1790
- (0, import_react7.useEffect)(() => {
3444
+ (0, import_react14.useEffect)(() => {
1791
3445
  if (!previewTexture || readyRef.current) {
1792
3446
  return;
1793
3447
  }
@@ -1795,31 +3449,31 @@ function Tile({
1795
3449
  onReady?.();
1796
3450
  }, [onReady, previewTexture]);
1797
3451
  const template = urlTemplate ?? buildDefaultTileUrlTemplate();
1798
- const resolveUrl = (0, import_react7.useMemo)(
3452
+ const resolveUrl = (0, import_react14.useMemo)(
1799
3453
  () => (address) => {
1800
3454
  const relativePath = resolveTileUrl ? resolveTileUrl(address) : resolveTemplateUrl(template, address);
1801
3455
  return resolveRelativeTileUrl(baseUrl, relativePath);
1802
3456
  },
1803
3457
  [baseUrl, resolveTileUrl, template]
1804
3458
  );
1805
- const [layers, setLayers] = (0, import_react7.useState)([]);
1806
- const layerTokenRef = (0, import_react7.useRef)("");
1807
- const currentLevelRef = (0, import_react7.useRef)(1);
1808
- const lastUpdateAtRef = (0, import_react7.useRef)(-Infinity);
1809
- const visibilityDirtyRef = (0, import_react7.useRef)(true);
1810
- const lastVisibilityQuaternionRef = (0, import_react7.useRef)(camera.quaternion.clone());
1811
- const lastVisibilityFovRef = (0, import_react7.useRef)(Number.NaN);
1812
- const lastViewportRef = (0, import_react7.useRef)({ width: 0, height: 0, dpr: 0 });
1813
- const frustumRef = (0, import_react7.useRef)(new import_three7.Frustum());
1814
- const projectionMatrixRef = (0, import_react7.useRef)(new import_three7.Matrix4());
1815
- const visibilityCameraRef = (0, import_react7.useRef)(new import_three7.PerspectiveCamera());
1816
- (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)(() => {
1817
3471
  currentLevelRef.current = 1;
1818
3472
  layerTokenRef.current = "";
1819
3473
  visibilityDirtyRef.current = true;
1820
3474
  setLayers([]);
1821
3475
  }, [manager, multiresToken]);
1822
- (0, import_react7.useEffect)(() => {
3476
+ (0, import_react14.useEffect)(() => {
1823
3477
  if (loadMode !== "preview") {
1824
3478
  return;
1825
3479
  }
@@ -1833,7 +3487,7 @@ function Tile({
1833
3487
  setLayers(nextLayers);
1834
3488
  }
1835
3489
  };
1836
- (0, import_fiber6.useFrame)(() => {
3490
+ (0, import_fiber11.useFrame)(() => {
1837
3491
  if (loadMode === "preview") {
1838
3492
  return;
1839
3493
  }
@@ -1899,15 +3553,15 @@ function Tile({
1899
3553
  };
1900
3554
  commitLayers(nextLayers);
1901
3555
  });
1902
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("group", { visible, children: [
1903
- 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)(
1904
3558
  PreviewCube,
1905
3559
  {
1906
3560
  texture: previewTexture,
1907
3561
  faceOrder: previewFaceOrder
1908
3562
  }
1909
3563
  ) : null,
1910
- layers.map((layer) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3564
+ layers.map((layer) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1911
3565
  TileLayerMeshes,
1912
3566
  {
1913
3567
  layer,
@@ -1920,7 +3574,7 @@ function Tile({
1920
3574
  }
1921
3575
 
1922
3576
  // src/panorama-scenes.tsx
1923
- var import_jsx_runtime7 = require("react/jsx-runtime");
3577
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1924
3578
  var TRANSITION_DEFAULTS = {
1925
3579
  none: { duration: 0, krpanoBlend: "NOBLEND" },
1926
3580
  crossfade: { duration: 1, krpanoBlend: "BLEND(1.0, easeInCubic)" },
@@ -1970,7 +3624,7 @@ function SceneSource({
1970
3624
  return null;
1971
3625
  }
1972
3626
  if (scene.type === "sphere") {
1973
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3627
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1974
3628
  Sphere,
1975
3629
  {
1976
3630
  src: scene.src,
@@ -1982,7 +3636,7 @@ function SceneSource({
1982
3636
  );
1983
3637
  }
1984
3638
  const { id: _id, type: _type, ...tileProps } = scene;
1985
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3639
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1986
3640
  Tile,
1987
3641
  {
1988
3642
  ...tileProps,
@@ -1998,17 +3652,17 @@ function SnapshotCapture({
1998
3652
  maxPixels,
1999
3653
  onCaptured
2000
3654
  }) {
2001
- const { camera, gl, scene, size } = (0, import_fiber7.useThree)();
2002
- const capturedRequestRef = (0, import_react8.useRef)(0);
2003
- const renderTargetRef = (0, import_react8.useRef)(null);
2004
- (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)(
2005
3659
  () => () => {
2006
3660
  renderTargetRef.current?.dispose();
2007
3661
  renderTargetRef.current = null;
2008
3662
  },
2009
3663
  []
2010
3664
  );
2011
- (0, import_fiber7.useFrame)(() => {
3665
+ (0, import_fiber12.useFrame)(() => {
2012
3666
  if (requestId === 0 || requestId === capturedRequestRef.current) {
2013
3667
  return;
2014
3668
  }
@@ -2054,12 +3708,12 @@ function SnapshotOverlay({
2054
3708
  runId,
2055
3709
  onFinish
2056
3710
  }) {
2057
- const { camera, size } = (0, import_fiber7.useThree)();
2058
- const meshRef = (0, import_react8.useRef)(null);
2059
- const startRef = (0, import_react8.useRef)(null);
2060
- const finishedRef = (0, import_react8.useRef)(false);
2061
- const directionRef = (0, import_react8.useRef)(new import_three8.Vector3());
2062
- 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)(
2063
3717
  () => new import_three8.ShaderMaterial({
2064
3718
  transparent: true,
2065
3719
  depthTest: false,
@@ -2138,13 +3792,13 @@ function SnapshotOverlay({
2138
3792
  }),
2139
3793
  [snapshot.texture, transition.preset]
2140
3794
  );
2141
- (0, import_react8.useEffect)(() => () => material.dispose(), [material]);
2142
- (0, import_react8.useEffect)(() => {
3795
+ (0, import_react15.useEffect)(() => () => material.dispose(), [material]);
3796
+ (0, import_react15.useEffect)(() => {
2143
3797
  startRef.current = null;
2144
3798
  finishedRef.current = false;
2145
3799
  material.uniforms.progress.value = 0;
2146
3800
  }, [material, runId]);
2147
- (0, import_fiber7.useFrame)((state) => {
3801
+ (0, import_fiber12.useFrame)((state) => {
2148
3802
  const mesh = meshRef.current;
2149
3803
  if (!mesh || !camera.isPerspectiveCamera) {
2150
3804
  return;
@@ -2169,9 +3823,9 @@ function SnapshotOverlay({
2169
3823
  onFinish();
2170
3824
  }
2171
3825
  });
2172
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
2173
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
2174
- /* @__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 })
2175
3829
  ] });
2176
3830
  }
2177
3831
  var FULLSCREEN_PLANE = new import_three8.PlaneGeometry(1, 1);
@@ -2184,47 +3838,47 @@ function PanoramaScenesController({
2184
3838
  onTransitionError,
2185
3839
  snapshotMaxPixels = 3686400
2186
3840
  }) {
2187
- const controlsRef = (0, import_react8.useContext)(PanoramaControlsContext);
3841
+ const controlsRef = (0, import_react15.useContext)(PanoramaViewContext);
2188
3842
  const manager = useSharedTileTextureManager();
2189
- const transition = (0, import_react8.useMemo)(
3843
+ const transition = (0, import_react15.useMemo)(
2190
3844
  () => resolveTransition(transitionInput),
2191
3845
  [transitionInput]
2192
3846
  );
2193
- const sceneMap = (0, import_react8.useMemo)(
3847
+ const sceneMap = (0, import_react15.useMemo)(
2194
3848
  () => new Map(scenes.map((scene) => [scene.id, scene])),
2195
3849
  [scenes]
2196
3850
  );
2197
3851
  const initialScene = sceneMap.get(activeSceneId) ?? null;
2198
- const [slots, setSlots] = (0, import_react8.useState)([
3852
+ const [slots, setSlots] = (0, import_react15.useState)([
2199
3853
  initialScene,
2200
3854
  null
2201
3855
  ]);
2202
- const [activeSlot, setActiveSlot] = (0, import_react8.useState)(0);
2203
- const [liveSlot, setLiveSlot] = (0, import_react8.useState)(0);
2204
- const [pendingSlot, setPendingSlot] = (0, import_react8.useState)(null);
2205
- const [phase, setPhase] = (0, import_react8.useState)(initialScene ? "idle" : "preloading");
2206
- const [captureRequestId, setCaptureRequestId] = (0, import_react8.useState)(0);
2207
- const [transitionRunId, setTransitionRunId] = (0, import_react8.useState)(0);
2208
- const [snapshot, setSnapshot] = (0, import_react8.useState)(null);
2209
- const [recoveryKey, setRecoveryKey] = (0, import_react8.useState)(0);
2210
- const requestedSceneIdRef = (0, import_react8.useRef)(activeSceneId);
2211
- const captureReasonRef = (0, import_react8.useRef)("start");
2212
- const interactionReleaseRef = (0, import_react8.useRef)(null);
2213
- const slotsRef = (0, import_react8.useRef)(slots);
2214
- const phaseRef = (0, import_react8.useRef)(phase);
2215
- const liveSlotRef = (0, import_react8.useRef)(liveSlot);
2216
- 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);
2217
3871
  slotsRef.current = slots;
2218
3872
  phaseRef.current = phase;
2219
3873
  liveSlotRef.current = liveSlot;
2220
- const releaseInteraction = (0, import_react8.useCallback)(() => {
3874
+ const releaseInteraction = (0, import_react15.useCallback)(() => {
2221
3875
  interactionReleaseRef.current?.();
2222
3876
  interactionReleaseRef.current = null;
2223
3877
  }, []);
2224
- const reportError = (0, import_react8.useCallback)((sceneId, error) => {
3878
+ const reportError = (0, import_react15.useCallback)((sceneId, error) => {
2225
3879
  onTransitionError?.({ sceneId, error });
2226
3880
  }, [onTransitionError]);
2227
- const startPreload = (0, import_react8.useCallback)((scene) => {
3881
+ const startPreload = (0, import_react15.useCallback)((scene) => {
2228
3882
  const nextSlot = 1 - liveSlotRef.current;
2229
3883
  requestedSceneIdRef.current = scene.id;
2230
3884
  setSlots((current) => {
@@ -2235,7 +3889,7 @@ function PanoramaScenesController({
2235
3889
  setPendingSlot(nextSlot);
2236
3890
  setPhase("preloading");
2237
3891
  }, []);
2238
- (0, import_react8.useEffect)(() => {
3892
+ (0, import_react15.useEffect)(() => {
2239
3893
  const requested = sceneMap.get(activeSceneId);
2240
3894
  if (!requested) {
2241
3895
  reportError(activeSceneId, new Error(`Unknown panorama scene: ${activeSceneId}`));
@@ -2259,7 +3913,7 @@ function PanoramaScenesController({
2259
3913
  }
2260
3914
  startPreload(requested);
2261
3915
  }, [activeSceneId, pendingSlot, reportError, sceneMap, startPreload]);
2262
- const handleReady = (0, import_react8.useCallback)((slot) => {
3916
+ const handleReady = (0, import_react15.useCallback)((slot) => {
2263
3917
  if (slot !== pendingSlot || phaseRef.current !== "preloading") {
2264
3918
  return;
2265
3919
  }
@@ -2290,7 +3944,7 @@ function PanoramaScenesController({
2290
3944
  setPhase("capturing");
2291
3945
  setCaptureRequestId((current) => current + 1);
2292
3946
  }, [controlsRef, onTransitionEnd, pendingSlot, transition.preset]);
2293
- const handleCaptured = (0, import_react8.useCallback)((nextSnapshot) => {
3947
+ const handleCaptured = (0, import_react15.useCallback)((nextSnapshot) => {
2294
3948
  setSnapshot((current) => {
2295
3949
  current?.dispose();
2296
3950
  return nextSnapshot;
@@ -2312,7 +3966,7 @@ function PanoramaScenesController({
2312
3966
  setPhase("transitioning");
2313
3967
  setTransitionRunId((current) => current + 1);
2314
3968
  }, [manager, pendingSlot, sceneMap, startPreload]);
2315
- const finishTransition = (0, import_react8.useCallback)(() => {
3969
+ const finishTransition = (0, import_react15.useCallback)(() => {
2316
3970
  const nextSlot = pendingSlot;
2317
3971
  const previousScene = transitionFromRef.current;
2318
3972
  const nextScene = nextSlot === null ? null : slotsRef.current[nextSlot];
@@ -2337,12 +3991,12 @@ function PanoramaScenesController({
2337
3991
  });
2338
3992
  }
2339
3993
  }, [onTransitionEnd, pendingSlot, releaseInteraction, snapshot, transition.preset]);
2340
- (0, import_react8.useEffect)(() => () => {
3994
+ (0, import_react15.useEffect)(() => () => {
2341
3995
  releaseInteraction();
2342
3996
  snapshot?.dispose();
2343
3997
  }, [releaseInteraction, snapshot]);
2344
- const { gl } = (0, import_fiber7.useThree)();
2345
- (0, import_react8.useEffect)(() => {
3998
+ const { gl } = (0, import_fiber12.useThree)();
3999
+ (0, import_react15.useEffect)(() => {
2346
4000
  const canvas = gl.domElement;
2347
4001
  const onContextLost = (event) => {
2348
4002
  event.preventDefault();
@@ -2366,8 +4020,8 @@ function PanoramaScenesController({
2366
4020
  }, [gl, manager, releaseInteraction, snapshot]);
2367
4021
  const renderedScene = slots[liveSlot];
2368
4022
  const snapshotVisible = snapshot !== null && (phase === "transitioning" || phase === "recapturing" || phase === "preloading");
2369
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react8.Fragment, { children: [
2370
- /* @__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)(
2371
4025
  SnapshotCapture,
2372
4026
  {
2373
4027
  requestId: captureRequestId,
@@ -2379,7 +4033,7 @@ function PanoramaScenesController({
2379
4033
  const source = slots[slot];
2380
4034
  const isLive = slot === liveSlot;
2381
4035
  const isPending = slot === pendingSlot;
2382
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4036
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2383
4037
  SceneSource,
2384
4038
  {
2385
4039
  scene: source,
@@ -2395,7 +4049,7 @@ function PanoramaScenesController({
2395
4049
  `${slot}:${source?.id ?? "empty"}:${recoveryKey}`
2396
4050
  );
2397
4051
  }),
2398
- snapshotVisible ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4052
+ snapshotVisible ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2399
4053
  SnapshotOverlay,
2400
4054
  {
2401
4055
  snapshot,
@@ -2413,26 +4067,26 @@ function PanoramaScenes({
2413
4067
  maxConcurrentTileLoads,
2414
4068
  ...props
2415
4069
  }) {
2416
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2417
4071
  TileTextureManagerProvider,
2418
4072
  {
2419
4073
  maxTextureMemoryMb,
2420
4074
  maxConcurrentLoads: maxConcurrentTileLoads,
2421
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PanoramaScenesController, { ...props })
4075
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PanoramaScenesController, { ...props })
2422
4076
  }
2423
4077
  );
2424
4078
  }
2425
4079
 
2426
4080
  // src/hotspot/image-hotspot.tsx
2427
- var import_fiber9 = require("@react-three/fiber");
2428
- var import_react10 = require("react");
4081
+ var import_fiber14 = require("@react-three/fiber");
4082
+ var import_react17 = require("react");
2429
4083
  var import_three10 = require("three");
2430
4084
 
2431
4085
  // src/hotspot/hotspot-anchor.tsx
2432
- var import_fiber8 = require("@react-three/fiber");
2433
- var import_react9 = require("react");
4086
+ var import_fiber13 = require("@react-three/fiber");
4087
+ var import_react16 = require("react");
2434
4088
  var import_three9 = require("three");
2435
- var import_jsx_runtime8 = require("react/jsx-runtime");
4089
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2436
4090
  var DEFAULT_FLOATING_DISTANCE = 10;
2437
4091
  var PANORAMA_RADIUS = DEFAULT_PANORAMA_RADIUS;
2438
4092
  var PANORAMA_SURFACE_INSET = 4;
@@ -2449,6 +4103,9 @@ function angularSizeToWorldSize(size, radius) {
2449
4103
  const safeSize = Number.isFinite(size) ? import_three9.MathUtils.clamp(Math.abs(size), MIN_ANGULAR_SIZE, MAX_ANGULAR_SIZE) : MIN_ANGULAR_SIZE;
2450
4104
  return 2 * radius * Math.tan(import_three9.MathUtils.degToRad(safeSize) / 2);
2451
4105
  }
4106
+ function resolveScale(scale) {
4107
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
4108
+ }
2452
4109
  function resolveDistance(placement, distance, width, height) {
2453
4110
  const maximumDistance = maximumHotspotDistance(width, height);
2454
4111
  if (placement === "surface") {
@@ -2513,6 +4170,7 @@ function HotspotAnchor({
2513
4170
  position,
2514
4171
  width,
2515
4172
  height,
4173
+ scale = 1,
2516
4174
  useAngularScale = true,
2517
4175
  focusContent,
2518
4176
  mode = "billboard",
@@ -2534,25 +4192,33 @@ function HotspotAnchor({
2534
4192
  onPositionChange,
2535
4193
  onDragEnd
2536
4194
  }) {
2537
- const controlsRef = (0, import_react9.useContext)(PanoramaControlsContext);
2538
- const groupRef = (0, import_react9.useRef)(null);
2539
- const dragStateRef = (0, import_react9.useRef)(null);
2540
- 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);
2541
4199
  const normalizedPosition = normalizePanoPosition(position);
2542
4200
  const rendering = internalOrientation && internalPlacement ? { orientation: internalOrientation, placement: internalPlacement } : HOTSPOT_MODE_RENDERING[mode];
2543
4201
  const { orientation: orientation2, placement } = rendering;
2544
- const resolvedDistance = resolveDistance(placement, distance, width, height);
2545
- const worldPosition = (0, import_react9.useMemo)(
4202
+ const resolvedScale = resolveScale(scale);
4203
+ const scaledWidth = width * resolvedScale;
4204
+ const scaledHeight = height * resolvedScale;
4205
+ const resolvedDistance = resolveDistance(
4206
+ placement,
4207
+ distance,
4208
+ scaledWidth,
4209
+ scaledHeight
4210
+ );
4211
+ const worldPosition = (0, import_react16.useMemo)(
2546
4212
  () => panoPositionToVector3(normalizedPosition, resolvedDistance),
2547
4213
  [normalizedPosition.pitch, normalizedPosition.yaw, resolvedDistance]
2548
4214
  );
2549
- const surfaceQuaternion3 = (0, import_react9.useMemo)(
4215
+ const surfaceQuaternion3 = (0, import_react16.useMemo)(
2550
4216
  () => createSurfaceQuaternion(normalizedPosition),
2551
4217
  [normalizedPosition.pitch, normalizedPosition.yaw]
2552
4218
  );
2553
- const worldWidth = angularSizeToWorldSize(width, resolvedDistance);
2554
- const worldHeight = angularSizeToWorldSize(height, resolvedDistance);
2555
- const focusGeometry = (0, import_react9.useMemo)(() => {
4219
+ const worldWidth = angularSizeToWorldSize(scaledWidth, resolvedDistance);
4220
+ const worldHeight = angularSizeToWorldSize(scaledHeight, resolvedDistance);
4221
+ const focusGeometry = (0, import_react16.useMemo)(() => {
2556
4222
  const planeGeometry = new import_three9.PlaneGeometry(1, 1);
2557
4223
  const geometry = new import_three9.EdgesGeometry(planeGeometry);
2558
4224
  planeGeometry.dispose();
@@ -2570,15 +4236,15 @@ function HotspotAnchor({
2570
4236
  });
2571
4237
  } : void 0
2572
4238
  });
2573
- (0, import_react9.useEffect)(
4239
+ (0, import_react16.useEffect)(
2574
4240
  () => () => {
2575
4241
  dragStateRef.current?.releaseInteractionLock();
2576
4242
  dragStateRef.current = null;
2577
4243
  },
2578
4244
  []
2579
4245
  );
2580
- (0, import_react9.useEffect)(() => () => focusGeometry.dispose(), [focusGeometry]);
2581
- (0, import_fiber8.useFrame)(({ camera }) => {
4246
+ (0, import_react16.useEffect)(() => () => focusGeometry.dispose(), [focusGeometry]);
4247
+ (0, import_fiber13.useFrame)(({ camera }) => {
2582
4248
  if (!groupRef.current) {
2583
4249
  return;
2584
4250
  }
@@ -2589,7 +4255,7 @@ function HotspotAnchor({
2589
4255
  }
2590
4256
  groupRef.current.rotateOnAxis(
2591
4257
  LOCAL_FORWARD,
2592
- import_three9.MathUtils.degToRad(rotation)
4258
+ import_three9.MathUtils.degToRad(-rotation)
2593
4259
  );
2594
4260
  const scaleFactor = scaleMode === "fixed" && camera instanceof import_three9.PerspectiveCamera ? fixedScaleFactor(camera.fov, referenceFov) : 1;
2595
4261
  groupRef.current.scale.set(
@@ -2624,7 +4290,7 @@ function HotspotAnchor({
2624
4290
  dragState.releaseInteractionLock();
2625
4291
  dragStateRef.current = null;
2626
4292
  };
2627
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4293
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2628
4294
  "group",
2629
4295
  {
2630
4296
  ref: groupRef,
@@ -2694,14 +4360,14 @@ function HotspotAnchor({
2694
4360
  } : void 0,
2695
4361
  onPointerUp: interactive ? (event) => releasePointer(event, true) : void 0,
2696
4362
  children: [
2697
- focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4363
+ focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2698
4364
  "lineSegments",
2699
4365
  {
2700
4366
  geometry: focusGeometry,
2701
4367
  position: [0, 0, 0.01],
2702
4368
  renderOrder: renderOrder + 1,
2703
4369
  scale: [1.12, 1.12, 1],
2704
- 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 })
2705
4371
  }
2706
4372
  ) : null,
2707
4373
  children
@@ -2711,7 +4377,7 @@ function HotspotAnchor({
2711
4377
  }
2712
4378
 
2713
4379
  // src/hotspot/image-hotspot.tsx
2714
- var import_jsx_runtime9 = require("react/jsx-runtime");
4380
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2715
4381
  function clampOpacity(opacity) {
2716
4382
  if (!Number.isFinite(opacity)) {
2717
4383
  return 1;
@@ -2719,13 +4385,13 @@ function clampOpacity(opacity) {
2719
4385
  return Math.max(0, Math.min(opacity, 1));
2720
4386
  }
2721
4387
  function useHotspotImageTexture(src, onLoad, onError) {
2722
- const gl = (0, import_fiber9.useThree)((state) => state.gl);
2723
- const [texture, setTexture] = (0, import_react10.useState)(null);
2724
- const onLoadRef = (0, import_react10.useRef)(onLoad);
2725
- 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);
2726
4392
  onLoadRef.current = onLoad;
2727
4393
  onErrorRef.current = onError;
2728
- (0, import_react10.useEffect)(() => {
4394
+ (0, import_react17.useEffect)(() => {
2729
4395
  let active = true;
2730
4396
  setTexture(null);
2731
4397
  if (!src) {
@@ -2763,7 +4429,7 @@ function useHotspotImageTexture(src, onLoad, onError) {
2763
4429
  active = false;
2764
4430
  };
2765
4431
  }, [gl, src]);
2766
- (0, import_react10.useEffect)(
4432
+ (0, import_react17.useEffect)(
2767
4433
  () => () => {
2768
4434
  texture?.dispose();
2769
4435
  },
@@ -2781,9 +4447,9 @@ function ImageHotspot({
2781
4447
  ...anchorProps
2782
4448
  }) {
2783
4449
  const texture = useHotspotImageTexture(src, onLoad, onError);
2784
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("mesh", { children: [
2785
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("planeGeometry", { args: [1, 1] }),
2786
- /* @__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)(
2787
4453
  "meshBasicMaterial",
2788
4454
  {
2789
4455
  map: texture,
@@ -2797,13 +4463,13 @@ function ImageHotspot({
2797
4463
  }
2798
4464
 
2799
4465
  // src/hotspot/polygon-hotspot.tsx
2800
- var import_fiber10 = require("@react-three/fiber");
2801
- var import_react11 = require("react");
4466
+ var import_fiber15 = require("@react-three/fiber");
4467
+ var import_react18 = require("react");
2802
4468
  var import_three11 = require("three");
2803
4469
  var import_Line2 = require("three/addons/lines/Line2.js");
2804
4470
  var import_LineGeometry = require("three/addons/lines/LineGeometry.js");
2805
4471
  var import_LineMaterial = require("three/addons/lines/LineMaterial.js");
2806
- var import_jsx_runtime10 = require("react/jsx-runtime");
4472
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2807
4473
  var POLYGON_RADIUS = 990;
2808
4474
  var MAX_FILL_SEGMENT_DEGREES = 7;
2809
4475
  function cloneVertices(vertices) {
@@ -3076,38 +4742,38 @@ function PolygonHotspot({
3076
4742
  onInvalid,
3077
4743
  ...anchorProps
3078
4744
  }) {
3079
- const gl = (0, import_fiber10.useThree)((state) => state.gl);
3080
- const size = (0, import_fiber10.useThree)((state) => state.size);
3081
- 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]);
3082
4748
  const valid = issues.length === 0;
3083
- const center = (0, import_react11.useMemo)(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
3084
- 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)(
3085
4751
  () => valid && center ? makeFillGeometry(vertices, center) : null,
3086
4752
  [center, valid, vertices]
3087
4753
  );
3088
- const strokePositions = (0, import_react11.useMemo)(
4754
+ const strokePositions = (0, import_react18.useMemo)(
3089
4755
  () => valid && center ? makeStrokePositions(vertices, center) : null,
3090
4756
  [center, valid, vertices]
3091
4757
  );
3092
4758
  const resolvedStrokeOpacity = Math.max(0, Math.min(strokeOpacity, 1));
3093
4759
  const devicePixelRatio = gl.getPixelRatio();
3094
- const strokeLine = (0, import_react11.useMemo)(
4760
+ const strokeLine = (0, import_react18.useMemo)(
3095
4761
  () => strokePositions ? makeStrokeLine(strokePositions, stroke, strokeWidth * devicePixelRatio, resolvedStrokeOpacity) : null,
3096
4762
  [devicePixelRatio, resolvedStrokeOpacity, stroke, strokePositions, strokeWidth]
3097
4763
  );
3098
- const focusStrokeLine = (0, import_react11.useMemo)(
4764
+ const focusStrokeLine = (0, import_react18.useMemo)(
3099
4765
  () => strokePositions ? makeStrokeLine(strokePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3100
4766
  [devicePixelRatio, strokePositions, strokeWidth]
3101
4767
  );
3102
- const startVerticesRef = (0, import_react11.useRef)(null);
4768
+ const startVerticesRef = (0, import_react18.useRef)(null);
3103
4769
  const issuesKey = issues.map((issue) => issue.code).join(",");
3104
- (0, import_react11.useEffect)(() => {
4770
+ (0, import_react18.useEffect)(() => {
3105
4771
  if (!valid) onInvalid?.(issues);
3106
4772
  }, [issues, issuesKey, onInvalid, valid]);
3107
- (0, import_react11.useEffect)(() => () => fillGeometry?.dispose(), [fillGeometry]);
3108
- (0, import_react11.useEffect)(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
3109
- (0, import_react11.useEffect)(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
3110
- (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)(() => {
3111
4777
  const width = gl.domElement.width;
3112
4778
  const height = gl.domElement.height;
3113
4779
  for (const line of [strokeLine, focusStrokeLine]) {
@@ -3126,7 +4792,7 @@ function PolygonHotspot({
3126
4792
  startPosition: event.startPosition
3127
4793
  });
3128
4794
  };
3129
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
4795
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3130
4796
  HotspotAnchor,
3131
4797
  {
3132
4798
  ...anchorProps,
@@ -3141,7 +4807,7 @@ function PolygonHotspot({
3141
4807
  draggable,
3142
4808
  interactive,
3143
4809
  useAngularScale: false,
3144
- 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 }),
3145
4811
  onDragStart: (event) => {
3146
4812
  startVerticesRef.current = cloneVertices(vertices);
3147
4813
  emitVerticesChange(event, onDragStart);
@@ -3152,7 +4818,7 @@ function PolygonHotspot({
3152
4818
  startVerticesRef.current = null;
3153
4819
  },
3154
4820
  children: [
3155
- /* @__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)(
3156
4822
  "meshBasicMaterial",
3157
4823
  {
3158
4824
  color: fill,
@@ -3162,20 +4828,20 @@ function PolygonHotspot({
3162
4828
  transparent: true
3163
4829
  }
3164
4830
  ) }),
3165
- /* @__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 })
3166
4832
  ]
3167
4833
  }
3168
4834
  );
3169
4835
  }
3170
4836
 
3171
4837
  // src/hotspot/polyline-hotspot.tsx
3172
- var import_fiber11 = require("@react-three/fiber");
3173
- var import_react12 = require("react");
4838
+ var import_fiber16 = require("@react-three/fiber");
4839
+ var import_react19 = require("react");
3174
4840
  var import_three12 = require("three");
3175
4841
  var import_Line22 = require("three/addons/lines/Line2.js");
3176
4842
  var import_LineGeometry2 = require("three/addons/lines/LineGeometry.js");
3177
4843
  var import_LineMaterial2 = require("three/addons/lines/LineMaterial.js");
3178
- var import_jsx_runtime11 = require("react/jsx-runtime");
4844
+ var import_jsx_runtime12 = require("react/jsx-runtime");
3179
4845
  var POLYLINE_RADIUS = 990;
3180
4846
  var MAX_SEGMENT_DEGREES = 7;
3181
4847
  function cloneVertices2(vertices) {
@@ -3312,32 +4978,32 @@ function PolylineHotspot({
3312
4978
  onInvalid,
3313
4979
  ...anchorProps
3314
4980
  }) {
3315
- const gl = (0, import_fiber11.useThree)((state) => state.gl);
3316
- const size = (0, import_fiber11.useThree)((state) => state.size);
3317
- 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]);
3318
4984
  const valid = issues.length === 0;
3319
- const center = (0, import_react12.useMemo)(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
3320
- 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)(
3321
4987
  () => valid && center ? makeLinePositions(vertices, center) : null,
3322
4988
  [center, valid, vertices]
3323
4989
  );
3324
4990
  const devicePixelRatio = gl.getPixelRatio();
3325
4991
  const opacity = Math.max(0, Math.min(strokeOpacity, 1));
3326
- const line = (0, import_react12.useMemo)(
4992
+ const line = (0, import_react19.useMemo)(
3327
4993
  () => linePositions ? makeLine(linePositions, stroke, strokeWidth * devicePixelRatio, opacity) : null,
3328
4994
  [devicePixelRatio, linePositions, opacity, stroke, strokeWidth]
3329
4995
  );
3330
- const focusLine = (0, import_react12.useMemo)(
4996
+ const focusLine = (0, import_react19.useMemo)(
3331
4997
  () => linePositions ? makeLine(linePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3332
4998
  [devicePixelRatio, linePositions, strokeWidth]
3333
4999
  );
3334
- const startVerticesRef = (0, import_react12.useRef)(null);
3335
- (0, import_react12.useEffect)(() => {
5000
+ const startVerticesRef = (0, import_react19.useRef)(null);
5001
+ (0, import_react19.useEffect)(() => {
3336
5002
  if (!valid) onInvalid?.(issues);
3337
5003
  }, [issues, onInvalid, valid]);
3338
- (0, import_react12.useEffect)(() => () => disposeLine(line), [line]);
3339
- (0, import_react12.useEffect)(() => () => disposeLine(focusLine), [focusLine]);
3340
- (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)(() => {
3341
5007
  const width = gl.domElement.width;
3342
5008
  const height = gl.domElement.height;
3343
5009
  for (const nextLine of [line, focusLine]) {
@@ -3355,7 +5021,7 @@ function PolylineHotspot({
3355
5021
  startPosition: event.startPosition
3356
5022
  });
3357
5023
  };
3358
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5024
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3359
5025
  HotspotAnchor,
3360
5026
  {
3361
5027
  ...anchorProps,
@@ -3369,7 +5035,7 @@ function PolylineHotspot({
3369
5035
  renderOrder,
3370
5036
  draggable,
3371
5037
  useAngularScale: false,
3372
- 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 }),
3373
5039
  onDragStart: (event) => {
3374
5040
  startVerticesRef.current = cloneVertices2(vertices);
3375
5041
  emitVerticesChange(event, onDragStart);
@@ -3379,15 +5045,15 @@ function PolylineHotspot({
3379
5045
  emitVerticesChange(event, onDragEnd);
3380
5046
  startVerticesRef.current = null;
3381
5047
  },
3382
- 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 })
3383
5049
  }
3384
5050
  );
3385
5051
  }
3386
5052
 
3387
5053
  // src/hotspot/graphic-hotspot.tsx
3388
- var import_react13 = require("react");
5054
+ var import_react20 = require("react");
3389
5055
  var import_three13 = require("three");
3390
- var import_jsx_runtime12 = require("react/jsx-runtime");
5056
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3391
5057
  var GRAPHIC_TEXTURE_SIZE = 512;
3392
5058
  function clampOpacity2(opacity) {
3393
5059
  if (!Number.isFinite(opacity)) {
@@ -3398,14 +5064,34 @@ function clampOpacity2(opacity) {
3398
5064
  function resolveStrokeWidth(value) {
3399
5065
  return Number.isFinite(value) ? Math.max(value, 0) : 8;
3400
5066
  }
3401
- function roundedRectanglePath(context, x, y, width, height, radius) {
3402
- 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) {
3403
5090
  context.beginPath();
3404
- context.moveTo(x + safeRadius, y);
3405
- context.arcTo(x + width, y, x + width, y + height, safeRadius);
3406
- context.arcTo(x + width, y + height, x, y + height, safeRadius);
3407
- context.arcTo(x, y + height, x, y, safeRadius);
3408
- 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
+ }
3409
5095
  context.closePath();
3410
5096
  }
3411
5097
  function paintPath(context, graphic, path) {
@@ -3419,7 +5105,7 @@ function paintPath(context, graphic, path) {
3419
5105
  path ? context.stroke(path) : context.stroke();
3420
5106
  }
3421
5107
  }
3422
- function drawGraphic(context, graphic) {
5108
+ function drawGraphic(context, graphic, aspectWidth, aspectHeight) {
3423
5109
  const padding = Math.max(resolveStrokeWidth(graphic.strokeWidth) / 2 + 2, 4);
3424
5110
  const drawableSize = GRAPHIC_TEXTURE_SIZE - padding * 2;
3425
5111
  context.clearRect(0, 0, GRAPHIC_TEXTURE_SIZE, GRAPHIC_TEXTURE_SIZE);
@@ -3437,6 +5123,63 @@ function drawGraphic(context, graphic) {
3437
5123
  paintPath(context, graphic);
3438
5124
  return;
3439
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
+ }
3440
5183
  if (graphic.kind === "rectangle") {
3441
5184
  roundedRectanglePath(
3442
5185
  context,
@@ -3444,7 +5187,9 @@ function drawGraphic(context, graphic) {
3444
5187
  padding,
3445
5188
  drawableSize,
3446
5189
  drawableSize,
3447
- graphic.cornerRadius ?? 0
5190
+ graphic.cornerRadius ?? 0,
5191
+ aspectWidth,
5192
+ aspectHeight
3448
5193
  );
3449
5194
  paintPath(context, graphic);
3450
5195
  return;
@@ -3483,7 +5228,7 @@ function drawGraphic(context, graphic) {
3483
5228
  paintPath(context, graphic, path);
3484
5229
  context.restore();
3485
5230
  }
3486
- function createGraphicTexture(graphic) {
5231
+ function createGraphicTexture(graphic, aspectWidth, aspectHeight) {
3487
5232
  const canvas = document.createElement("canvas");
3488
5233
  canvas.width = GRAPHIC_TEXTURE_SIZE;
3489
5234
  canvas.height = GRAPHIC_TEXTURE_SIZE;
@@ -3491,7 +5236,7 @@ function createGraphicTexture(graphic) {
3491
5236
  if (!context) {
3492
5237
  throw new Error("Canvas 2D is unavailable for this graphic hotspot.");
3493
5238
  }
3494
- drawGraphic(context, graphic);
5239
+ drawGraphic(context, graphic, aspectWidth, aspectHeight);
3495
5240
  const texture = new import_three13.CanvasTexture(canvas);
3496
5241
  texture.colorSpace = import_three13.SRGBColorSpace;
3497
5242
  texture.wrapS = import_three13.ClampToEdgeWrapping;
@@ -3502,16 +5247,16 @@ function createGraphicTexture(graphic) {
3502
5247
  texture.needsUpdate = true;
3503
5248
  return texture;
3504
5249
  }
3505
- function useGraphicTexture(graphic, onLoad, onError) {
3506
- const [texture, setTexture] = (0, import_react13.useState)(null);
3507
- const onLoadRef = (0, import_react13.useRef)(onLoad);
3508
- 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);
3509
5254
  onLoadRef.current = onLoad;
3510
5255
  onErrorRef.current = onError;
3511
- (0, import_react13.useEffect)(() => {
5256
+ (0, import_react20.useEffect)(() => {
3512
5257
  let nextTexture = null;
3513
5258
  try {
3514
- nextTexture = createGraphicTexture(graphic);
5259
+ nextTexture = createGraphicTexture(graphic, aspectWidth, aspectHeight);
3515
5260
  setTexture(nextTexture);
3516
5261
  onLoadRef.current?.(nextTexture);
3517
5262
  } catch (error) {
@@ -3521,7 +5266,7 @@ function useGraphicTexture(graphic, onLoad, onError) {
3521
5266
  return () => {
3522
5267
  nextTexture?.dispose();
3523
5268
  };
3524
- }, [graphic]);
5269
+ }, [aspectHeight, aspectWidth, graphic]);
3525
5270
  return texture;
3526
5271
  }
3527
5272
  function CanvasGraphicHotspot({
@@ -3533,10 +5278,10 @@ function CanvasGraphicHotspot({
3533
5278
  onError,
3534
5279
  ...anchorProps
3535
5280
  }) {
3536
- const texture = useGraphicTexture(graphic, onLoad, onError);
3537
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("mesh", { children: [
3538
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("planeGeometry", { args: [1, 1] }),
3539
- /* @__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)(
3540
5285
  "meshBasicMaterial",
3541
5286
  {
3542
5287
  map: texture,
@@ -3552,16 +5297,16 @@ function GraphicHotspot(props) {
3552
5297
  const { graphic, ...restProps } = props;
3553
5298
  if (graphic.kind === "svg") {
3554
5299
  const imageProps = restProps;
3555
- 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 });
3556
5301
  }
3557
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(CanvasGraphicHotspot, { ...restProps, graphic });
5302
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CanvasGraphicHotspot, { ...restProps, graphic });
3558
5303
  }
3559
5304
 
3560
5305
  // src/hotspot/sequence-hotspot.tsx
3561
- var import_fiber12 = require("@react-three/fiber");
3562
- var import_react14 = require("react");
5306
+ var import_fiber17 = require("@react-three/fiber");
5307
+ var import_react21 = require("react");
3563
5308
  var import_three14 = require("three");
3564
- var import_jsx_runtime13 = require("react/jsx-runtime");
5309
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3565
5310
  var DEFAULT_SEQUENCE_FPS = 12;
3566
5311
  function clampOpacity3(opacity) {
3567
5312
  if (!Number.isFinite(opacity)) {
@@ -3581,13 +5326,13 @@ function useSpriteTexture({
3581
5326
  onError,
3582
5327
  onLoadProgress
3583
5328
  }) {
3584
- const gl = (0, import_fiber12.useThree)((state) => state.gl);
3585
- const [texture, setTexture] = (0, import_react14.useState)(null);
3586
- const onErrorRef = (0, import_react14.useRef)(onError);
3587
- 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);
3588
5333
  onErrorRef.current = onError;
3589
5334
  onLoadProgressRef.current = onLoadProgress;
3590
- (0, import_react14.useEffect)(() => {
5335
+ (0, import_react21.useEffect)(() => {
3591
5336
  let active = true;
3592
5337
  setTexture(null);
3593
5338
  if (!src) {
@@ -3628,7 +5373,7 @@ function useSpriteTexture({
3628
5373
  active = false;
3629
5374
  };
3630
5375
  }, [gl, id, src]);
3631
- (0, import_react14.useEffect)(
5376
+ (0, import_react21.useEffect)(
3632
5377
  () => () => {
3633
5378
  texture?.dispose();
3634
5379
  },
@@ -3657,24 +5402,24 @@ function SequenceHotspot({
3657
5402
  const texture = useSpriteTexture({ id, src, onError, onLoadProgress });
3658
5403
  const frameCount = resolveFrameCount(requestedFrameCount);
3659
5404
  const safeFps = resolveFps(fps);
3660
- const [frameIndex, setFrameIndex] = (0, import_react14.useState)(0);
3661
- const elapsedRef = (0, import_react14.useRef)(0);
3662
- const frameIndexRef = (0, import_react14.useRef)(0);
3663
- const endedRef = (0, import_react14.useRef)(false);
3664
- const lastPlaybackStateRef = (0, import_react14.useRef)(null);
3665
- const onFrameChangeRef = (0, import_react14.useRef)(onFrameChange);
3666
- const onPlaybackStateChangeRef = (0, import_react14.useRef)(onPlaybackStateChange);
3667
- 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);
3668
5413
  onFrameChangeRef.current = onFrameChange;
3669
5414
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3670
5415
  onEndedRef.current = onEnded;
3671
- (0, import_react14.useEffect)(() => {
5416
+ (0, import_react21.useEffect)(() => {
3672
5417
  elapsedRef.current = 0;
3673
5418
  frameIndexRef.current = 0;
3674
5419
  endedRef.current = false;
3675
5420
  setFrameIndex(0);
3676
5421
  }, [frameCount, frameDirection, src]);
3677
- (0, import_react14.useEffect)(() => {
5422
+ (0, import_react21.useEffect)(() => {
3678
5423
  if (!texture) {
3679
5424
  return;
3680
5425
  }
@@ -3688,13 +5433,13 @@ function SequenceHotspot({
3688
5433
  );
3689
5434
  texture.needsUpdate = true;
3690
5435
  }, [frameCount, frameDirection, frameIndex, texture]);
3691
- (0, import_react14.useEffect)(() => {
5436
+ (0, import_react21.useEffect)(() => {
3692
5437
  if (!texture) {
3693
5438
  return;
3694
5439
  }
3695
5440
  onFrameChangeRef.current?.({ id, index: 0, src });
3696
5441
  }, [id, src, texture]);
3697
- (0, import_react14.useEffect)(() => {
5442
+ (0, import_react21.useEffect)(() => {
3698
5443
  if (!texture) {
3699
5444
  return;
3700
5445
  }
@@ -3713,7 +5458,7 @@ function SequenceHotspot({
3713
5458
  onPlaybackStateChangeRef.current?.(nextState);
3714
5459
  }
3715
5460
  }, [playing, texture]);
3716
- (0, import_fiber12.useFrame)((_, deltaSeconds) => {
5461
+ (0, import_fiber17.useFrame)((_, deltaSeconds) => {
3717
5462
  if (!texture || !playing || endedRef.current) {
3718
5463
  return;
3719
5464
  }
@@ -3743,9 +5488,9 @@ function SequenceHotspot({
3743
5488
  onFrameChangeRef.current?.({ id, index: nextIndex, src });
3744
5489
  }
3745
5490
  });
3746
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("mesh", { children: [
3747
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("planeGeometry", { args: [1, 1] }),
3748
- /* @__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)(
3749
5494
  "meshBasicMaterial",
3750
5495
  {
3751
5496
  map: texture,
@@ -3759,10 +5504,10 @@ function SequenceHotspot({
3759
5504
  }
3760
5505
 
3761
5506
  // src/hotspot/video-hotspot.tsx
3762
- var import_fiber13 = require("@react-three/fiber");
3763
- var import_react15 = require("react");
5507
+ var import_fiber18 = require("@react-three/fiber");
5508
+ var import_react22 = require("react");
3764
5509
  var import_three15 = require("three");
3765
- var import_jsx_runtime14 = require("react/jsx-runtime");
5510
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3766
5511
  function clampOpacity4(opacity) {
3767
5512
  if (!Number.isFinite(opacity)) {
3768
5513
  return 1;
@@ -3776,11 +5521,11 @@ function clampVolume(volume) {
3776
5521
  return Math.max(0, Math.min(volume, 1));
3777
5522
  }
3778
5523
  function usePosterTexture(id, poster, onError) {
3779
- const gl = (0, import_fiber13.useThree)((state) => state.gl);
3780
- const [texture, setTexture] = (0, import_react15.useState)(null);
3781
- 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);
3782
5527
  onErrorRef.current = onError;
3783
- (0, import_react15.useEffect)(() => {
5528
+ (0, import_react22.useEffect)(() => {
3784
5529
  let active = true;
3785
5530
  setTexture(null);
3786
5531
  if (!poster) {
@@ -3817,7 +5562,7 @@ function usePosterTexture(id, poster, onError) {
3817
5562
  active = false;
3818
5563
  };
3819
5564
  }, [gl, id, poster]);
3820
- (0, import_react15.useEffect)(
5565
+ (0, import_react22.useEffect)(
3821
5566
  () => () => {
3822
5567
  texture?.dispose();
3823
5568
  },
@@ -3835,14 +5580,14 @@ function useVideoResource({
3835
5580
  poster,
3836
5581
  src
3837
5582
  }) {
3838
- const [resource, setResource] = (0, import_react15.useState)(null);
3839
- const onEndedRef = (0, import_react15.useRef)(onEnded);
3840
- const onErrorRef = (0, import_react15.useRef)(onError);
3841
- 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);
3842
5587
  onEndedRef.current = onEnded;
3843
5588
  onErrorRef.current = onError;
3844
5589
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3845
- (0, import_react15.useEffect)(() => {
5590
+ (0, import_react22.useEffect)(() => {
3846
5591
  if (!src) {
3847
5592
  setResource(null);
3848
5593
  return;
@@ -3943,11 +5688,11 @@ function VideoHotspot({
3943
5688
  src
3944
5689
  });
3945
5690
  const posterTexture = usePosterTexture(id, poster, onError);
3946
- const onPlaybackErrorRef = (0, import_react15.useRef)(onPlaybackError);
3947
- const onPlaybackStateChangeRef = (0, import_react15.useRef)(onPlaybackStateChange);
5691
+ const onPlaybackErrorRef = (0, import_react22.useRef)(onPlaybackError);
5692
+ const onPlaybackStateChangeRef = (0, import_react22.useRef)(onPlaybackStateChange);
3948
5693
  onPlaybackErrorRef.current = onPlaybackError;
3949
5694
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
3950
- (0, import_react15.useEffect)(() => {
5695
+ (0, import_react22.useEffect)(() => {
3951
5696
  if (!resource) {
3952
5697
  return;
3953
5698
  }
@@ -3957,7 +5702,7 @@ function VideoHotspot({
3957
5702
  resource.video.playsInline = playsInline;
3958
5703
  resource.video.preload = preload;
3959
5704
  }, [loop, muted, playsInline, preload, resource, volume]);
3960
- (0, import_react15.useEffect)(() => {
5705
+ (0, import_react22.useEffect)(() => {
3961
5706
  if (!resource) {
3962
5707
  return;
3963
5708
  }
@@ -3980,9 +5725,9 @@ function VideoHotspot({
3980
5725
  };
3981
5726
  }, [playing, resource]);
3982
5727
  const activeTexture = resource?.ready ? resource.texture : posterTexture;
3983
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("mesh", { children: [
3984
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("planeGeometry", { args: [1, 1] }),
3985
- /* @__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)(
3986
5731
  "meshBasicMaterial",
3987
5732
  {
3988
5733
  map: activeTexture,
@@ -3999,7 +5744,12 @@ function VideoHotspot({
3999
5744
  AutoRotate,
4000
5745
  GraphicHotspot,
4001
5746
  ImageHotspot,
5747
+ KeyboardControls,
4002
5748
  MAX_HOTSPOT_PITCH,
5749
+ MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY,
5750
+ MouseControls,
5751
+ PanoContextMenu,
5752
+ PanoEvents,
4003
5753
  PanoView,
4004
5754
  PanoramaScenes,
4005
5755
  PolygonHotspot,
@@ -4007,12 +5757,19 @@ function VideoHotspot({
4007
5757
  SequenceHotspot,
4008
5758
  Sphere,
4009
5759
  Tile,
5760
+ TouchControls,
4010
5761
  VideoHotspot,
4011
5762
  clampPanoPitch,
5763
+ composePanoContextMenuItems,
5764
+ createDefaultPanoContextMenuItems,
5765
+ createPanoContextMenuPresets,
5766
+ cycleSceneId,
4012
5767
  normalizePanoPosition,
4013
5768
  normalizePanoYaw,
4014
5769
  panoPositionToVector3,
5770
+ resolvePanoContextMenuEntries,
4015
5771
  unwrapPolygonVertices,
5772
+ usePanoEvents,
4016
5773
  validatePolygonVertices,
4017
5774
  validatePolylineVertices,
4018
5775
  vector3ToPanoPosition