@ericchen1990/pano-view 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,4092 @@
1
+ "use client";
2
+
3
+ // src/pano-view.tsx
4
+ import { Canvas } from "@react-three/fiber";
5
+ import {
6
+ forwardRef as forwardRef2,
7
+ useEffect as useEffect4,
8
+ useImperativeHandle as useImperativeHandle2,
9
+ useMemo as useMemo2,
10
+ useRef as useRef4,
11
+ useState as useState2
12
+ } from "react";
13
+
14
+ // src/panorama-controls.tsx
15
+ import { useFrame, useThree } from "@react-three/fiber";
16
+ import {
17
+ forwardRef,
18
+ useEffect,
19
+ useImperativeHandle,
20
+ useRef
21
+ } from "react";
22
+ import { Euler, MathUtils, PerspectiveCamera } from "three";
23
+ var MAX_PITCH = 89.9;
24
+ var INERTIA_ROTATION_DAMPING = 8;
25
+ var INERTIA_ZOOM_DAMPING = 12;
26
+ var DEFAULT_ROTATE_DAMPING = 14;
27
+ var DEFAULT_ZOOM_DAMPING = 16;
28
+ var MIN_ROTATION_VELOCITY = 0.01;
29
+ var MIN_ZOOM_VELOCITY = 0.01;
30
+ var VIEW_SETTLE_EPSILON = 1e-3;
31
+ function clamp(value, min, max) {
32
+ return Math.max(min, Math.min(max, value));
33
+ }
34
+ function normalizeYaw(yaw) {
35
+ return MathUtils.euclideanModulo(yaw + 180, 360) - 180;
36
+ }
37
+ function shortestYawDelta(from, to) {
38
+ return normalizeYaw(to - from);
39
+ }
40
+ function dampingFactor(damping, deltaSeconds) {
41
+ return damping === 0 ? 1 : 1 - Math.exp(-damping * deltaSeconds);
42
+ }
43
+ function resolveDamping(value, fallback) {
44
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
45
+ }
46
+ function pointerDistance(a, b) {
47
+ return Math.hypot(a.x - b.x, a.y - b.y);
48
+ }
49
+ function viewsEqual(a, b) {
50
+ return a.yaw === b.yaw && a.pitch === b.pitch && a.fov === b.fov;
51
+ }
52
+ var PanoramaControls = forwardRef(function PanoramaControls2({ enabled, initialView, minFov, maxFov, options, onViewChange }, ref) {
53
+ const { camera, gl } = useThree();
54
+ const viewRef = useRef({ ...initialView });
55
+ const targetViewRef = useRef({ ...initialView });
56
+ const initialViewRef = useRef({ ...initialView });
57
+ const pointersRef = useRef(/* @__PURE__ */ new Map());
58
+ const pinchDistanceRef = useRef(null);
59
+ const yawVelocityRef = useRef(0);
60
+ const pitchVelocityRef = useRef(0);
61
+ const zoomVelocityRef = useRef(0);
62
+ const dirtyRef = useRef(true);
63
+ const interactingRef = useRef(false);
64
+ const interactionLockCountRef = useRef(0);
65
+ const optionsRef = useRef(options);
66
+ const onViewChangeRef = useRef(onViewChange);
67
+ const lastEmittedViewRef = useRef(null);
68
+ const eulerRef = useRef(new Euler(0, 0, 0, "YXZ"));
69
+ optionsRef.current = options;
70
+ onViewChangeRef.current = onViewChange;
71
+ const constrainView = (view, current = targetViewRef.current) => {
72
+ return {
73
+ yaw: normalizeYaw(view.yaw ?? current.yaw),
74
+ pitch: clamp(view.pitch ?? current.pitch, -MAX_PITCH, MAX_PITCH),
75
+ fov: clamp(view.fov ?? current.fov, minFov, maxFov)
76
+ };
77
+ };
78
+ const stopVelocity = () => {
79
+ yawVelocityRef.current = 0;
80
+ pitchVelocityRef.current = 0;
81
+ zoomVelocityRef.current = 0;
82
+ };
83
+ const acquireInteractionLock = () => {
84
+ interactionLockCountRef.current += 1;
85
+ stopVelocity();
86
+ interactingRef.current = false;
87
+ pinchDistanceRef.current = null;
88
+ for (const pointerId of pointersRef.current.keys()) {
89
+ if (gl.domElement.hasPointerCapture?.(pointerId)) {
90
+ gl.domElement.releasePointerCapture?.(pointerId);
91
+ }
92
+ }
93
+ pointersRef.current.clear();
94
+ let released = false;
95
+ return () => {
96
+ if (released) {
97
+ return;
98
+ }
99
+ released = true;
100
+ interactionLockCountRef.current = Math.max(
101
+ 0,
102
+ interactionLockCountRef.current - 1
103
+ );
104
+ };
105
+ };
106
+ const setView = (view, setOptions) => {
107
+ const nextView = constrainView(view);
108
+ targetViewRef.current = nextView;
109
+ viewRef.current = nextView;
110
+ if (setOptions?.immediate !== false) {
111
+ stopVelocity();
112
+ }
113
+ dirtyRef.current = true;
114
+ };
115
+ useImperativeHandle(
116
+ ref,
117
+ () => ({
118
+ getView: () => ({ ...viewRef.current }),
119
+ setView,
120
+ reset: () => setView(initialViewRef.current),
121
+ acquireInteractionLock,
122
+ applyAutoRotation: (yawDelta) => {
123
+ const inertiaFinished = yawVelocityRef.current === 0 && pitchVelocityRef.current === 0 && zoomVelocityRef.current === 0;
124
+ if (!Number.isFinite(yawDelta) || interactionLockCountRef.current > 0 || interactingRef.current || !inertiaFinished) {
125
+ return false;
126
+ }
127
+ targetViewRef.current.yaw += yawDelta;
128
+ dirtyRef.current = true;
129
+ return true;
130
+ }
131
+ }),
132
+ [gl, maxFov, minFov]
133
+ );
134
+ useEffect(() => {
135
+ initialViewRef.current = { ...initialView };
136
+ setView(initialView);
137
+ }, [initialView.fov, initialView.pitch, initialView.yaw]);
138
+ useEffect(() => {
139
+ const element = gl.domElement;
140
+ if (!enabled) {
141
+ pointersRef.current.clear();
142
+ interactingRef.current = false;
143
+ return;
144
+ }
145
+ const updatePointer = (event) => {
146
+ const previous = pointersRef.current.get(event.pointerId);
147
+ const now = performance.now();
148
+ const next = { x: event.clientX, y: event.clientY, at: now };
149
+ pointersRef.current.set(event.pointerId, next);
150
+ return { previous, next };
151
+ };
152
+ const onPointerDown = (event) => {
153
+ if (interactionLockCountRef.current > 0) {
154
+ return;
155
+ }
156
+ event.preventDefault();
157
+ interactingRef.current = true;
158
+ stopVelocity();
159
+ pointersRef.current.set(event.pointerId, {
160
+ x: event.clientX,
161
+ y: event.clientY,
162
+ at: performance.now()
163
+ });
164
+ element.setPointerCapture?.(event.pointerId);
165
+ if (pointersRef.current.size === 2) {
166
+ const [first, second] = Array.from(pointersRef.current.values());
167
+ pinchDistanceRef.current = pointerDistance(first, second);
168
+ }
169
+ };
170
+ const onPointerMove = (event) => {
171
+ if (interactionLockCountRef.current > 0) {
172
+ return;
173
+ }
174
+ if (!pointersRef.current.has(event.pointerId)) {
175
+ return;
176
+ }
177
+ event.preventDefault();
178
+ const { previous, next } = updatePointer(event);
179
+ if (!previous) {
180
+ return;
181
+ }
182
+ const pointers = Array.from(pointersRef.current.values());
183
+ if (pointers.length >= 2) {
184
+ const distance = pointerDistance(pointers[0], pointers[1]);
185
+ const previousDistance = pinchDistanceRef.current;
186
+ pinchDistanceRef.current = distance;
187
+ if (previousDistance && distance > 0) {
188
+ const currentFov = targetViewRef.current.fov;
189
+ const nextFov = clamp(
190
+ currentFov * (previousDistance / distance),
191
+ minFov,
192
+ maxFov
193
+ );
194
+ const elapsed2 = Math.max((next.at - previous.at) / 1e3, 1 / 120);
195
+ zoomVelocityRef.current = (nextFov - currentFov) / elapsed2;
196
+ targetViewRef.current = {
197
+ ...targetViewRef.current,
198
+ fov: nextFov
199
+ };
200
+ dirtyRef.current = true;
201
+ }
202
+ return;
203
+ }
204
+ const rect = element.getBoundingClientRect();
205
+ const rotateSpeed = optionsRef.current.rotateSpeed ?? 0.35;
206
+ const deltaYaw = -(next.x - previous.x) * 360 * rotateSpeed / Math.max(rect.width, 1);
207
+ const deltaPitch = (next.y - previous.y) * 180 * rotateSpeed / Math.max(rect.height, 1);
208
+ const elapsed = Math.max((next.at - previous.at) / 1e3, 1 / 120);
209
+ targetViewRef.current = constrainView({
210
+ yaw: targetViewRef.current.yaw + deltaYaw,
211
+ pitch: targetViewRef.current.pitch + deltaPitch
212
+ });
213
+ yawVelocityRef.current = deltaYaw / elapsed;
214
+ pitchVelocityRef.current = deltaPitch / elapsed;
215
+ dirtyRef.current = true;
216
+ };
217
+ const releasePointer = (event) => {
218
+ pointersRef.current.delete(event.pointerId);
219
+ if (element.hasPointerCapture?.(event.pointerId)) {
220
+ element.releasePointerCapture?.(event.pointerId);
221
+ }
222
+ if (pointersRef.current.size < 2) {
223
+ pinchDistanceRef.current = null;
224
+ }
225
+ interactingRef.current = pointersRef.current.size > 0;
226
+ };
227
+ const onWheel = (event) => {
228
+ if (interactionLockCountRef.current > 0) {
229
+ event.preventDefault();
230
+ return;
231
+ }
232
+ event.preventDefault();
233
+ const zoomSpeed = optionsRef.current.zoomSpeed ?? 0.08;
234
+ const delta = event.deltaY * zoomSpeed;
235
+ const nextFov = clamp(
236
+ targetViewRef.current.fov + delta,
237
+ minFov,
238
+ maxFov
239
+ );
240
+ zoomVelocityRef.current = clamp(
241
+ zoomVelocityRef.current + delta * 12,
242
+ -180,
243
+ 180
244
+ );
245
+ targetViewRef.current = {
246
+ ...targetViewRef.current,
247
+ fov: nextFov
248
+ };
249
+ dirtyRef.current = true;
250
+ };
251
+ const onKeyDown = (event) => {
252
+ if (interactionLockCountRef.current > 0) {
253
+ return;
254
+ }
255
+ if (optionsRef.current.keyboard === false) {
256
+ return;
257
+ }
258
+ const step = event.shiftKey ? 10 : 3;
259
+ let next = null;
260
+ if (event.key === "ArrowLeft") {
261
+ next = { yaw: targetViewRef.current.yaw - step };
262
+ } else if (event.key === "ArrowRight") {
263
+ next = { yaw: targetViewRef.current.yaw + step };
264
+ } else if (event.key === "ArrowUp") {
265
+ next = { pitch: targetViewRef.current.pitch + step };
266
+ } else if (event.key === "ArrowDown") {
267
+ next = { pitch: targetViewRef.current.pitch - step };
268
+ } else if (event.key === "+" || event.key === "=") {
269
+ next = { fov: targetViewRef.current.fov - step };
270
+ } else if (event.key === "-" || event.key === "_") {
271
+ next = { fov: targetViewRef.current.fov + step };
272
+ } else if (event.key === "0") {
273
+ setView(initialViewRef.current);
274
+ event.preventDefault();
275
+ return;
276
+ }
277
+ if (next) {
278
+ event.preventDefault();
279
+ targetViewRef.current = constrainView(next);
280
+ dirtyRef.current = true;
281
+ }
282
+ };
283
+ element.addEventListener("pointerdown", onPointerDown);
284
+ element.addEventListener("pointermove", onPointerMove);
285
+ element.addEventListener("pointerup", releasePointer);
286
+ element.addEventListener("pointercancel", releasePointer);
287
+ element.addEventListener("wheel", onWheel, { passive: false });
288
+ element.addEventListener("keydown", onKeyDown);
289
+ return () => {
290
+ element.removeEventListener("pointerdown", onPointerDown);
291
+ element.removeEventListener("pointermove", onPointerMove);
292
+ element.removeEventListener("pointerup", releasePointer);
293
+ element.removeEventListener("pointercancel", releasePointer);
294
+ element.removeEventListener("wheel", onWheel);
295
+ element.removeEventListener("keydown", onKeyDown);
296
+ pointersRef.current.clear();
297
+ };
298
+ }, [enabled, gl, maxFov, minFov]);
299
+ useFrame((_, deltaSeconds) => {
300
+ if (!(camera instanceof PerspectiveCamera)) {
301
+ return;
302
+ }
303
+ const inertiaEnabled = optionsRef.current.inertia !== false;
304
+ if (!interactingRef.current && inertiaEnabled) {
305
+ const rotationDamping = Math.exp(
306
+ -INERTIA_ROTATION_DAMPING * deltaSeconds
307
+ );
308
+ const zoomDamping2 = Math.exp(-INERTIA_ZOOM_DAMPING * deltaSeconds);
309
+ if (Math.abs(yawVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
310
+ targetViewRef.current.yaw += yawVelocityRef.current * deltaSeconds;
311
+ yawVelocityRef.current *= rotationDamping;
312
+ dirtyRef.current = true;
313
+ } else {
314
+ yawVelocityRef.current = 0;
315
+ }
316
+ if (Math.abs(pitchVelocityRef.current) >= MIN_ROTATION_VELOCITY) {
317
+ targetViewRef.current.pitch += pitchVelocityRef.current * deltaSeconds;
318
+ pitchVelocityRef.current *= rotationDamping;
319
+ dirtyRef.current = true;
320
+ } else {
321
+ pitchVelocityRef.current = 0;
322
+ }
323
+ if (Math.abs(zoomVelocityRef.current) >= MIN_ZOOM_VELOCITY) {
324
+ targetViewRef.current.fov += zoomVelocityRef.current * deltaSeconds;
325
+ zoomVelocityRef.current *= zoomDamping2;
326
+ dirtyRef.current = true;
327
+ } else {
328
+ zoomVelocityRef.current = 0;
329
+ }
330
+ } else if (!inertiaEnabled) {
331
+ stopVelocity();
332
+ }
333
+ targetViewRef.current = constrainView(targetViewRef.current);
334
+ if (!dirtyRef.current) {
335
+ return;
336
+ }
337
+ const targetView = targetViewRef.current;
338
+ const rotateDamping = resolveDamping(
339
+ optionsRef.current.rotateDamping,
340
+ DEFAULT_ROTATE_DAMPING
341
+ );
342
+ const zoomDamping = resolveDamping(
343
+ optionsRef.current.zoomDamping,
344
+ DEFAULT_ZOOM_DAMPING
345
+ );
346
+ const rotationFactor = dampingFactor(rotateDamping, deltaSeconds);
347
+ const zoomFactor = dampingFactor(zoomDamping, deltaSeconds);
348
+ const yawDelta = shortestYawDelta(viewRef.current.yaw, targetView.yaw);
349
+ const smoothedView = {
350
+ yaw: normalizeYaw(viewRef.current.yaw + yawDelta * rotationFactor),
351
+ pitch: viewRef.current.pitch + (targetView.pitch - viewRef.current.pitch) * rotationFactor,
352
+ fov: viewRef.current.fov + (targetView.fov - viewRef.current.fov) * zoomFactor
353
+ };
354
+ const hasSettled = Math.abs(shortestYawDelta(smoothedView.yaw, targetView.yaw)) < VIEW_SETTLE_EPSILON && Math.abs(smoothedView.pitch - targetView.pitch) < VIEW_SETTLE_EPSILON && Math.abs(smoothedView.fov - targetView.fov) < VIEW_SETTLE_EPSILON;
355
+ viewRef.current = hasSettled ? { ...targetView } : smoothedView;
356
+ eulerRef.current.set(
357
+ MathUtils.degToRad(viewRef.current.pitch),
358
+ MathUtils.degToRad(-viewRef.current.yaw),
359
+ 0,
360
+ "YXZ"
361
+ );
362
+ camera.quaternion.setFromEuler(eulerRef.current);
363
+ camera.fov = viewRef.current.fov;
364
+ camera.updateProjectionMatrix();
365
+ camera.updateMatrixWorld();
366
+ const nextView = { ...viewRef.current };
367
+ const lastEmitted = lastEmittedViewRef.current;
368
+ if (!lastEmitted || !viewsEqual(lastEmitted, nextView)) {
369
+ lastEmittedViewRef.current = nextView;
370
+ onViewChangeRef.current?.(nextView);
371
+ }
372
+ dirtyRef.current = !hasSettled;
373
+ });
374
+ return null;
375
+ });
376
+
377
+ // src/auto-rotate.tsx
378
+ import { useFrame as useFrame2 } from "@react-three/fiber";
379
+ import {
380
+ createContext,
381
+ useContext,
382
+ useEffect as useEffect2,
383
+ useRef as useRef2
384
+ } from "react";
385
+ var DEFAULT_AUTO_ROTATE_SPEED = 18;
386
+ var DEFAULT_AUTO_ROTATE_ACCELERATION = 18;
387
+ var DEFAULT_AUTO_ROTATE_START_DELAY = 2;
388
+ var PanoramaControlsContext = createContext(null);
389
+ function resolveStartDelay(value) {
390
+ return Number.isFinite(value) ? Math.max(value, 0) : 0;
391
+ }
392
+ function resolveAcceleration(value) {
393
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_AUTO_ROTATE_ACCELERATION;
394
+ }
395
+ function AutoRotate({
396
+ enabled = true,
397
+ speed = DEFAULT_AUTO_ROTATE_SPEED,
398
+ acceleration = DEFAULT_AUTO_ROTATE_ACCELERATION,
399
+ startDelay = DEFAULT_AUTO_ROTATE_START_DELAY
400
+ }) {
401
+ const controlsRef = useContext(PanoramaControlsContext);
402
+ const elapsedRef = useRef2(0);
403
+ const rotationSpeedRef = useRef2(0);
404
+ if (!controlsRef) {
405
+ throw new Error("<AutoRotate> must be rendered inside <PanoView>.");
406
+ }
407
+ useEffect2(() => {
408
+ elapsedRef.current = 0;
409
+ rotationSpeedRef.current = 0;
410
+ }, [enabled, startDelay]);
411
+ useFrame2((_, deltaSeconds) => {
412
+ if (!enabled || !Number.isFinite(speed) || speed === 0) {
413
+ rotationSpeedRef.current = 0;
414
+ return;
415
+ }
416
+ const delaySeconds = resolveStartDelay(startDelay) / 1e3;
417
+ elapsedRef.current += deltaSeconds;
418
+ if (elapsedRef.current < delaySeconds) {
419
+ return;
420
+ }
421
+ const targetSpeed = Math.abs(speed);
422
+ const currentSpeed = rotationSpeedRef.current;
423
+ const accelerationPerSecond = resolveAcceleration(acceleration);
424
+ const nextSpeed = accelerationPerSecond === 0 ? targetSpeed : Math.min(
425
+ targetSpeed,
426
+ currentSpeed + accelerationPerSecond * deltaSeconds
427
+ );
428
+ const averageSpeed = (currentSpeed + nextSpeed) / 2;
429
+ const yawDelta = Math.sign(speed) * averageSpeed * deltaSeconds;
430
+ if (!controlsRef.current?.applyAutoRotation(yawDelta)) {
431
+ rotationSpeedRef.current = 0;
432
+ return;
433
+ }
434
+ rotationSpeedRef.current = nextSpeed;
435
+ });
436
+ return null;
437
+ }
438
+
439
+ // src/hotspot/accessibility.tsx
440
+ import {
441
+ createContext as createContext2,
442
+ useCallback,
443
+ useContext as useContext2,
444
+ useEffect as useEffect3,
445
+ useMemo,
446
+ useRef as useRef3,
447
+ useState
448
+ } from "react";
449
+ import { jsx } from "react/jsx-runtime";
450
+ var HotspotAccessibilityContext = createContext2(null);
451
+ var VISUALLY_HIDDEN_STYLE = {
452
+ border: 0,
453
+ clip: "rect(0 0 0 0)",
454
+ clipPath: "inset(50%)",
455
+ height: 1,
456
+ margin: -1,
457
+ overflow: "hidden",
458
+ padding: 0,
459
+ position: "absolute",
460
+ whiteSpace: "nowrap",
461
+ width: 1
462
+ };
463
+ function warnAboutMissingLabel(id) {
464
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
465
+ if (nodeEnv !== "production") {
466
+ console.warn(
467
+ `Hotspot "${id}" is clickable but has no ariaLabel, so it is unavailable to keyboard and screen-reader users.`
468
+ );
469
+ }
470
+ }
471
+ function useHotspotAccessibility({
472
+ id,
473
+ ariaLabel,
474
+ onActivate
475
+ }) {
476
+ const registry = useContext2(HotspotAccessibilityContext);
477
+ const [focused, setFocused] = useState(false);
478
+ const activateRef = useRef3(onActivate);
479
+ const enabled = Boolean(onActivate);
480
+ activateRef.current = onActivate;
481
+ useEffect3(() => {
482
+ if (!enabled) {
483
+ setFocused(false);
484
+ return;
485
+ }
486
+ if (!ariaLabel) {
487
+ warnAboutMissingLabel(id);
488
+ return;
489
+ }
490
+ if (!registry) {
491
+ return;
492
+ }
493
+ return registry.register({
494
+ id,
495
+ label: ariaLabel,
496
+ activate: (event) => activateRef.current?.(event),
497
+ onFocusChange: setFocused
498
+ });
499
+ }, [ariaLabel, enabled, id, registry]);
500
+ return focused;
501
+ }
502
+ function useHotspotAccessibilityLayer() {
503
+ const [entries, setEntries] = useState(() => /* @__PURE__ */ new Map());
504
+ const register = useCallback(
505
+ (entry) => {
506
+ const token = Symbol(entry.id);
507
+ const nextEntry = { ...entry, token };
508
+ setEntries((current) => {
509
+ const next = new Map(current);
510
+ next.set(entry.id, nextEntry);
511
+ return next;
512
+ });
513
+ return () => {
514
+ setEntries((current) => {
515
+ if (current.get(entry.id)?.token !== token) {
516
+ return current;
517
+ }
518
+ const next = new Map(current);
519
+ next.delete(entry.id);
520
+ return next;
521
+ });
522
+ };
523
+ },
524
+ []
525
+ );
526
+ const registry = useMemo(
527
+ () => ({ register }),
528
+ [register]
529
+ );
530
+ const controls = useMemo(
531
+ () => /* @__PURE__ */ jsx("div", { "aria-label": "Panorama hotspots", style: { pointerEvents: "none" }, children: Array.from(entries.values()).map((entry) => /* @__PURE__ */ jsx(
532
+ "button",
533
+ {
534
+ "aria-label": entry.label,
535
+ onBlur: () => entry.onFocusChange(false),
536
+ onClick: (event) => entry.activate(event.nativeEvent),
537
+ onFocus: () => entry.onFocusChange(true),
538
+ onKeyDown: (event) => {
539
+ if (event.key === "Enter" || event.key === " ") {
540
+ event.preventDefault();
541
+ entry.activate(event.nativeEvent);
542
+ }
543
+ },
544
+ style: VISUALLY_HIDDEN_STYLE,
545
+ type: "button",
546
+ children: entry.label
547
+ },
548
+ entry.id
549
+ )) }),
550
+ [entries]
551
+ );
552
+ return { controls, registry };
553
+ }
554
+
555
+ // src/panorama-event-surface.tsx
556
+ import { BackSide } from "three";
557
+
558
+ // src/hotspot/coordinates.ts
559
+ import { MathUtils as MathUtils2, Vector3 } from "three";
560
+
561
+ // src/hotspot/types.ts
562
+ var MAX_HOTSPOT_PITCH = 89.9;
563
+
564
+ // src/hotspot/coordinates.ts
565
+ function normalizePanoYaw(yaw) {
566
+ if (!Number.isFinite(yaw)) {
567
+ return 0;
568
+ }
569
+ return MathUtils2.euclideanModulo(yaw + 180, 360) - 180;
570
+ }
571
+ function clampPanoPitch(pitch) {
572
+ if (!Number.isFinite(pitch)) {
573
+ return 0;
574
+ }
575
+ return MathUtils2.clamp(pitch, -MAX_HOTSPOT_PITCH, MAX_HOTSPOT_PITCH);
576
+ }
577
+ function normalizePanoPosition(position) {
578
+ return {
579
+ yaw: normalizePanoYaw(position.yaw),
580
+ pitch: clampPanoPitch(position.pitch)
581
+ };
582
+ }
583
+ function panoPositionToVector3(position, radius = 1) {
584
+ const normalized = normalizePanoPosition(position);
585
+ const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 1;
586
+ const yaw = MathUtils2.degToRad(normalized.yaw);
587
+ const pitch = MathUtils2.degToRad(normalized.pitch);
588
+ const cosPitch = Math.cos(pitch);
589
+ return new Vector3(
590
+ cosPitch * Math.sin(yaw) * safeRadius,
591
+ Math.sin(pitch) * safeRadius,
592
+ -cosPitch * Math.cos(yaw) * safeRadius
593
+ );
594
+ }
595
+ function vector3ToPanoPosition(vector) {
596
+ const length = Math.hypot(vector.x, vector.y, vector.z);
597
+ if (!Number.isFinite(length) || length === 0) {
598
+ return { yaw: 0, pitch: 0 };
599
+ }
600
+ return normalizePanoPosition({
601
+ yaw: MathUtils2.radToDeg(Math.atan2(vector.x, -vector.z)),
602
+ pitch: MathUtils2.radToDeg(
603
+ Math.asin(MathUtils2.clamp(vector.y / length, -1, 1))
604
+ )
605
+ });
606
+ }
607
+
608
+ // src/panorama-radius.ts
609
+ var DEFAULT_PANORAMA_RADIUS = 1e3;
610
+ var DEFAULT_PANORAMA_CAMERA_NEAR = 0.1;
611
+ var DEFAULT_PANORAMA_CAMERA_FAR = DEFAULT_PANORAMA_RADIUS * 2;
612
+
613
+ // src/panorama-event-surface.tsx
614
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
615
+ var EVENT_SURFACE_RADIUS = DEFAULT_PANORAMA_RADIUS - 1;
616
+ function makePanoramaPointerEvent(event) {
617
+ const nativeEvent = event.nativeEvent;
618
+ return {
619
+ position: vector3ToPanoPosition(event.ray.direction),
620
+ nativeEvent,
621
+ pointerId: "pointerId" in nativeEvent ? nativeEvent.pointerId : void 0,
622
+ button: nativeEvent.button,
623
+ buttons: nativeEvent.buttons,
624
+ clientX: nativeEvent.clientX,
625
+ clientY: nativeEvent.clientY
626
+ };
627
+ }
628
+ function emitPanoramaEvent(event, callback) {
629
+ if (!callback) {
630
+ return;
631
+ }
632
+ event.stopPropagation();
633
+ callback(makePanoramaPointerEvent(event));
634
+ }
635
+ function PanoramaEventSurface({
636
+ onClick,
637
+ onDoubleClick,
638
+ onPointerMove
639
+ }) {
640
+ if (!onClick && !onDoubleClick && !onPointerMove) {
641
+ return null;
642
+ }
643
+ return /* @__PURE__ */ jsxs(
644
+ "mesh",
645
+ {
646
+ renderOrder: -200,
647
+ onClick: (event) => emitPanoramaEvent(event, onClick),
648
+ onDoubleClick: (event) => emitPanoramaEvent(event, onDoubleClick),
649
+ onPointerMove: (event) => emitPanoramaEvent(event, onPointerMove),
650
+ children: [
651
+ /* @__PURE__ */ jsx2("sphereGeometry", { args: [EVENT_SURFACE_RADIUS, 64, 32] }),
652
+ /* @__PURE__ */ jsx2(
653
+ "meshBasicMaterial",
654
+ {
655
+ colorWrite: false,
656
+ depthTest: false,
657
+ depthWrite: false,
658
+ opacity: 0,
659
+ side: BackSide,
660
+ transparent: true
661
+ }
662
+ )
663
+ ]
664
+ }
665
+ );
666
+ }
667
+
668
+ // src/pano-view.tsx
669
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
670
+ var DEFAULT_VIEW = {
671
+ yaw: 0,
672
+ pitch: 0,
673
+ fov: 75
674
+ };
675
+ var DEFAULT_CANVAS_STYLE = {
676
+ display: "block",
677
+ height: "100%",
678
+ width: "100%"
679
+ };
680
+ var PanoView = forwardRef2(
681
+ function PanoView2({
682
+ children,
683
+ initialView,
684
+ minFov = 30,
685
+ maxFov = 100,
686
+ controls = true,
687
+ onViewChange,
688
+ onPanoramaClick,
689
+ onPanoramaDoubleClick,
690
+ onPanoramaPointerMove,
691
+ dpr = [1, 2],
692
+ style,
693
+ "aria-label": ariaLabel = "Panorama viewer",
694
+ ...divProps
695
+ }, ref) {
696
+ const rootRef = useRef4(null);
697
+ const controlsRef = useRef4(null);
698
+ const { controls: hotspotAccessibilityControls, registry } = useHotspotAccessibilityLayer();
699
+ const fallbackViewRef = useRef4(DEFAULT_VIEW);
700
+ const normalizedMinFov = Math.max(1, Math.min(minFov, maxFov - 1));
701
+ const normalizedMaxFov = Math.min(
702
+ 179,
703
+ Math.max(maxFov, normalizedMinFov + 1)
704
+ );
705
+ const normalizedInitialView = useMemo2(
706
+ () => ({
707
+ yaw: initialView?.yaw ?? DEFAULT_VIEW.yaw,
708
+ pitch: initialView?.pitch ?? DEFAULT_VIEW.pitch,
709
+ fov: Math.max(
710
+ normalizedMinFov,
711
+ Math.min(initialView?.fov ?? DEFAULT_VIEW.fov, normalizedMaxFov)
712
+ )
713
+ }),
714
+ [
715
+ initialView?.fov,
716
+ initialView?.pitch,
717
+ initialView?.yaw,
718
+ normalizedMaxFov,
719
+ normalizedMinFov
720
+ ]
721
+ );
722
+ fallbackViewRef.current = normalizedInitialView;
723
+ const controlsEnabled = controls !== false;
724
+ const controlOptions = useMemo2(
725
+ () => typeof controls === "object" ? controls : {},
726
+ [controls]
727
+ );
728
+ const legacyAutoRotateOptions = controlOptions;
729
+ const [legacyAutoRotate, setLegacyAutoRotate] = useState2(
730
+ legacyAutoRotateOptions.autoRotate ?? false
731
+ );
732
+ useEffect4(() => {
733
+ setLegacyAutoRotate(legacyAutoRotateOptions.autoRotate ?? false);
734
+ }, [legacyAutoRotateOptions.autoRotate]);
735
+ useImperativeHandle2(
736
+ ref,
737
+ () => ({
738
+ getView: () => controlsRef.current?.getView() ?? { ...fallbackViewRef.current },
739
+ setView: (view, options) => {
740
+ controlsRef.current?.setView(view, options);
741
+ },
742
+ reset: () => {
743
+ controlsRef.current?.reset();
744
+ },
745
+ startAutoRotate: () => {
746
+ setLegacyAutoRotate(true);
747
+ },
748
+ stopAutoRotate: () => {
749
+ setLegacyAutoRotate(false);
750
+ },
751
+ toggleFullscreen: async () => {
752
+ if (typeof document === "undefined") {
753
+ return;
754
+ }
755
+ if (document.fullscreenElement) {
756
+ await document.exitFullscreen?.();
757
+ return;
758
+ }
759
+ await rootRef.current?.requestFullscreen?.();
760
+ }
761
+ }),
762
+ []
763
+ );
764
+ const rootStyle = useMemo2(
765
+ () => ({
766
+ overflow: "hidden",
767
+ position: "relative",
768
+ ...style
769
+ }),
770
+ [style]
771
+ );
772
+ return /* @__PURE__ */ jsxs2(
773
+ "div",
774
+ {
775
+ ...divProps,
776
+ ref: rootRef,
777
+ "aria-label": ariaLabel,
778
+ style: rootStyle,
779
+ children: [
780
+ /* @__PURE__ */ jsx3(
781
+ Canvas,
782
+ {
783
+ "aria-label": `${ariaLabel} canvas`,
784
+ camera: {
785
+ far: DEFAULT_PANORAMA_CAMERA_FAR,
786
+ fov: normalizedInitialView.fov,
787
+ near: DEFAULT_PANORAMA_CAMERA_NEAR,
788
+ position: [0, 0, 0.01]
789
+ },
790
+ dpr,
791
+ gl: {
792
+ // Hotspots contain transparent textures and curved outlines. Keep
793
+ // the canvas multisampled so their visible edges are smoothed.
794
+ antialias: true,
795
+ powerPreference: "high-performance",
796
+ stencil: false
797
+ },
798
+ style: {
799
+ ...DEFAULT_CANVAS_STYLE,
800
+ touchAction: controlsEnabled ? "none" : "auto"
801
+ },
802
+ tabIndex: controlsEnabled ? 0 : void 0,
803
+ children: /* @__PURE__ */ jsx3(HotspotAccessibilityContext.Provider, { value: registry, children: /* @__PURE__ */ jsxs2(PanoramaControlsContext.Provider, { value: controlsRef, children: [
804
+ /* @__PURE__ */ jsx3(
805
+ PanoramaControls,
806
+ {
807
+ ref: controlsRef,
808
+ enabled: controlsEnabled,
809
+ initialView: normalizedInitialView,
810
+ maxFov: normalizedMaxFov,
811
+ minFov: normalizedMinFov,
812
+ onViewChange,
813
+ options: controlOptions
814
+ }
815
+ ),
816
+ /* @__PURE__ */ jsx3(
817
+ AutoRotate,
818
+ {
819
+ enabled: legacyAutoRotate,
820
+ speed: legacyAutoRotateOptions.autoRotateSpeed
821
+ }
822
+ ),
823
+ /* @__PURE__ */ jsx3(
824
+ PanoramaEventSurface,
825
+ {
826
+ onClick: onPanoramaClick,
827
+ onDoubleClick: onPanoramaDoubleClick,
828
+ onPointerMove: onPanoramaPointerMove
829
+ }
830
+ ),
831
+ children
832
+ ] }) })
833
+ }
834
+ ),
835
+ hotspotAccessibilityControls
836
+ ]
837
+ }
838
+ );
839
+ }
840
+ );
841
+
842
+ // src/sphere.tsx
843
+ import { useThree as useThree2 } from "@react-three/fiber";
844
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState3 } from "react";
845
+ import {
846
+ BackSide as BackSide2,
847
+ LinearFilter,
848
+ LinearMipmapLinearFilter,
849
+ MathUtils as MathUtils3,
850
+ RepeatWrapping,
851
+ SRGBColorSpace,
852
+ TextureLoader
853
+ } from "three";
854
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
855
+ function Sphere({
856
+ src,
857
+ yawOffset = 0,
858
+ onLoad,
859
+ onError,
860
+ visible = true
861
+ }) {
862
+ const gl = useThree2((state) => state.gl);
863
+ const [texture, setTexture] = useState3(null);
864
+ const onLoadRef = useRef5(onLoad);
865
+ const onErrorRef = useRef5(onError);
866
+ onLoadRef.current = onLoad;
867
+ onErrorRef.current = onError;
868
+ useEffect5(() => {
869
+ let active = true;
870
+ const loader = new TextureLoader();
871
+ setTexture(null);
872
+ loader.load(
873
+ src,
874
+ (loadedTexture) => {
875
+ if (!active) {
876
+ loadedTexture.dispose();
877
+ return;
878
+ }
879
+ loadedTexture.colorSpace = SRGBColorSpace;
880
+ loadedTexture.wrapS = RepeatWrapping;
881
+ loadedTexture.repeat.x = -1;
882
+ loadedTexture.offset.x = 1;
883
+ loadedTexture.anisotropy = gl.capabilities.getMaxAnisotropy();
884
+ loadedTexture.magFilter = LinearFilter;
885
+ loadedTexture.minFilter = LinearMipmapLinearFilter;
886
+ loadedTexture.generateMipmaps = true;
887
+ loadedTexture.needsUpdate = true;
888
+ setTexture(loadedTexture);
889
+ onLoadRef.current?.(loadedTexture);
890
+ },
891
+ void 0,
892
+ (error) => {
893
+ if (active) {
894
+ onErrorRef.current?.(error);
895
+ }
896
+ }
897
+ );
898
+ return () => {
899
+ active = false;
900
+ };
901
+ }, [gl, src]);
902
+ useEffect5(
903
+ () => () => {
904
+ texture?.dispose();
905
+ },
906
+ [texture]
907
+ );
908
+ if (!texture) {
909
+ return null;
910
+ }
911
+ return /* @__PURE__ */ jsxs3(
912
+ "mesh",
913
+ {
914
+ rotation: [0, MathUtils3.degToRad(90 + yawOffset), 0],
915
+ visible,
916
+ children: [
917
+ /* @__PURE__ */ jsx4("sphereGeometry", { args: [DEFAULT_PANORAMA_RADIUS, 128, 64] }),
918
+ /* @__PURE__ */ jsx4(
919
+ "meshBasicMaterial",
920
+ {
921
+ map: texture,
922
+ side: BackSide2,
923
+ toneMapped: false
924
+ }
925
+ )
926
+ ]
927
+ }
928
+ );
929
+ }
930
+
931
+ // src/panorama-scenes.tsx
932
+ import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
933
+ import {
934
+ Fragment,
935
+ useCallback as useCallback2,
936
+ useContext as useContext4,
937
+ useEffect as useEffect8,
938
+ useMemo as useMemo5,
939
+ useRef as useRef7,
940
+ useState as useState5
941
+ } from "react";
942
+ import {
943
+ DoubleSide,
944
+ LinearSRGBColorSpace,
945
+ LinearFilter as LinearFilter4,
946
+ PlaneGeometry as PlaneGeometry2,
947
+ ShaderMaterial,
948
+ Vector3 as Vector34,
949
+ WebGLRenderTarget
950
+ } from "three";
951
+
952
+ // src/tile/texture-manager-context.tsx
953
+ import { useThree as useThree3 } from "@react-three/fiber";
954
+ import {
955
+ createContext as createContext3,
956
+ useContext as useContext3,
957
+ useEffect as useEffect6,
958
+ useMemo as useMemo3
959
+ } from "react";
960
+
961
+ // src/tile/texture-manager.ts
962
+ import {
963
+ ClampToEdgeWrapping,
964
+ LinearFilter as LinearFilter2,
965
+ SRGBColorSpace as SRGBColorSpace2,
966
+ TextureLoader as TextureLoader2
967
+ } from "three";
968
+ var EMPTY_TEXTURE_SNAPSHOT = {
969
+ status: "idle",
970
+ texture: null,
971
+ error: null
972
+ };
973
+ var TileTextureManager = class {
974
+ constructor(options) {
975
+ this.loader = new TextureLoader2();
976
+ this.entries = /* @__PURE__ */ new Map();
977
+ this.queue = [];
978
+ this.activeLoads = 0;
979
+ this.totalBytes = 0;
980
+ this.requested = 0;
981
+ this.loaded = 0;
982
+ this.failed = 0;
983
+ this.disposed = false;
984
+ this.generation = 0;
985
+ this.options = options;
986
+ }
987
+ resume() {
988
+ this.disposed = false;
989
+ this.drain();
990
+ }
991
+ getSnapshot(key) {
992
+ const entry = this.entries.get(key);
993
+ return entry ? this.snapshot(entry) : EMPTY_TEXTURE_SNAPSHOT;
994
+ }
995
+ acquire(key, url, address, listener) {
996
+ let entry = this.entries.get(key);
997
+ if (!entry) {
998
+ entry = {
999
+ key,
1000
+ url,
1001
+ address,
1002
+ status: "idle",
1003
+ texture: null,
1004
+ error: null,
1005
+ refs: 0,
1006
+ listeners: /* @__PURE__ */ new Set(),
1007
+ lastUsed: performance.now(),
1008
+ bytes: 0
1009
+ };
1010
+ this.entries.set(key, entry);
1011
+ }
1012
+ entry.refs += 1;
1013
+ entry.lastUsed = performance.now();
1014
+ entry.listeners.add(listener);
1015
+ listener(this.snapshot(entry));
1016
+ if (entry.status === "idle") {
1017
+ entry.status = "queued";
1018
+ this.queue.push(entry);
1019
+ this.requested += 1;
1020
+ this.emitProgress();
1021
+ this.drain();
1022
+ }
1023
+ return () => {
1024
+ entry.listeners.delete(listener);
1025
+ entry.refs = Math.max(0, entry.refs - 1);
1026
+ entry.lastUsed = performance.now();
1027
+ if (entry.refs === 0 && entry.status === "queued") {
1028
+ const queueIndex = this.queue.indexOf(entry);
1029
+ if (queueIndex >= 0) {
1030
+ this.queue.splice(queueIndex, 1);
1031
+ }
1032
+ entry.status = "idle";
1033
+ this.entries.delete(entry.key);
1034
+ this.requested = Math.max(0, this.requested - 1);
1035
+ this.emitProgress();
1036
+ this.drain();
1037
+ return;
1038
+ }
1039
+ this.evict();
1040
+ };
1041
+ }
1042
+ dispose() {
1043
+ this.disposed = true;
1044
+ this.generation += 1;
1045
+ this.queue.length = 0;
1046
+ for (const entry of this.entries.values()) {
1047
+ entry.texture?.dispose();
1048
+ entry.listeners.clear();
1049
+ }
1050
+ this.entries.clear();
1051
+ this.totalBytes = 0;
1052
+ }
1053
+ /**
1054
+ * Releases textures that are no longer rendered by any Tile instance.
1055
+ * Scene transitions call this after the outgoing panorama has been
1056
+ * snapshotted, so the incoming scene never competes with stale GPU tiles.
1057
+ */
1058
+ releaseUnused() {
1059
+ for (const entry of Array.from(this.entries.values())) {
1060
+ if (entry.refs > 0) {
1061
+ continue;
1062
+ }
1063
+ entry.texture?.dispose();
1064
+ this.totalBytes -= entry.bytes;
1065
+ this.entries.delete(entry.key);
1066
+ }
1067
+ this.totalBytes = Math.max(0, this.totalBytes);
1068
+ }
1069
+ snapshot(entry) {
1070
+ return {
1071
+ status: entry.status,
1072
+ texture: entry.texture,
1073
+ error: entry.error
1074
+ };
1075
+ }
1076
+ notify(entry) {
1077
+ const snapshot = this.snapshot(entry);
1078
+ for (const listener of entry.listeners) {
1079
+ listener(snapshot);
1080
+ }
1081
+ }
1082
+ emitProgress() {
1083
+ this.options.onProgress?.({
1084
+ requested: this.requested,
1085
+ loaded: this.loaded,
1086
+ failed: this.failed,
1087
+ active: this.activeLoads,
1088
+ queued: this.queue.length
1089
+ });
1090
+ }
1091
+ drain() {
1092
+ while (!this.disposed && this.activeLoads < this.options.concurrency && this.queue.length > 0) {
1093
+ const entry = this.queue.shift();
1094
+ if (entry.refs === 0) {
1095
+ entry.status = "idle";
1096
+ this.entries.delete(entry.key);
1097
+ this.requested = Math.max(0, this.requested - 1);
1098
+ continue;
1099
+ }
1100
+ entry.status = "loading";
1101
+ this.activeLoads += 1;
1102
+ this.notify(entry);
1103
+ this.emitProgress();
1104
+ void this.loadAndFinish(entry);
1105
+ }
1106
+ }
1107
+ async load(entry, generation) {
1108
+ let lastError = null;
1109
+ for (let attempt = 0; attempt <= this.options.retryCount; attempt += 1) {
1110
+ try {
1111
+ const texture = await this.loader.loadAsync(entry.url);
1112
+ if (this.disposed || generation !== this.generation) {
1113
+ texture.dispose();
1114
+ return;
1115
+ }
1116
+ this.configureTexture(texture);
1117
+ entry.texture = texture;
1118
+ entry.status = "loaded";
1119
+ entry.error = null;
1120
+ entry.bytes = entry.address.width * entry.address.height * 4;
1121
+ entry.lastUsed = performance.now();
1122
+ this.totalBytes += entry.bytes;
1123
+ this.loaded += 1;
1124
+ this.notify(entry);
1125
+ return;
1126
+ } catch (error) {
1127
+ lastError = error;
1128
+ }
1129
+ }
1130
+ if (!this.disposed && generation === this.generation) {
1131
+ entry.status = "error";
1132
+ entry.error = lastError;
1133
+ this.failed += 1;
1134
+ this.notify(entry);
1135
+ this.options.onError?.({
1136
+ address: entry.address,
1137
+ url: entry.url,
1138
+ error: lastError
1139
+ });
1140
+ }
1141
+ }
1142
+ configureTexture(texture) {
1143
+ texture.colorSpace = SRGBColorSpace2;
1144
+ texture.wrapS = ClampToEdgeWrapping;
1145
+ texture.wrapT = ClampToEdgeWrapping;
1146
+ texture.magFilter = LinearFilter2;
1147
+ texture.minFilter = LinearFilter2;
1148
+ texture.generateMipmaps = false;
1149
+ texture.anisotropy = Math.min(4, this.options.anisotropy);
1150
+ texture.needsUpdate = true;
1151
+ }
1152
+ evict() {
1153
+ if (this.totalBytes <= this.options.maxBytes) {
1154
+ return;
1155
+ }
1156
+ const candidates = Array.from(this.entries.values()).filter((entry) => entry.refs === 0 && entry.texture).sort((a, b) => a.lastUsed - b.lastUsed);
1157
+ for (const entry of candidates) {
1158
+ if (this.totalBytes <= this.options.maxBytes) {
1159
+ break;
1160
+ }
1161
+ entry.texture?.dispose();
1162
+ this.totalBytes -= entry.bytes;
1163
+ this.entries.delete(entry.key);
1164
+ }
1165
+ this.totalBytes = Math.max(0, this.totalBytes);
1166
+ }
1167
+ finishLoad(generation) {
1168
+ this.activeLoads = Math.max(0, this.activeLoads - 1);
1169
+ if (!this.disposed && generation === this.generation) {
1170
+ this.emitProgress();
1171
+ this.evict();
1172
+ }
1173
+ if (!this.disposed) {
1174
+ this.drain();
1175
+ }
1176
+ }
1177
+ async loadAndFinish(entry) {
1178
+ const generation = this.generation;
1179
+ try {
1180
+ await this.load(entry, generation);
1181
+ } finally {
1182
+ this.finishLoad(generation);
1183
+ }
1184
+ }
1185
+ };
1186
+
1187
+ // src/tile/texture-manager-context.tsx
1188
+ import { jsx as jsx5 } from "react/jsx-runtime";
1189
+ var TileTextureManagerContext = createContext3(
1190
+ null
1191
+ );
1192
+ function TileTextureManagerProvider({
1193
+ children,
1194
+ maxTextureMemoryMb = 128,
1195
+ maxConcurrentLoads = 4
1196
+ }) {
1197
+ const gl = useThree3((state) => state.gl);
1198
+ const anisotropy = gl.capabilities.getMaxAnisotropy();
1199
+ const manager = useMemo3(
1200
+ () => new TileTextureManager({
1201
+ anisotropy,
1202
+ maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
1203
+ concurrency: Math.max(1, Math.floor(maxConcurrentLoads)),
1204
+ retryCount: 1
1205
+ }),
1206
+ [anisotropy, maxConcurrentLoads, maxTextureMemoryMb]
1207
+ );
1208
+ useEffect6(() => {
1209
+ manager.resume();
1210
+ return () => manager.dispose();
1211
+ }, [manager]);
1212
+ return /* @__PURE__ */ jsx5(TileTextureManagerContext.Provider, { value: manager, children });
1213
+ }
1214
+ function useSharedTileTextureManager() {
1215
+ return useContext3(TileTextureManagerContext);
1216
+ }
1217
+
1218
+ // src/tile/tile.tsx
1219
+ import { useFrame as useFrame3, useThree as useThree4 } from "@react-three/fiber";
1220
+ import {
1221
+ memo,
1222
+ useEffect as useEffect7,
1223
+ useMemo as useMemo4,
1224
+ useRef as useRef6,
1225
+ useState as useState4
1226
+ } from "react";
1227
+ import {
1228
+ ClampToEdgeWrapping as ClampToEdgeWrapping2,
1229
+ Frustum,
1230
+ LinearFilter as LinearFilter3,
1231
+ Matrix4,
1232
+ PlaneGeometry,
1233
+ PerspectiveCamera as PerspectiveCamera2,
1234
+ SRGBColorSpace as SRGBColorSpace3,
1235
+ TextureLoader as TextureLoader3,
1236
+ Vector3 as Vector33
1237
+ } from "three";
1238
+
1239
+ // src/tile/face-layout.ts
1240
+ import {
1241
+ Box3,
1242
+ Euler as Euler2,
1243
+ Quaternion,
1244
+ Vector3 as Vector32
1245
+ } from "three";
1246
+ var CUBE_FACES = ["f", "r", "b", "l", "u", "d"];
1247
+ var PREVIEW_FACE_ORDER = ["f", "r", "b", "l", "u", "d"];
1248
+ var CUBE_RADIUS = DEFAULT_PANORAMA_RADIUS;
1249
+ function getFaceTransform(face, radius = CUBE_RADIUS) {
1250
+ switch (face) {
1251
+ case "f":
1252
+ return { position: [0, 0, -radius], rotation: [0, 0, 0] };
1253
+ case "b":
1254
+ return { position: [0, 0, radius], rotation: [0, Math.PI, 0] };
1255
+ case "r":
1256
+ return {
1257
+ position: [radius, 0, 0],
1258
+ rotation: [0, -Math.PI / 2, 0]
1259
+ };
1260
+ case "l":
1261
+ return {
1262
+ position: [-radius, 0, 0],
1263
+ rotation: [0, Math.PI / 2, 0]
1264
+ };
1265
+ case "u":
1266
+ return {
1267
+ position: [0, radius, 0],
1268
+ rotation: [Math.PI / 2, 0, 0]
1269
+ };
1270
+ case "d":
1271
+ return {
1272
+ position: [0, -radius, 0],
1273
+ rotation: [-Math.PI / 2, 0, 0]
1274
+ };
1275
+ }
1276
+ }
1277
+ function getTileLocalLayout(address, radius = CUBE_RADIUS) {
1278
+ const left = (address.col - 1) * address.tileSize;
1279
+ const top = (address.row - 1) * address.tileSize;
1280
+ const right = left + address.width;
1281
+ const bottom = top + address.height;
1282
+ return {
1283
+ x: ((left + right) / address.faceSize - 1) * radius,
1284
+ y: (1 - (top + bottom) / address.faceSize) * radius,
1285
+ width: address.width / address.faceSize * radius * 2,
1286
+ height: address.height / address.faceSize * radius * 2
1287
+ };
1288
+ }
1289
+ var faceQuaternionCache = /* @__PURE__ */ new Map();
1290
+ function getFaceQuaternion(face) {
1291
+ const cached = faceQuaternionCache.get(face);
1292
+ if (cached) {
1293
+ return cached;
1294
+ }
1295
+ const transform = getFaceTransform(face);
1296
+ const quaternion = new Quaternion().setFromEuler(
1297
+ new Euler2(...transform.rotation, "XYZ")
1298
+ );
1299
+ faceQuaternionCache.set(face, quaternion);
1300
+ return quaternion;
1301
+ }
1302
+ function getTileBoundingBox(address, radius = CUBE_RADIUS) {
1303
+ const transform = getFaceTransform(address.face, radius);
1304
+ const local = getTileLocalLayout(address, radius);
1305
+ const halfWidth = local.width / 2;
1306
+ const halfHeight = local.height / 2;
1307
+ const quaternion = getFaceQuaternion(address.face);
1308
+ const position = new Vector32(...transform.position);
1309
+ const corners = [
1310
+ new Vector32(local.x - halfWidth, local.y - halfHeight, 0),
1311
+ new Vector32(local.x + halfWidth, local.y - halfHeight, 0),
1312
+ new Vector32(local.x - halfWidth, local.y + halfHeight, 0),
1313
+ new Vector32(local.x + halfWidth, local.y + halfHeight, 0)
1314
+ ];
1315
+ for (const corner of corners) {
1316
+ corner.applyQuaternion(quaternion).add(position);
1317
+ }
1318
+ return new Box3().setFromPoints(corners);
1319
+ }
1320
+ function makeTileAddress(face, level, faceSize, tileSize, col, row) {
1321
+ const left = (col - 1) * tileSize;
1322
+ const top = (row - 1) * tileSize;
1323
+ return {
1324
+ face,
1325
+ level,
1326
+ row,
1327
+ col,
1328
+ faceSize,
1329
+ tileSize,
1330
+ width: Math.min(tileSize, faceSize - left),
1331
+ height: Math.min(tileSize, faceSize - top)
1332
+ };
1333
+ }
1334
+ function tileKey(address) {
1335
+ return `${address.level}/${address.face}/${address.row}/${address.col}`;
1336
+ }
1337
+ function getPreferredLevel(levels, viewportHeight, pixelRatio, verticalFov, currentLevel) {
1338
+ const requiredFaceSize = viewportHeight * pixelRatio * (90 / Math.max(verticalFov, 1));
1339
+ let preferredLevel = levels.length;
1340
+ for (let index = 0; index < levels.length; index += 1) {
1341
+ if (levels[index] >= requiredFaceSize) {
1342
+ preferredLevel = index + 1;
1343
+ break;
1344
+ }
1345
+ }
1346
+ if (preferredLevel > currentLevel) {
1347
+ const currentSize = levels[currentLevel - 1] ?? levels[0];
1348
+ return requiredFaceSize > currentSize * 1.2 ? preferredLevel : currentLevel;
1349
+ }
1350
+ if (preferredLevel < currentLevel) {
1351
+ const preferredSize = levels[preferredLevel - 1] ?? levels[0];
1352
+ return requiredFaceSize < preferredSize * 0.8 ? preferredLevel : currentLevel;
1353
+ }
1354
+ return currentLevel;
1355
+ }
1356
+ function previewAtlasVRange(face) {
1357
+ const index = PREVIEW_FACE_ORDER.indexOf(face);
1358
+ return {
1359
+ offset: (PREVIEW_FACE_ORDER.length - index - 1) / PREVIEW_FACE_ORDER.length,
1360
+ scale: 1 / PREVIEW_FACE_ORDER.length
1361
+ };
1362
+ }
1363
+
1364
+ // src/tile/multires.ts
1365
+ function positiveInteger(value, name) {
1366
+ if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
1367
+ throw new Error(`${name} must be a positive integer, received ${String(value)}`);
1368
+ }
1369
+ return value;
1370
+ }
1371
+ function ascendingLevels(levels) {
1372
+ for (let index = 1; index < levels.length; index += 1) {
1373
+ if (levels[index] <= levels[index - 1]) {
1374
+ throw new Error("multires face sizes must be strictly ascending");
1375
+ }
1376
+ }
1377
+ return levels;
1378
+ }
1379
+ function parseMultires(multires) {
1380
+ if (typeof multires !== "string") {
1381
+ const tileSize2 = positiveInteger(multires.tileSize, "multires.tileSize");
1382
+ if (multires.levels.length === 0) {
1383
+ throw new Error("multires.levels must contain at least one face size");
1384
+ }
1385
+ const levels2 = ascendingLevels(
1386
+ multires.levels.map(
1387
+ (level, index) => positiveInteger(level, `multires.levels[${index}]`)
1388
+ )
1389
+ );
1390
+ return { tileSize: tileSize2, levels: levels2 };
1391
+ }
1392
+ const values = multires.split(",").map((part) => Number(part.trim()));
1393
+ if (values.length < 2 || values.some((value) => !Number.isFinite(value))) {
1394
+ throw new Error(
1395
+ "multires must contain a tile size followed by at least one face size"
1396
+ );
1397
+ }
1398
+ const tileSize = positiveInteger(values[0], "multires tile size");
1399
+ const levels = ascendingLevels(
1400
+ values.slice(1).map(
1401
+ (level, index) => positiveInteger(level, `multires level ${index + 1}`)
1402
+ )
1403
+ );
1404
+ return { tileSize, levels };
1405
+ }
1406
+ function buildDefaultTileUrlTemplate() {
1407
+ return "tiles/%s/l%l/%v/l%l_%s_%h_%v.webp";
1408
+ }
1409
+ function resolveRelativeTileUrl(baseUrl, relativePath) {
1410
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
1411
+ const normalizedPath = relativePath.replace(/^\/+/, "");
1412
+ return normalizedBaseUrl ? `${normalizedBaseUrl}/${normalizedPath}` : normalizedPath;
1413
+ }
1414
+ function resolveTemplateUrl(template, address) {
1415
+ return template.replace(/%s/g, address.face).replace(/%l/g, String(address.level)).replace(/%(0*)([hxucvywr])/g, (_match, zeroes, placeholder) => {
1416
+ const value = "hxuc".includes(placeholder) ? address.col : address.row;
1417
+ return String(value).padStart(zeroes.length + 1, "0");
1418
+ });
1419
+ }
1420
+
1421
+ // src/tile/tile.tsx
1422
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1423
+ var MAX_LOD_UPDATE_INTERVAL_MS = 100;
1424
+ var FAST_ROTATION_THRESHOLD_RADIANS = Math.PI / 180;
1425
+ var FAST_FOV_THRESHOLD_DEGREES = 0.5;
1426
+ var VIEW_CHANGE_EPSILON = 1e-4;
1427
+ var VISIBILITY_FOV_PADDING_DEGREES = 8;
1428
+ var PREVIEW_RADIUS = CUBE_RADIUS + 0.05;
1429
+ var SHARED_TILE_GEOMETRY = new PlaneGeometry(1, 1);
1430
+ function usePreviewTexture(url, anisotropy, onError) {
1431
+ const [texture, setTexture] = useState4(null);
1432
+ const onErrorRef = useRef6(onError);
1433
+ onErrorRef.current = onError;
1434
+ useEffect7(() => {
1435
+ if (!url) {
1436
+ setTexture(null);
1437
+ return;
1438
+ }
1439
+ let active = true;
1440
+ const loader = new TextureLoader3();
1441
+ setTexture(null);
1442
+ loader.load(
1443
+ url,
1444
+ (loadedTexture) => {
1445
+ if (!active) {
1446
+ loadedTexture.dispose();
1447
+ return;
1448
+ }
1449
+ loadedTexture.colorSpace = SRGBColorSpace3;
1450
+ loadedTexture.wrapS = ClampToEdgeWrapping2;
1451
+ loadedTexture.wrapT = ClampToEdgeWrapping2;
1452
+ loadedTexture.magFilter = LinearFilter3;
1453
+ loadedTexture.minFilter = LinearFilter3;
1454
+ loadedTexture.generateMipmaps = false;
1455
+ loadedTexture.anisotropy = Math.min(4, anisotropy);
1456
+ loadedTexture.needsUpdate = true;
1457
+ setTexture(loadedTexture);
1458
+ },
1459
+ void 0,
1460
+ (error) => {
1461
+ if (active) {
1462
+ setTexture(null);
1463
+ onErrorRef.current?.(error);
1464
+ }
1465
+ }
1466
+ );
1467
+ return () => {
1468
+ active = false;
1469
+ };
1470
+ }, [anisotropy, url]);
1471
+ useEffect7(
1472
+ () => () => {
1473
+ texture?.dispose();
1474
+ },
1475
+ [texture]
1476
+ );
1477
+ return texture;
1478
+ }
1479
+ function PreviewFace({
1480
+ face,
1481
+ texture
1482
+ }) {
1483
+ const geometry = useMemo4(() => {
1484
+ const nextGeometry = new PlaneGeometry(
1485
+ PREVIEW_RADIUS * 2,
1486
+ PREVIEW_RADIUS * 2
1487
+ );
1488
+ const uv = nextGeometry.getAttribute("uv");
1489
+ const { offset, scale } = previewAtlasVRange(face);
1490
+ const inset = scale / 512;
1491
+ for (let index = 0; index < uv.count; index += 1) {
1492
+ const sourceV = uv.getY(index);
1493
+ uv.setY(
1494
+ index,
1495
+ offset + inset + sourceV * Math.max(0, scale - inset * 2)
1496
+ );
1497
+ }
1498
+ uv.needsUpdate = true;
1499
+ return nextGeometry;
1500
+ }, [face]);
1501
+ useEffect7(
1502
+ () => () => {
1503
+ geometry.dispose();
1504
+ },
1505
+ [geometry]
1506
+ );
1507
+ const transform = getFaceTransform(face, PREVIEW_RADIUS);
1508
+ return /* @__PURE__ */ jsx6(
1509
+ "mesh",
1510
+ {
1511
+ geometry,
1512
+ position: transform.position,
1513
+ renderOrder: -100,
1514
+ rotation: transform.rotation,
1515
+ children: /* @__PURE__ */ jsx6(
1516
+ "meshBasicMaterial",
1517
+ {
1518
+ depthTest: false,
1519
+ depthWrite: false,
1520
+ map: texture,
1521
+ toneMapped: false
1522
+ }
1523
+ )
1524
+ }
1525
+ );
1526
+ }
1527
+ function PreviewCube({ texture }) {
1528
+ return CUBE_FACES.map((face) => /* @__PURE__ */ jsx6(PreviewFace, { face, texture }, face));
1529
+ }
1530
+ function useManagedTexture(manager, key, url, address) {
1531
+ const [managedState, setManagedState] = useState4(() => ({
1532
+ manager,
1533
+ key,
1534
+ url,
1535
+ snapshot: manager.getSnapshot(key)
1536
+ }));
1537
+ const snapshot = managedState.manager === manager && managedState.key === key && managedState.url === url ? managedState.snapshot : manager.getSnapshot(key);
1538
+ useEffect7(
1539
+ () => manager.acquire(key, url, address, (nextSnapshot) => {
1540
+ setManagedState({
1541
+ manager,
1542
+ key,
1543
+ url,
1544
+ snapshot: nextSnapshot
1545
+ });
1546
+ }),
1547
+ [key, manager, url]
1548
+ );
1549
+ return snapshot;
1550
+ }
1551
+ var TileMesh = memo(function TileMesh2({
1552
+ address,
1553
+ manager,
1554
+ renderOrder,
1555
+ resolveUrl
1556
+ }) {
1557
+ const url = resolveUrl(address);
1558
+ const cacheKey = `${url}\0${tileKey(address)}`;
1559
+ const snapshot = useManagedTexture(manager, cacheKey, url, address);
1560
+ const layout = getTileLocalLayout(address);
1561
+ const transform = getFaceTransform(address.face);
1562
+ if (!snapshot.texture) {
1563
+ return null;
1564
+ }
1565
+ return /* @__PURE__ */ jsx6("group", { position: transform.position, rotation: transform.rotation, children: /* @__PURE__ */ jsx6(
1566
+ "mesh",
1567
+ {
1568
+ geometry: SHARED_TILE_GEOMETRY,
1569
+ position: [layout.x, layout.y, 0],
1570
+ renderOrder,
1571
+ scale: [layout.width, layout.height, 1],
1572
+ children: /* @__PURE__ */ jsx6(
1573
+ "meshBasicMaterial",
1574
+ {
1575
+ depthTest: false,
1576
+ depthWrite: false,
1577
+ map: snapshot.texture,
1578
+ toneMapped: false
1579
+ }
1580
+ )
1581
+ }
1582
+ ) });
1583
+ });
1584
+ var TileLayerMeshes = memo(function TileLayerMeshes2({
1585
+ layer,
1586
+ manager,
1587
+ resolveUrl
1588
+ }) {
1589
+ return layer.addresses.map((address) => /* @__PURE__ */ jsx6(
1590
+ TileMesh,
1591
+ {
1592
+ address,
1593
+ manager,
1594
+ renderOrder: layer.renderOrder,
1595
+ resolveUrl
1596
+ },
1597
+ tileKey(address)
1598
+ ));
1599
+ });
1600
+ function buildTileCandidates(levels, tileSize) {
1601
+ return levels.map((faceSize, index) => {
1602
+ const level = index + 1;
1603
+ const cols = Math.ceil(faceSize / tileSize);
1604
+ const rows = Math.ceil(faceSize / tileSize);
1605
+ const candidates = [];
1606
+ for (const face of CUBE_FACES) {
1607
+ for (let row = 1; row <= rows; row += 1) {
1608
+ for (let col = 1; col <= cols; col += 1) {
1609
+ const address = makeTileAddress(
1610
+ face,
1611
+ level,
1612
+ faceSize,
1613
+ tileSize,
1614
+ col,
1615
+ row
1616
+ );
1617
+ candidates.push({
1618
+ address,
1619
+ bounds: getTileBoundingBox(address)
1620
+ });
1621
+ }
1622
+ }
1623
+ }
1624
+ return candidates;
1625
+ });
1626
+ }
1627
+ function visibleAddresses(candidates, frustum) {
1628
+ return candidates.filter((candidate) => frustum.intersectsBox(candidate.bounds)).map((candidate) => candidate.address);
1629
+ }
1630
+ function updateVisibilityFrustum(camera, projectionMatrix, frustum) {
1631
+ camera.updateProjectionMatrix();
1632
+ camera.updateMatrixWorld();
1633
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
1634
+ projectionMatrix.multiplyMatrices(
1635
+ camera.projectionMatrix,
1636
+ camera.matrixWorldInverse
1637
+ );
1638
+ frustum.setFromProjectionMatrix(projectionMatrix);
1639
+ }
1640
+ function addAddress(addressesByLevel, address) {
1641
+ let levelAddresses = addressesByLevel.get(address.level);
1642
+ if (!levelAddresses) {
1643
+ levelAddresses = /* @__PURE__ */ new Map();
1644
+ addressesByLevel.set(address.level, levelAddresses);
1645
+ }
1646
+ levelAddresses.set(tileKey(address), address);
1647
+ }
1648
+ function parentAddresses(child, parentLevel, parentFaceSize, tileSize) {
1649
+ const childLeft = (child.col - 1) * child.tileSize / child.faceSize;
1650
+ const childTop = (child.row - 1) * child.tileSize / child.faceSize;
1651
+ const childRight = childLeft + child.width / child.faceSize;
1652
+ const childBottom = childTop + child.height / child.faceSize;
1653
+ const cols = Math.ceil(parentFaceSize / tileSize);
1654
+ const rows = Math.ceil(parentFaceSize / tileSize);
1655
+ const firstCol = Math.max(
1656
+ 1,
1657
+ Math.floor(childLeft * parentFaceSize / tileSize) + 1
1658
+ );
1659
+ const lastCol = Math.min(
1660
+ cols,
1661
+ Math.ceil(childRight * parentFaceSize / tileSize)
1662
+ );
1663
+ const firstRow = Math.max(
1664
+ 1,
1665
+ Math.floor(childTop * parentFaceSize / tileSize) + 1
1666
+ );
1667
+ const lastRow = Math.min(
1668
+ rows,
1669
+ Math.ceil(childBottom * parentFaceSize / tileSize)
1670
+ );
1671
+ const addresses = [];
1672
+ for (let row = firstRow; row <= lastRow; row += 1) {
1673
+ for (let col = firstCol; col <= lastCol; col += 1) {
1674
+ addresses.push(
1675
+ makeTileAddress(
1676
+ child.face,
1677
+ parentLevel,
1678
+ parentFaceSize,
1679
+ tileSize,
1680
+ col,
1681
+ row
1682
+ )
1683
+ );
1684
+ }
1685
+ }
1686
+ return addresses;
1687
+ }
1688
+ function centerFacingAddress(candidates, camera) {
1689
+ const direction = camera.getWorldDirection(new Vector33());
1690
+ const center = new Vector33();
1691
+ let bestCandidate = candidates[0];
1692
+ let bestAlignment = -Infinity;
1693
+ for (const candidate of candidates) {
1694
+ candidate.bounds.getCenter(center).normalize();
1695
+ const alignment = center.dot(direction);
1696
+ if (alignment > bestAlignment) {
1697
+ bestCandidate = candidate;
1698
+ bestAlignment = alignment;
1699
+ }
1700
+ }
1701
+ return bestCandidate.address;
1702
+ }
1703
+ function buildLayers(targetLevel, levels, tileSize, frustum, camera, candidatesByLevel) {
1704
+ const addressesByLevel = /* @__PURE__ */ new Map();
1705
+ const targetCandidates = candidatesByLevel[targetLevel - 1];
1706
+ const targetAddresses = visibleAddresses(targetCandidates, frustum);
1707
+ const visibleTargets = targetAddresses.length > 0 ? targetAddresses : [centerFacingAddress(targetCandidates, camera)];
1708
+ for (const address of visibleTargets) {
1709
+ addAddress(addressesByLevel, address);
1710
+ }
1711
+ let children = visibleTargets;
1712
+ for (let parentLevel = targetLevel - 1; parentLevel >= 1; parentLevel -= 1) {
1713
+ const parentFaceSize = levels[parentLevel - 1];
1714
+ const parents = /* @__PURE__ */ new Map();
1715
+ for (const child of children) {
1716
+ for (const parent of parentAddresses(
1717
+ child,
1718
+ parentLevel,
1719
+ parentFaceSize,
1720
+ tileSize
1721
+ )) {
1722
+ parents.set(tileKey(parent), parent);
1723
+ addAddress(addressesByLevel, parent);
1724
+ }
1725
+ }
1726
+ children = Array.from(parents.values());
1727
+ }
1728
+ return Array.from(addressesByLevel.entries()).sort(([firstLevel], [secondLevel]) => firstLevel - secondLevel).map(([level, addresses]) => ({
1729
+ level,
1730
+ addresses: Array.from(addresses.values()),
1731
+ renderOrder: -100 + level * 10
1732
+ }));
1733
+ }
1734
+ function layerToken(layers) {
1735
+ return layers.map(
1736
+ (layer) => `${layer.level}:${layer.addresses.map(tileKey).join(",")}`
1737
+ ).join("|");
1738
+ }
1739
+ function Tile({
1740
+ baseUrl,
1741
+ multires,
1742
+ previewUrl,
1743
+ urlTemplate,
1744
+ resolveTileUrl,
1745
+ maxTextureMemoryMb = 128,
1746
+ maxConcurrentLoads = 8,
1747
+ retryCount = 1,
1748
+ loadMode = "full",
1749
+ visible = true,
1750
+ onReady,
1751
+ onPreviewError,
1752
+ onLoadProgress,
1753
+ onTileError,
1754
+ onLevelChange
1755
+ }) {
1756
+ const { camera, gl, size } = useThree4();
1757
+ const multiresConfig = useMemo4(() => parseMultires(multires), [multires]);
1758
+ const { levels, tileSize } = multiresConfig;
1759
+ const multiresToken = `${tileSize}:${levels.join(",")}`;
1760
+ const candidatesByLevel = useMemo4(
1761
+ () => buildTileCandidates(levels, tileSize),
1762
+ [levels, tileSize]
1763
+ );
1764
+ const anisotropy = gl.capabilities.getMaxAnisotropy();
1765
+ const effectivePreviewUrl = previewUrl === void 0 ? `${baseUrl.replace(/\/$/, "")}/previews/cube-vertical.webp` : previewUrl;
1766
+ const previewTexture = usePreviewTexture(
1767
+ effectivePreviewUrl,
1768
+ anisotropy,
1769
+ onPreviewError
1770
+ );
1771
+ const sharedManager = useSharedTileTextureManager();
1772
+ const progressRef = useRef6(onLoadProgress);
1773
+ const errorRef = useRef6(onTileError);
1774
+ const levelChangeRef = useRef6(onLevelChange);
1775
+ progressRef.current = onLoadProgress;
1776
+ errorRef.current = onTileError;
1777
+ levelChangeRef.current = onLevelChange;
1778
+ const ownedManager = useMemo4(
1779
+ () => new TileTextureManager({
1780
+ anisotropy,
1781
+ maxBytes: Math.max(1, maxTextureMemoryMb) * 1024 * 1024,
1782
+ concurrency: Math.max(1, Math.floor(maxConcurrentLoads)),
1783
+ retryCount: Math.max(0, Math.floor(retryCount)),
1784
+ onProgress: (progress) => progressRef.current?.(progress),
1785
+ onError: (event) => errorRef.current?.(event)
1786
+ }),
1787
+ [
1788
+ anisotropy,
1789
+ baseUrl,
1790
+ maxConcurrentLoads,
1791
+ maxTextureMemoryMb,
1792
+ multiresToken,
1793
+ retryCount,
1794
+ resolveTileUrl,
1795
+ urlTemplate
1796
+ ]
1797
+ );
1798
+ const manager = sharedManager ?? ownedManager;
1799
+ useEffect7(() => {
1800
+ manager.resume();
1801
+ return () => {
1802
+ if (manager === ownedManager) {
1803
+ manager.dispose();
1804
+ return;
1805
+ }
1806
+ queueMicrotask(() => manager.releaseUnused());
1807
+ };
1808
+ }, [manager, ownedManager]);
1809
+ const readyRef = useRef6(false);
1810
+ useEffect7(() => {
1811
+ readyRef.current = false;
1812
+ }, [effectivePreviewUrl]);
1813
+ useEffect7(() => {
1814
+ if (!previewTexture || readyRef.current) {
1815
+ return;
1816
+ }
1817
+ readyRef.current = true;
1818
+ onReady?.();
1819
+ }, [onReady, previewTexture]);
1820
+ const template = urlTemplate ?? buildDefaultTileUrlTemplate();
1821
+ const resolveUrl = useMemo4(
1822
+ () => (address) => {
1823
+ const relativePath = resolveTileUrl ? resolveTileUrl(address) : resolveTemplateUrl(template, address);
1824
+ return resolveRelativeTileUrl(baseUrl, relativePath);
1825
+ },
1826
+ [baseUrl, resolveTileUrl, template]
1827
+ );
1828
+ const [layers, setLayers] = useState4([]);
1829
+ const layerTokenRef = useRef6("");
1830
+ const currentLevelRef = useRef6(1);
1831
+ const lastUpdateAtRef = useRef6(-Infinity);
1832
+ const visibilityDirtyRef = useRef6(true);
1833
+ const lastVisibilityQuaternionRef = useRef6(camera.quaternion.clone());
1834
+ const lastVisibilityFovRef = useRef6(Number.NaN);
1835
+ const lastViewportRef = useRef6({ width: 0, height: 0, dpr: 0 });
1836
+ const frustumRef = useRef6(new Frustum());
1837
+ const projectionMatrixRef = useRef6(new Matrix4());
1838
+ const visibilityCameraRef = useRef6(new PerspectiveCamera2());
1839
+ useEffect7(() => {
1840
+ currentLevelRef.current = 1;
1841
+ layerTokenRef.current = "";
1842
+ visibilityDirtyRef.current = true;
1843
+ setLayers([]);
1844
+ }, [manager, multiresToken]);
1845
+ useEffect7(() => {
1846
+ if (loadMode !== "preview") {
1847
+ return;
1848
+ }
1849
+ layerTokenRef.current = "";
1850
+ setLayers([]);
1851
+ }, [loadMode]);
1852
+ const commitLayers = (nextLayers) => {
1853
+ const nextToken = layerToken(nextLayers);
1854
+ if (nextToken !== layerTokenRef.current) {
1855
+ layerTokenRef.current = nextToken;
1856
+ setLayers(nextLayers);
1857
+ }
1858
+ };
1859
+ useFrame3(() => {
1860
+ if (loadMode === "preview") {
1861
+ return;
1862
+ }
1863
+ if (!(camera instanceof PerspectiveCamera2)) {
1864
+ return;
1865
+ }
1866
+ const now = performance.now();
1867
+ const pixelRatio = gl.getPixelRatio();
1868
+ const rotationDelta = lastVisibilityQuaternionRef.current.angleTo(
1869
+ camera.quaternion
1870
+ );
1871
+ const fovDelta = Number.isFinite(lastVisibilityFovRef.current) ? Math.abs(camera.fov - lastVisibilityFovRef.current) : Infinity;
1872
+ const lastViewport = lastViewportRef.current;
1873
+ const viewportChanged = lastViewport.width !== size.width || lastViewport.height !== size.height || lastViewport.dpr !== pixelRatio;
1874
+ const viewChanged = visibilityDirtyRef.current || viewportChanged || rotationDelta > VIEW_CHANGE_EPSILON || fovDelta > VIEW_CHANGE_EPSILON;
1875
+ const movingQuickly = rotationDelta >= FAST_ROTATION_THRESHOLD_RADIANS || fovDelta >= FAST_FOV_THRESHOLD_DEGREES;
1876
+ if (!viewChanged) {
1877
+ return;
1878
+ }
1879
+ const needsImmediateUpdate = visibilityDirtyRef.current || viewportChanged || movingQuickly;
1880
+ if (!needsImmediateUpdate && now - lastUpdateAtRef.current < MAX_LOD_UPDATE_INTERVAL_MS) {
1881
+ return;
1882
+ }
1883
+ lastUpdateAtRef.current = now;
1884
+ const targetLevel = loadMode === "base" ? 1 : getPreferredLevel(
1885
+ levels,
1886
+ size.height,
1887
+ pixelRatio,
1888
+ camera.fov,
1889
+ currentLevelRef.current
1890
+ );
1891
+ if (targetLevel !== currentLevelRef.current) {
1892
+ currentLevelRef.current = targetLevel;
1893
+ levelChangeRef.current?.(targetLevel);
1894
+ }
1895
+ const visibilityCamera = visibilityCameraRef.current;
1896
+ visibilityCamera.copy(camera, false);
1897
+ visibilityCamera.fov = Math.min(
1898
+ 179,
1899
+ camera.fov + VISIBILITY_FOV_PADDING_DEGREES
1900
+ );
1901
+ visibilityCamera.aspect = camera.aspect;
1902
+ updateVisibilityFrustum(
1903
+ visibilityCamera,
1904
+ projectionMatrixRef.current,
1905
+ frustumRef.current
1906
+ );
1907
+ const nextLayers = buildLayers(
1908
+ targetLevel,
1909
+ levels,
1910
+ tileSize,
1911
+ frustumRef.current,
1912
+ camera,
1913
+ candidatesByLevel
1914
+ );
1915
+ visibilityDirtyRef.current = false;
1916
+ lastVisibilityQuaternionRef.current.copy(camera.quaternion);
1917
+ lastVisibilityFovRef.current = camera.fov;
1918
+ lastViewportRef.current = {
1919
+ width: size.width,
1920
+ height: size.height,
1921
+ dpr: pixelRatio
1922
+ };
1923
+ commitLayers(nextLayers);
1924
+ });
1925
+ return /* @__PURE__ */ jsxs4("group", { visible, children: [
1926
+ previewTexture ? /* @__PURE__ */ jsx6(PreviewCube, { texture: previewTexture }) : null,
1927
+ layers.map((layer) => /* @__PURE__ */ jsx6(
1928
+ TileLayerMeshes,
1929
+ {
1930
+ layer,
1931
+ manager,
1932
+ resolveUrl
1933
+ },
1934
+ layer.level
1935
+ ))
1936
+ ] });
1937
+ }
1938
+
1939
+ // src/panorama-scenes.tsx
1940
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1941
+ var TRANSITION_DEFAULTS = {
1942
+ none: { duration: 0, krpanoBlend: "NOBLEND" },
1943
+ crossfade: { duration: 1, krpanoBlend: "BLEND(1.0, easeInCubic)" },
1944
+ zoom: { duration: 2, krpanoBlend: "ZOOMBLEND(2.0, 2.0, easeInOutSine)" },
1945
+ blackout: { duration: 2, krpanoBlend: "COLORBLEND(2.0, 0x000000, easeOutSine)" },
1946
+ whiteFlash: { duration: 1, krpanoBlend: "LIGHTBLEND(1.0, 0xFFFFFF, 2.0, linear)" },
1947
+ slideRightToLeft: { duration: 1, krpanoBlend: "SLIDEBLEND(1.0, 0.0, 0.2, linear)" },
1948
+ slideTopToBottom: { duration: 1, krpanoBlend: "SLIDEBLEND(1.0, 90.0, 0.01, linear)" },
1949
+ slideDiagonal: { duration: 1, krpanoBlend: "SLIDEBLEND(1.0, 135.0, 0.4, linear)" },
1950
+ circleOpen: { duration: 1, krpanoBlend: "OPENBLEND(1.0, 0.0, 0.2, 0.0, linear)" },
1951
+ verticalOpen: { duration: 0.7, krpanoBlend: "OPENBLEND(0.7, 1.0, 0.1, 0.0, linear)" },
1952
+ horizontalOpen: { duration: 1, krpanoBlend: "OPENBLEND(1.0, -1.0, 0.3, 0.0, linear)" },
1953
+ ellipticZoomOpen: { duration: 1, krpanoBlend: "OPENBLEND(1.0, -0.5, 0.3, 0.8, linear)" }
1954
+ };
1955
+ var EFFECT_INDEX = {
1956
+ none: 0,
1957
+ crossfade: 1,
1958
+ zoom: 2,
1959
+ blackout: 3,
1960
+ whiteFlash: 4,
1961
+ slideRightToLeft: 5,
1962
+ slideTopToBottom: 6,
1963
+ slideDiagonal: 7,
1964
+ circleOpen: 8,
1965
+ verticalOpen: 9,
1966
+ horizontalOpen: 10,
1967
+ ellipticZoomOpen: 11
1968
+ };
1969
+ function resolveTransition(transition) {
1970
+ const preset = typeof transition === "string" ? transition : transition?.preset ?? "crossfade";
1971
+ return {
1972
+ preset,
1973
+ duration: Math.max(
1974
+ 0,
1975
+ transition && typeof transition === "object" && Number.isFinite(transition.duration) ? transition.duration : TRANSITION_DEFAULTS[preset].duration
1976
+ )
1977
+ };
1978
+ }
1979
+ function SceneSource({
1980
+ scene,
1981
+ visible,
1982
+ baseOnly,
1983
+ onReady,
1984
+ onError
1985
+ }) {
1986
+ if (!scene) {
1987
+ return null;
1988
+ }
1989
+ if (scene.type === "sphere") {
1990
+ return /* @__PURE__ */ jsx7(
1991
+ Sphere,
1992
+ {
1993
+ src: scene.src,
1994
+ yawOffset: scene.yawOffset,
1995
+ visible,
1996
+ onLoad: onReady,
1997
+ onError
1998
+ }
1999
+ );
2000
+ }
2001
+ const { id: _id, type: _type, ...tileProps } = scene;
2002
+ return /* @__PURE__ */ jsx7(
2003
+ Tile,
2004
+ {
2005
+ ...tileProps,
2006
+ visible,
2007
+ loadMode: baseOnly ? "base" : "full",
2008
+ onReady,
2009
+ onPreviewError: onError
2010
+ }
2011
+ );
2012
+ }
2013
+ function SnapshotCapture({
2014
+ requestId,
2015
+ maxPixels,
2016
+ onCaptured
2017
+ }) {
2018
+ const { camera, gl, scene, size } = useThree5();
2019
+ const capturedRequestRef = useRef7(0);
2020
+ const renderTargetRef = useRef7(null);
2021
+ useEffect8(
2022
+ () => () => {
2023
+ renderTargetRef.current?.dispose();
2024
+ renderTargetRef.current = null;
2025
+ },
2026
+ []
2027
+ );
2028
+ useFrame4(() => {
2029
+ if (requestId === 0 || requestId === capturedRequestRef.current) {
2030
+ return;
2031
+ }
2032
+ const pixelRatio = gl.getPixelRatio();
2033
+ const sourceWidth = Math.max(1, Math.floor(size.width * pixelRatio));
2034
+ const sourceHeight = Math.max(1, Math.floor(size.height * pixelRatio));
2035
+ const scale = Math.min(1, Math.sqrt(maxPixels / (sourceWidth * sourceHeight)));
2036
+ const width = Math.max(1, Math.floor(sourceWidth * scale));
2037
+ const height = Math.max(1, Math.floor(sourceHeight * scale));
2038
+ const nextTarget = new WebGLRenderTarget(width, height, {
2039
+ depthBuffer: false,
2040
+ stencilBuffer: false
2041
+ });
2042
+ nextTarget.texture.colorSpace = LinearSRGBColorSpace;
2043
+ nextTarget.texture.generateMipmaps = false;
2044
+ nextTarget.texture.minFilter = LinearFilter4;
2045
+ nextTarget.texture.magFilter = LinearFilter4;
2046
+ const previousTarget = gl.getRenderTarget();
2047
+ gl.setRenderTarget(nextTarget);
2048
+ gl.clear();
2049
+ gl.render(scene, camera);
2050
+ gl.setRenderTarget(previousTarget);
2051
+ capturedRequestRef.current = requestId;
2052
+ const previous = renderTargetRef.current;
2053
+ renderTargetRef.current = nextTarget;
2054
+ previous?.dispose();
2055
+ onCaptured({
2056
+ texture: nextTarget.texture,
2057
+ dispose: () => {
2058
+ if (renderTargetRef.current === nextTarget) {
2059
+ renderTargetRef.current = null;
2060
+ }
2061
+ nextTarget.dispose();
2062
+ }
2063
+ });
2064
+ });
2065
+ return null;
2066
+ }
2067
+ function SnapshotOverlay({
2068
+ snapshot,
2069
+ transition,
2070
+ running,
2071
+ runId,
2072
+ onFinish
2073
+ }) {
2074
+ const { camera, size } = useThree5();
2075
+ const meshRef = useRef7(null);
2076
+ const startRef = useRef7(null);
2077
+ const finishedRef = useRef7(false);
2078
+ const directionRef = useRef7(new Vector34());
2079
+ const material = useMemo5(
2080
+ () => new ShaderMaterial({
2081
+ transparent: true,
2082
+ depthTest: false,
2083
+ depthWrite: false,
2084
+ side: DoubleSide,
2085
+ uniforms: {
2086
+ map: { value: snapshot.texture },
2087
+ progress: { value: 0 },
2088
+ effect: { value: EFFECT_INDEX[transition.preset] }
2089
+ },
2090
+ vertexShader: `
2091
+ varying vec2 vUv;
2092
+ void main() {
2093
+ vUv = uv;
2094
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2095
+ }
2096
+ `,
2097
+ fragmentShader: `
2098
+ uniform sampler2D map;
2099
+ uniform float progress;
2100
+ uniform float effect;
2101
+ varying vec2 vUv;
2102
+
2103
+ float easeInCubic(float t) { return t * t * t; }
2104
+ float easeOutSine(float t) { return sin(t * 1.57079632679); }
2105
+ float easeInOutSine(float t) { return -(cos(3.14159265359 * t) - 1.0) * 0.5; }
2106
+
2107
+ void main() {
2108
+ float t = clamp(progress, 0.0, 1.0);
2109
+ vec2 uv = vUv;
2110
+ vec4 old = texture2D(map, uv);
2111
+ float alpha = 1.0;
2112
+
2113
+ if (effect == 1.0) {
2114
+ alpha = 1.0 - easeInCubic(t);
2115
+ } else if (effect == 2.0) {
2116
+ float eased = easeInOutSine(t);
2117
+ // ZOOMBLEND(2.0, 2.0, easeInOutSine): reach 2x zoom.
2118
+ uv = 0.5 + (vUv - 0.5) / mix(1.0, 2.0, eased);
2119
+ old = texture2D(map, uv);
2120
+ alpha = 1.0 - eased;
2121
+ } else if (effect == 3.0) {
2122
+ // COLORBLEND(2.0, 0x000000, easeOutSine).
2123
+ float blackout = easeOutSine(min(1.0, t * 2.0));
2124
+ float reveal = easeOutSine(max(0.0, (t - 0.5) * 2.0));
2125
+ old.rgb = mix(old.rgb, vec3(0.0), blackout);
2126
+ alpha = 1.0 - reveal;
2127
+ } else if (effect == 4.0) {
2128
+ // LIGHTBLEND(1.0, 0xFFFFFF, 2.0, linear).
2129
+ float flash = min(1.0, 2.0 * sin(t * 3.14159265359));
2130
+ old.rgb = mix(old.rgb, vec3(1.0), flash);
2131
+ alpha = 1.0 - t;
2132
+ } else if (effect >= 5.0 && effect <= 7.0) {
2133
+ vec2 direction = effect == 5.0 ? vec2(1.0, 0.0) : (effect == 6.0 ? vec2(0.0, 1.0) : normalize(vec2(-1.0, 1.0)));
2134
+ float coordinate = dot(vUv - 0.5, direction) + 0.5;
2135
+ float softness = effect == 5.0 ? 0.2 : (effect == 6.0 ? 0.01 : 0.4);
2136
+ alpha = smoothstep(t - softness, t + softness, coordinate);
2137
+ } else if (effect >= 8.0) {
2138
+ vec2 point = vUv - 0.5;
2139
+ vec2 scale = effect == 9.0 ? vec2(4.0, 1.0) : (effect == 10.0 ? vec2(1.0, 4.0) : vec2(1.35, 0.8));
2140
+ float distanceFromCenter = length(point * scale);
2141
+ float edge = effect == 8.0 ? 0.2 : (effect == 9.0 ? 0.1 : 0.3);
2142
+ float radius = t * (effect == 9.0 ? 1.1 : 1.45);
2143
+ alpha = smoothstep(radius, radius + edge, distanceFromCenter);
2144
+ if (effect == 11.0) {
2145
+ // OPENBLEND(1.0, -0.5, 0.3, 0.8, linear).
2146
+ old = texture2D(map, 0.5 + point / (1.0 + 0.8 * t));
2147
+ }
2148
+ }
2149
+ gl_FragColor = vec4(old.rgb, old.a * alpha);
2150
+ // The snapshot is linear. Encode it for the drawing buffer so it
2151
+ // matches the newly rendered panorama beneath this overlay.
2152
+ #include <colorspace_fragment>
2153
+ }
2154
+ `
2155
+ }),
2156
+ [snapshot.texture, transition.preset]
2157
+ );
2158
+ useEffect8(() => () => material.dispose(), [material]);
2159
+ useEffect8(() => {
2160
+ startRef.current = null;
2161
+ finishedRef.current = false;
2162
+ material.uniforms.progress.value = 0;
2163
+ }, [material, runId]);
2164
+ useFrame4((state) => {
2165
+ const mesh = meshRef.current;
2166
+ if (!mesh || !camera.isPerspectiveCamera) {
2167
+ return;
2168
+ }
2169
+ const perspectiveCamera = camera;
2170
+ const distance = Math.max(perspectiveCamera.near * 1.1, 0.11);
2171
+ camera.getWorldDirection(directionRef.current);
2172
+ mesh.position.copy(camera.position).addScaledVector(directionRef.current, distance);
2173
+ mesh.quaternion.copy(camera.quaternion);
2174
+ const height = 2 * distance * Math.tan(perspectiveCamera.fov * Math.PI / 360);
2175
+ mesh.scale.set(height * perspectiveCamera.aspect, height, 1);
2176
+ if (!running || finishedRef.current) {
2177
+ return;
2178
+ }
2179
+ if (startRef.current === null) {
2180
+ startRef.current = state.clock.elapsedTime;
2181
+ }
2182
+ const progress = transition.duration === 0 ? 1 : Math.min(1, (state.clock.elapsedTime - startRef.current) / transition.duration);
2183
+ material.uniforms.progress.value = progress;
2184
+ if (progress === 1) {
2185
+ finishedRef.current = true;
2186
+ onFinish();
2187
+ }
2188
+ });
2189
+ return /* @__PURE__ */ jsxs5("mesh", { ref: meshRef, renderOrder: 1e4, frustumCulled: false, children: [
2190
+ /* @__PURE__ */ jsx7("primitive", { attach: "geometry", object: FULLSCREEN_PLANE }),
2191
+ /* @__PURE__ */ jsx7("primitive", { attach: "material", object: material })
2192
+ ] });
2193
+ }
2194
+ var FULLSCREEN_PLANE = new PlaneGeometry2(1, 1);
2195
+ function PanoramaScenesController({
2196
+ scenes,
2197
+ activeSceneId,
2198
+ transition: transitionInput,
2199
+ renderHotspots,
2200
+ onTransitionEnd,
2201
+ onTransitionError,
2202
+ snapshotMaxPixels = 3686400
2203
+ }) {
2204
+ const controlsRef = useContext4(PanoramaControlsContext);
2205
+ const manager = useSharedTileTextureManager();
2206
+ const transition = useMemo5(
2207
+ () => resolveTransition(transitionInput),
2208
+ [transitionInput]
2209
+ );
2210
+ const sceneMap = useMemo5(
2211
+ () => new Map(scenes.map((scene) => [scene.id, scene])),
2212
+ [scenes]
2213
+ );
2214
+ const initialScene = sceneMap.get(activeSceneId) ?? null;
2215
+ const [slots, setSlots] = useState5([
2216
+ initialScene,
2217
+ null
2218
+ ]);
2219
+ const [activeSlot, setActiveSlot] = useState5(0);
2220
+ const [liveSlot, setLiveSlot] = useState5(0);
2221
+ const [pendingSlot, setPendingSlot] = useState5(null);
2222
+ const [phase, setPhase] = useState5(initialScene ? "idle" : "preloading");
2223
+ const [captureRequestId, setCaptureRequestId] = useState5(0);
2224
+ const [transitionRunId, setTransitionRunId] = useState5(0);
2225
+ const [snapshot, setSnapshot] = useState5(null);
2226
+ const [recoveryKey, setRecoveryKey] = useState5(0);
2227
+ const requestedSceneIdRef = useRef7(activeSceneId);
2228
+ const captureReasonRef = useRef7("start");
2229
+ const interactionReleaseRef = useRef7(null);
2230
+ const slotsRef = useRef7(slots);
2231
+ const phaseRef = useRef7(phase);
2232
+ const liveSlotRef = useRef7(liveSlot);
2233
+ const transitionFromRef = useRef7(initialScene);
2234
+ slotsRef.current = slots;
2235
+ phaseRef.current = phase;
2236
+ liveSlotRef.current = liveSlot;
2237
+ const releaseInteraction = useCallback2(() => {
2238
+ interactionReleaseRef.current?.();
2239
+ interactionReleaseRef.current = null;
2240
+ }, []);
2241
+ const reportError = useCallback2((sceneId, error) => {
2242
+ onTransitionError?.({ sceneId, error });
2243
+ }, [onTransitionError]);
2244
+ const startPreload = useCallback2((scene) => {
2245
+ const nextSlot = 1 - liveSlotRef.current;
2246
+ requestedSceneIdRef.current = scene.id;
2247
+ setSlots((current) => {
2248
+ const next = [...current];
2249
+ next[nextSlot] = scene;
2250
+ return next;
2251
+ });
2252
+ setPendingSlot(nextSlot);
2253
+ setPhase("preloading");
2254
+ }, []);
2255
+ useEffect8(() => {
2256
+ const requested = sceneMap.get(activeSceneId);
2257
+ if (!requested) {
2258
+ reportError(activeSceneId, new Error(`Unknown panorama scene: ${activeSceneId}`));
2259
+ return;
2260
+ }
2261
+ const liveScene = slotsRef.current[liveSlotRef.current];
2262
+ if (phaseRef.current === "idle" && liveScene?.id === requested.id) {
2263
+ requestedSceneIdRef.current = requested.id;
2264
+ return;
2265
+ }
2266
+ if (phaseRef.current === "preloading" && pendingSlot !== null && slotsRef.current[pendingSlot]?.id === requested.id) {
2267
+ requestedSceneIdRef.current = requested.id;
2268
+ return;
2269
+ }
2270
+ if (phaseRef.current === "transitioning" || phaseRef.current === "capturing") {
2271
+ requestedSceneIdRef.current = requested.id;
2272
+ captureReasonRef.current = "interrupt";
2273
+ setPhase("recapturing");
2274
+ setCaptureRequestId((current) => current + 1);
2275
+ return;
2276
+ }
2277
+ startPreload(requested);
2278
+ }, [activeSceneId, pendingSlot, reportError, sceneMap, startPreload]);
2279
+ const handleReady = useCallback2((slot) => {
2280
+ if (slot !== pendingSlot || phaseRef.current !== "preloading") {
2281
+ return;
2282
+ }
2283
+ if (transition.preset === "none") {
2284
+ const previousScene = slotsRef.current[liveSlotRef.current];
2285
+ const nextScene = slotsRef.current[slot];
2286
+ setSlots((current) => {
2287
+ const next = [...current];
2288
+ next[liveSlotRef.current] = null;
2289
+ return next;
2290
+ });
2291
+ setLiveSlot(slot);
2292
+ setActiveSlot(slot);
2293
+ setPendingSlot(null);
2294
+ setPhase("idle");
2295
+ if (previousScene && nextScene) {
2296
+ onTransitionEnd?.({
2297
+ previousSceneId: previousScene.id,
2298
+ sceneId: nextScene.id,
2299
+ preset: "none"
2300
+ });
2301
+ }
2302
+ return;
2303
+ }
2304
+ interactionReleaseRef.current ?? (interactionReleaseRef.current = controlsRef?.current?.acquireInteractionLock() ?? null);
2305
+ transitionFromRef.current = slotsRef.current[liveSlotRef.current];
2306
+ captureReasonRef.current = "start";
2307
+ setPhase("capturing");
2308
+ setCaptureRequestId((current) => current + 1);
2309
+ }, [controlsRef, onTransitionEnd, pendingSlot, transition.preset]);
2310
+ const handleCaptured = useCallback2((nextSnapshot) => {
2311
+ setSnapshot((current) => {
2312
+ current?.dispose();
2313
+ return nextSnapshot;
2314
+ });
2315
+ if (captureReasonRef.current === "interrupt") {
2316
+ startPreload(sceneMap.get(requestedSceneIdRef.current));
2317
+ return;
2318
+ }
2319
+ const sourceSlot = liveSlotRef.current;
2320
+ setSlots((current) => {
2321
+ const next = [...current];
2322
+ next[sourceSlot] = null;
2323
+ return next;
2324
+ });
2325
+ manager?.releaseUnused();
2326
+ if (pendingSlot !== null) {
2327
+ setLiveSlot(pendingSlot);
2328
+ }
2329
+ setPhase("transitioning");
2330
+ setTransitionRunId((current) => current + 1);
2331
+ }, [manager, pendingSlot, sceneMap, startPreload]);
2332
+ const finishTransition = useCallback2(() => {
2333
+ const nextSlot = pendingSlot;
2334
+ const previousScene = transitionFromRef.current;
2335
+ const nextScene = nextSlot === null ? null : slotsRef.current[nextSlot];
2336
+ if (nextSlot === null || !nextScene) {
2337
+ releaseInteraction();
2338
+ setPhase("idle");
2339
+ return;
2340
+ }
2341
+ setActiveSlot(nextSlot);
2342
+ setLiveSlot(nextSlot);
2343
+ setPendingSlot(null);
2344
+ setPhase("idle");
2345
+ const oldSnapshot = snapshot;
2346
+ setSnapshot(null);
2347
+ oldSnapshot?.dispose();
2348
+ releaseInteraction();
2349
+ if (previousScene) {
2350
+ onTransitionEnd?.({
2351
+ previousSceneId: previousScene.id,
2352
+ sceneId: nextScene.id,
2353
+ preset: transition.preset
2354
+ });
2355
+ }
2356
+ }, [onTransitionEnd, pendingSlot, releaseInteraction, snapshot, transition.preset]);
2357
+ useEffect8(() => () => {
2358
+ releaseInteraction();
2359
+ snapshot?.dispose();
2360
+ }, [releaseInteraction, snapshot]);
2361
+ const { gl } = useThree5();
2362
+ useEffect8(() => {
2363
+ const canvas = gl.domElement;
2364
+ const onContextLost = (event) => {
2365
+ event.preventDefault();
2366
+ releaseInteraction();
2367
+ manager?.dispose();
2368
+ snapshot?.dispose();
2369
+ setSnapshot(null);
2370
+ setPendingSlot(null);
2371
+ setPhase("idle");
2372
+ };
2373
+ const onContextRestored = () => {
2374
+ manager?.resume();
2375
+ setRecoveryKey((current) => current + 1);
2376
+ };
2377
+ canvas.addEventListener("webglcontextlost", onContextLost);
2378
+ canvas.addEventListener("webglcontextrestored", onContextRestored);
2379
+ return () => {
2380
+ canvas.removeEventListener("webglcontextlost", onContextLost);
2381
+ canvas.removeEventListener("webglcontextrestored", onContextRestored);
2382
+ };
2383
+ }, [gl, manager, releaseInteraction, snapshot]);
2384
+ const renderedScene = slots[liveSlot];
2385
+ const snapshotVisible = snapshot !== null && (phase === "transitioning" || phase === "recapturing" || phase === "preloading");
2386
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
2387
+ /* @__PURE__ */ jsx7(
2388
+ SnapshotCapture,
2389
+ {
2390
+ requestId: captureRequestId,
2391
+ maxPixels: Math.max(1, snapshotMaxPixels),
2392
+ onCaptured: handleCaptured
2393
+ }
2394
+ ),
2395
+ [0, 1].map((slot) => {
2396
+ const source = slots[slot];
2397
+ const isLive = slot === liveSlot;
2398
+ const isPending = slot === pendingSlot;
2399
+ return /* @__PURE__ */ jsx7(
2400
+ SceneSource,
2401
+ {
2402
+ scene: source,
2403
+ visible: isLive,
2404
+ baseOnly: isPending && phase !== "idle",
2405
+ onReady: () => handleReady(slot),
2406
+ onError: (error) => {
2407
+ if (source) {
2408
+ reportError(source.id, error);
2409
+ }
2410
+ }
2411
+ },
2412
+ `${slot}:${source?.id ?? "empty"}:${recoveryKey}`
2413
+ );
2414
+ }),
2415
+ snapshotVisible ? /* @__PURE__ */ jsx7(
2416
+ SnapshotOverlay,
2417
+ {
2418
+ snapshot,
2419
+ transition,
2420
+ running: phase === "transitioning",
2421
+ runId: transitionRunId,
2422
+ onFinish: finishTransition
2423
+ }
2424
+ ) : null,
2425
+ phase === "idle" && renderedScene ? renderHotspots?.(renderedScene) : null
2426
+ ] });
2427
+ }
2428
+ function PanoramaScenes({
2429
+ maxTextureMemoryMb,
2430
+ maxConcurrentTileLoads,
2431
+ ...props
2432
+ }) {
2433
+ return /* @__PURE__ */ jsx7(
2434
+ TileTextureManagerProvider,
2435
+ {
2436
+ maxTextureMemoryMb,
2437
+ maxConcurrentLoads: maxConcurrentTileLoads,
2438
+ children: /* @__PURE__ */ jsx7(PanoramaScenesController, { ...props })
2439
+ }
2440
+ );
2441
+ }
2442
+
2443
+ // src/hotspot/image-hotspot.tsx
2444
+ import { useThree as useThree6 } from "@react-three/fiber";
2445
+ import { useEffect as useEffect10, useRef as useRef9, useState as useState6 } from "react";
2446
+ import {
2447
+ ClampToEdgeWrapping as ClampToEdgeWrapping3,
2448
+ DoubleSide as DoubleSide2,
2449
+ LinearFilter as LinearFilter5,
2450
+ LinearMipmapLinearFilter as LinearMipmapLinearFilter2,
2451
+ SRGBColorSpace as SRGBColorSpace4,
2452
+ TextureLoader as TextureLoader4
2453
+ } from "three";
2454
+
2455
+ // src/hotspot/hotspot-anchor.tsx
2456
+ import { useFrame as useFrame5 } from "@react-three/fiber";
2457
+ import {
2458
+ useContext as useContext5,
2459
+ useEffect as useEffect9,
2460
+ useMemo as useMemo6,
2461
+ useRef as useRef8
2462
+ } from "react";
2463
+ import {
2464
+ EdgesGeometry,
2465
+ MathUtils as MathUtils4,
2466
+ Matrix4 as Matrix42,
2467
+ PlaneGeometry as PlaneGeometry3,
2468
+ PerspectiveCamera as PerspectiveCamera4,
2469
+ Quaternion as Quaternion2,
2470
+ Vector3 as Vector35
2471
+ } from "three";
2472
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2473
+ var DEFAULT_FLOATING_DISTANCE = 10;
2474
+ var PANORAMA_RADIUS = DEFAULT_PANORAMA_RADIUS;
2475
+ var PANORAMA_SURFACE_INSET = 4;
2476
+ var DEFAULT_REFERENCE_FOV = 75;
2477
+ var MIN_ANGULAR_SIZE = 0.01;
2478
+ var MAX_ANGULAR_SIZE = 179;
2479
+ var DRAG_EPSILON_DEGREES = 1e-3;
2480
+ var LOCAL_FORWARD = new Vector35(0, 0, 1);
2481
+ var HOTSPOT_MODE_RENDERING = {
2482
+ surface: { orientation: "surface", placement: "surface" },
2483
+ billboard: { orientation: "billboard", placement: "floating" }
2484
+ };
2485
+ function angularSizeToWorldSize(size, radius) {
2486
+ const safeSize = Number.isFinite(size) ? MathUtils4.clamp(Math.abs(size), MIN_ANGULAR_SIZE, MAX_ANGULAR_SIZE) : MIN_ANGULAR_SIZE;
2487
+ return 2 * radius * Math.tan(MathUtils4.degToRad(safeSize) / 2);
2488
+ }
2489
+ function resolveDistance(placement, distance, width, height) {
2490
+ const maximumDistance = maximumHotspotDistance(width, height);
2491
+ if (placement === "surface") {
2492
+ return maximumDistance;
2493
+ }
2494
+ if (!Number.isFinite(distance)) {
2495
+ return DEFAULT_FLOATING_DISTANCE;
2496
+ }
2497
+ return MathUtils4.clamp(Math.abs(distance), 0.1, maximumDistance);
2498
+ }
2499
+ function maximumHotspotDistance(width, height) {
2500
+ const safeWidth = MathUtils4.clamp(
2501
+ Math.abs(Number.isFinite(width) ? width : MIN_ANGULAR_SIZE),
2502
+ MIN_ANGULAR_SIZE,
2503
+ MAX_ANGULAR_SIZE
2504
+ );
2505
+ const safeHeight = MathUtils4.clamp(
2506
+ Math.abs(Number.isFinite(height) ? height : MIN_ANGULAR_SIZE),
2507
+ MIN_ANGULAR_SIZE,
2508
+ MAX_ANGULAR_SIZE
2509
+ );
2510
+ const cornerFactor = Math.hypot(
2511
+ 1,
2512
+ Math.tan(MathUtils4.degToRad(safeWidth) / 2),
2513
+ Math.tan(MathUtils4.degToRad(safeHeight) / 2)
2514
+ );
2515
+ return (PANORAMA_RADIUS - PANORAMA_SURFACE_INSET) / cornerFactor;
2516
+ }
2517
+ function fixedScaleFactor(cameraFov, referenceFov) {
2518
+ const safeReferenceFov = MathUtils4.clamp(referenceFov, 1, 179);
2519
+ return Math.tan(MathUtils4.degToRad(cameraFov) / 2) / Math.tan(MathUtils4.degToRad(safeReferenceFov) / 2);
2520
+ }
2521
+ function createSurfaceQuaternion(position) {
2522
+ const outward = panoPositionToVector3(position).normalize();
2523
+ const inward = outward.clone().negate();
2524
+ const pitch = MathUtils4.degToRad(position.pitch);
2525
+ const yaw = MathUtils4.degToRad(position.yaw);
2526
+ const localUp = new Vector35(
2527
+ -Math.sin(pitch) * Math.sin(yaw),
2528
+ Math.cos(pitch),
2529
+ Math.sin(pitch) * Math.cos(yaw)
2530
+ ).normalize();
2531
+ const localRight = localUp.clone().cross(inward).normalize();
2532
+ const basis = new Matrix42().makeBasis(localRight, localUp, inward);
2533
+ return new Quaternion2().setFromRotationMatrix(basis);
2534
+ }
2535
+ function makeInteractionEvent(id, position, event) {
2536
+ return {
2537
+ id,
2538
+ position: normalizePanoPosition(position),
2539
+ source: "pointer",
2540
+ nativeEvent: event.nativeEvent
2541
+ };
2542
+ }
2543
+ function angularDistance(a, b) {
2544
+ const aVector = panoPositionToVector3(a);
2545
+ const bVector = panoPositionToVector3(b);
2546
+ return MathUtils4.radToDeg(aVector.angleTo(bVector));
2547
+ }
2548
+ function HotspotAnchor({
2549
+ id,
2550
+ position,
2551
+ width,
2552
+ height,
2553
+ useAngularScale = true,
2554
+ focusContent,
2555
+ mode = "billboard",
2556
+ distance,
2557
+ internalOrientation,
2558
+ internalPlacement,
2559
+ scaleMode = "fov",
2560
+ referenceFov = DEFAULT_REFERENCE_FOV,
2561
+ rotation = 0,
2562
+ renderOrder = 0,
2563
+ visible = true,
2564
+ draggable = false,
2565
+ interactive = true,
2566
+ ariaLabel,
2567
+ children,
2568
+ onClick,
2569
+ onHoverChange,
2570
+ onDragStart,
2571
+ onPositionChange,
2572
+ onDragEnd
2573
+ }) {
2574
+ const controlsRef = useContext5(PanoramaControlsContext);
2575
+ const groupRef = useRef8(null);
2576
+ const dragStateRef = useRef8(null);
2577
+ const suppressNextClickRef = useRef8(false);
2578
+ const normalizedPosition = normalizePanoPosition(position);
2579
+ const rendering = internalOrientation && internalPlacement ? { orientation: internalOrientation, placement: internalPlacement } : HOTSPOT_MODE_RENDERING[mode];
2580
+ const { orientation: orientation2, placement } = rendering;
2581
+ const resolvedDistance = resolveDistance(placement, distance, width, height);
2582
+ const worldPosition = useMemo6(
2583
+ () => panoPositionToVector3(normalizedPosition, resolvedDistance),
2584
+ [normalizedPosition.pitch, normalizedPosition.yaw, resolvedDistance]
2585
+ );
2586
+ const surfaceQuaternion3 = useMemo6(
2587
+ () => createSurfaceQuaternion(normalizedPosition),
2588
+ [normalizedPosition.pitch, normalizedPosition.yaw]
2589
+ );
2590
+ const worldWidth = angularSizeToWorldSize(width, resolvedDistance);
2591
+ const worldHeight = angularSizeToWorldSize(height, resolvedDistance);
2592
+ const focusGeometry = useMemo6(() => {
2593
+ const planeGeometry = new PlaneGeometry3(1, 1);
2594
+ const geometry = new EdgesGeometry(planeGeometry);
2595
+ planeGeometry.dispose();
2596
+ return geometry;
2597
+ }, []);
2598
+ const focused = useHotspotAccessibility({
2599
+ id,
2600
+ ariaLabel,
2601
+ onActivate: visible && interactive && onClick ? (event) => {
2602
+ onClick({
2603
+ id,
2604
+ position: normalizedPosition,
2605
+ source: "keyboard",
2606
+ nativeEvent: event
2607
+ });
2608
+ } : void 0
2609
+ });
2610
+ useEffect9(
2611
+ () => () => {
2612
+ dragStateRef.current?.releaseInteractionLock();
2613
+ dragStateRef.current = null;
2614
+ },
2615
+ []
2616
+ );
2617
+ useEffect9(() => () => focusGeometry.dispose(), [focusGeometry]);
2618
+ useFrame5(({ camera }) => {
2619
+ if (!groupRef.current) {
2620
+ return;
2621
+ }
2622
+ if (orientation2 === "billboard") {
2623
+ groupRef.current.quaternion.copy(camera.quaternion);
2624
+ } else {
2625
+ groupRef.current.quaternion.copy(surfaceQuaternion3);
2626
+ }
2627
+ groupRef.current.rotateOnAxis(
2628
+ LOCAL_FORWARD,
2629
+ MathUtils4.degToRad(rotation)
2630
+ );
2631
+ const scaleFactor = scaleMode === "fixed" && camera instanceof PerspectiveCamera4 ? fixedScaleFactor(camera.fov, referenceFov) : 1;
2632
+ groupRef.current.scale.set(
2633
+ (useAngularScale ? worldWidth : 1) * scaleFactor,
2634
+ (useAngularScale ? worldHeight : 1) * scaleFactor,
2635
+ 1
2636
+ );
2637
+ });
2638
+ const stopPointerEvent = (event) => {
2639
+ event.stopPropagation();
2640
+ event.nativeEvent.stopImmediatePropagation?.();
2641
+ };
2642
+ const releasePointer = (event, emitDragEnd) => {
2643
+ const dragState = dragStateRef.current;
2644
+ if (!dragState || dragState.pointerId !== event.pointerId) {
2645
+ return;
2646
+ }
2647
+ stopPointerEvent(event);
2648
+ const target = event.target;
2649
+ if (target.hasPointerCapture?.(event.pointerId)) {
2650
+ target.releasePointerCapture?.(event.pointerId);
2651
+ }
2652
+ const nextPosition = vector3ToPanoPosition(event.ray.direction);
2653
+ if (emitDragEnd) {
2654
+ onDragEnd?.({
2655
+ ...makeInteractionEvent(id, nextPosition, event),
2656
+ source: "pointer",
2657
+ startPosition: dragState.startPosition
2658
+ });
2659
+ }
2660
+ suppressNextClickRef.current = dragState.moved;
2661
+ dragState.releaseInteractionLock();
2662
+ dragStateRef.current = null;
2663
+ };
2664
+ return /* @__PURE__ */ jsxs6(
2665
+ "group",
2666
+ {
2667
+ ref: groupRef,
2668
+ position: worldPosition,
2669
+ renderOrder,
2670
+ scale: [worldWidth, worldHeight, 1],
2671
+ visible,
2672
+ onClick: interactive ? (event) => {
2673
+ stopPointerEvent(event);
2674
+ if (!suppressNextClickRef.current) {
2675
+ onClick?.(makeInteractionEvent(id, normalizedPosition, event));
2676
+ }
2677
+ suppressNextClickRef.current = false;
2678
+ } : void 0,
2679
+ onLostPointerCapture: interactive ? (event) => releasePointer(event, false) : void 0,
2680
+ onPointerCancel: interactive ? (event) => releasePointer(event, false) : void 0,
2681
+ onPointerDown: interactive ? (event) => {
2682
+ stopPointerEvent(event);
2683
+ if (!draggable || dragStateRef.current) {
2684
+ return;
2685
+ }
2686
+ const releaseInteractionLock = controlsRef?.current?.acquireInteractionLock() ?? (() => void 0);
2687
+ const target = event.target;
2688
+ target.setPointerCapture?.(event.pointerId);
2689
+ const startPosition = { ...normalizedPosition };
2690
+ dragStateRef.current = {
2691
+ pointerId: event.pointerId,
2692
+ startPosition,
2693
+ releaseInteractionLock,
2694
+ moved: false
2695
+ };
2696
+ onDragStart?.({
2697
+ ...makeInteractionEvent(id, startPosition, event),
2698
+ source: "pointer",
2699
+ startPosition
2700
+ });
2701
+ } : void 0,
2702
+ onPointerMove: interactive ? (event) => {
2703
+ const dragState = dragStateRef.current;
2704
+ if (!dragState || dragState.pointerId !== event.pointerId) {
2705
+ return;
2706
+ }
2707
+ stopPointerEvent(event);
2708
+ const nextPosition = vector3ToPanoPosition(event.ray.direction);
2709
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
2710
+ onPositionChange?.({
2711
+ ...makeInteractionEvent(id, nextPosition, event),
2712
+ source: "pointer",
2713
+ startPosition: dragState.startPosition
2714
+ });
2715
+ } : void 0,
2716
+ onPointerOut: interactive ? (event) => {
2717
+ if (!dragStateRef.current) {
2718
+ onHoverChange?.(
2719
+ false,
2720
+ makeInteractionEvent(id, normalizedPosition, event)
2721
+ );
2722
+ }
2723
+ } : void 0,
2724
+ onPointerOver: interactive ? (event) => {
2725
+ if (!dragStateRef.current) {
2726
+ onHoverChange?.(
2727
+ true,
2728
+ makeInteractionEvent(id, normalizedPosition, event)
2729
+ );
2730
+ }
2731
+ } : void 0,
2732
+ onPointerUp: interactive ? (event) => releasePointer(event, true) : void 0,
2733
+ children: [
2734
+ focused ? focusContent ?? /* @__PURE__ */ jsx8(
2735
+ "lineSegments",
2736
+ {
2737
+ geometry: focusGeometry,
2738
+ position: [0, 0, 0.01],
2739
+ renderOrder: renderOrder + 1,
2740
+ scale: [1.12, 1.12, 1],
2741
+ children: /* @__PURE__ */ jsx8("lineBasicMaterial", { color: "#f5fbfc", depthTest: false })
2742
+ }
2743
+ ) : null,
2744
+ children
2745
+ ]
2746
+ }
2747
+ );
2748
+ }
2749
+
2750
+ // src/hotspot/image-hotspot.tsx
2751
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2752
+ function clampOpacity(opacity) {
2753
+ if (!Number.isFinite(opacity)) {
2754
+ return 1;
2755
+ }
2756
+ return Math.max(0, Math.min(opacity, 1));
2757
+ }
2758
+ function useHotspotImageTexture(src, onLoad, onError) {
2759
+ const gl = useThree6((state) => state.gl);
2760
+ const [texture, setTexture] = useState6(null);
2761
+ const onLoadRef = useRef9(onLoad);
2762
+ const onErrorRef = useRef9(onError);
2763
+ onLoadRef.current = onLoad;
2764
+ onErrorRef.current = onError;
2765
+ useEffect10(() => {
2766
+ let active = true;
2767
+ setTexture(null);
2768
+ if (!src) {
2769
+ return () => {
2770
+ active = false;
2771
+ };
2772
+ }
2773
+ const loader = new TextureLoader4();
2774
+ loader.load(
2775
+ src,
2776
+ (loadedTexture) => {
2777
+ if (!active) {
2778
+ loadedTexture.dispose();
2779
+ return;
2780
+ }
2781
+ loadedTexture.colorSpace = SRGBColorSpace4;
2782
+ loadedTexture.wrapS = ClampToEdgeWrapping3;
2783
+ loadedTexture.wrapT = ClampToEdgeWrapping3;
2784
+ loadedTexture.magFilter = LinearFilter5;
2785
+ loadedTexture.minFilter = LinearMipmapLinearFilter2;
2786
+ loadedTexture.generateMipmaps = true;
2787
+ loadedTexture.anisotropy = gl.capabilities.getMaxAnisotropy();
2788
+ loadedTexture.needsUpdate = true;
2789
+ setTexture(loadedTexture);
2790
+ onLoadRef.current?.(loadedTexture);
2791
+ },
2792
+ void 0,
2793
+ (error) => {
2794
+ if (active) {
2795
+ onErrorRef.current?.(error);
2796
+ }
2797
+ }
2798
+ );
2799
+ return () => {
2800
+ active = false;
2801
+ };
2802
+ }, [gl, src]);
2803
+ useEffect10(
2804
+ () => () => {
2805
+ texture?.dispose();
2806
+ },
2807
+ [texture]
2808
+ );
2809
+ return texture;
2810
+ }
2811
+ function ImageHotspot({
2812
+ src,
2813
+ width = 12,
2814
+ height = 8,
2815
+ opacity,
2816
+ onLoad,
2817
+ onError,
2818
+ ...anchorProps
2819
+ }) {
2820
+ const texture = useHotspotImageTexture(src, onLoad, onError);
2821
+ return /* @__PURE__ */ jsx9(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs7("mesh", { children: [
2822
+ /* @__PURE__ */ jsx9("planeGeometry", { args: [1, 1] }),
2823
+ /* @__PURE__ */ jsx9(
2824
+ "meshBasicMaterial",
2825
+ {
2826
+ map: texture,
2827
+ opacity: texture ? clampOpacity(opacity) : 0,
2828
+ side: DoubleSide2,
2829
+ toneMapped: false,
2830
+ transparent: true
2831
+ }
2832
+ )
2833
+ ] }) });
2834
+ }
2835
+
2836
+ // src/hotspot/polygon-hotspot.tsx
2837
+ import { useThree as useThree7 } from "@react-three/fiber";
2838
+ import { useEffect as useEffect11, useMemo as useMemo7, useRef as useRef10 } from "react";
2839
+ import {
2840
+ BufferGeometry,
2841
+ DoubleSide as DoubleSide3,
2842
+ Float32BufferAttribute,
2843
+ Matrix4 as Matrix43,
2844
+ Quaternion as Quaternion3,
2845
+ ShapeUtils,
2846
+ Vector2 as Vector22,
2847
+ Vector3 as Vector36
2848
+ } from "three";
2849
+ import { Line2 } from "three/addons/lines/Line2.js";
2850
+ import { LineGeometry } from "three/addons/lines/LineGeometry.js";
2851
+ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
2852
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2853
+ var POLYGON_RADIUS = 990;
2854
+ var MAX_FILL_SEGMENT_DEGREES = 7;
2855
+ function cloneVertices(vertices) {
2856
+ return vertices.map((vertex) => normalizePanoPosition(vertex));
2857
+ }
2858
+ function unwrapPolygonVertices(vertices) {
2859
+ const unwrapped = [];
2860
+ for (const vertex of vertices) {
2861
+ const normalized = normalizePanoPosition(vertex);
2862
+ const previous = unwrapped[unwrapped.length - 1];
2863
+ if (!previous) {
2864
+ unwrapped.push({ ...normalized, unwrappedYaw: normalized.yaw });
2865
+ continue;
2866
+ }
2867
+ const previousYaw = previous.unwrappedYaw;
2868
+ let yaw = normalized.yaw;
2869
+ while (yaw - previousYaw > 180) yaw -= 360;
2870
+ while (yaw - previousYaw <= -180) yaw += 360;
2871
+ unwrapped.push({ ...normalized, unwrappedYaw: yaw });
2872
+ }
2873
+ return unwrapped;
2874
+ }
2875
+ function orientation(a, b, c) {
2876
+ return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
2877
+ }
2878
+ function intersects(a, b, c, d) {
2879
+ const abC = orientation(a, b, c);
2880
+ const abD = orientation(a, b, d);
2881
+ const cdA = orientation(c, d, a);
2882
+ const cdB = orientation(c, d, b);
2883
+ return Math.sign(abC) !== Math.sign(abD) && Math.sign(cdA) !== Math.sign(cdB);
2884
+ }
2885
+ function hasSelfIntersection(vertices) {
2886
+ const points = vertices.map(
2887
+ (vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch)
2888
+ );
2889
+ for (let index = 0; index < points.length; index += 1) {
2890
+ const nextIndex = (index + 1) % points.length;
2891
+ for (let other = index + 1; other < points.length; other += 1) {
2892
+ const otherNext = (other + 1) % points.length;
2893
+ if (index === other || nextIndex === other || otherNext === index || index === 0 && otherNext === 0) {
2894
+ continue;
2895
+ }
2896
+ if (intersects(points[index], points[nextIndex], points[other], points[otherNext])) {
2897
+ return true;
2898
+ }
2899
+ }
2900
+ }
2901
+ return false;
2902
+ }
2903
+ function polygonContainsDirection(vertices, direction) {
2904
+ let winding = 0;
2905
+ for (let index = 0; index < vertices.length; index += 1) {
2906
+ const a = panoPositionToVector3(vertices[index]).normalize();
2907
+ const b = panoPositionToVector3(vertices[(index + 1) % vertices.length]).normalize();
2908
+ const projectedA = a.clone().addScaledVector(direction, -a.dot(direction));
2909
+ const projectedB = b.clone().addScaledVector(direction, -b.dot(direction));
2910
+ if (projectedA.lengthSq() < 1e-8 || projectedB.lengthSq() < 1e-8) {
2911
+ return false;
2912
+ }
2913
+ projectedA.normalize();
2914
+ projectedB.normalize();
2915
+ winding += Math.atan2(
2916
+ direction.dot(projectedA.clone().cross(projectedB)),
2917
+ projectedA.dot(projectedB)
2918
+ );
2919
+ }
2920
+ return Math.abs(winding) > Math.PI;
2921
+ }
2922
+ function fitsHemisphere(vertices) {
2923
+ const directions = vertices.map((vertex) => panoPositionToVector3(vertex).normalize());
2924
+ const candidates = [new Vector36()];
2925
+ for (const direction of directions) {
2926
+ candidates[0].add(direction);
2927
+ candidates.push(direction.clone());
2928
+ }
2929
+ for (let index = 0; index < directions.length; index += 1) {
2930
+ for (let other = index + 1; other < directions.length; other += 1) {
2931
+ candidates.push(directions[index].clone().add(directions[other]));
2932
+ }
2933
+ }
2934
+ return candidates.some(
2935
+ (candidate) => candidate.lengthSq() > 1e-8 && directions.every((direction) => candidate.dot(direction) > 1e-6)
2936
+ );
2937
+ }
2938
+ function validatePolygonVertices(vertices) {
2939
+ if (vertices.length < 3) {
2940
+ return [{ code: "too_few_vertices", message: "A polygon needs at least three vertices." }];
2941
+ }
2942
+ const normalized = cloneVertices(vertices);
2943
+ const unwrapped = unwrapPolygonVertices(normalized);
2944
+ const issues = [];
2945
+ if (hasSelfIntersection(unwrapped)) {
2946
+ issues.push({ code: "self_intersection", message: "Polygon edges cannot cross." });
2947
+ }
2948
+ if (polygonContainsDirection(normalized, new Vector36(0, 1, 0)) || polygonContainsDirection(normalized, new Vector36(0, -1, 0))) {
2949
+ issues.push({ code: "contains_pole", message: "Polygons cannot contain a panorama pole." });
2950
+ }
2951
+ if (!fitsHemisphere(normalized)) {
2952
+ issues.push({ code: "outside_hemisphere", message: "Polygons must fit within one hemisphere." });
2953
+ }
2954
+ return issues;
2955
+ }
2956
+ function polygonCenter(vertices) {
2957
+ const sum = vertices.reduce(
2958
+ (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
2959
+ new Vector36()
2960
+ );
2961
+ return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
2962
+ }
2963
+ function surfaceQuaternion(position) {
2964
+ const outward = panoPositionToVector3(position).normalize();
2965
+ const inward = outward.clone().negate();
2966
+ const pitch = position.pitch * Math.PI / 180;
2967
+ const yaw = position.yaw * Math.PI / 180;
2968
+ const localUp = new Vector36(
2969
+ -Math.sin(pitch) * Math.sin(yaw),
2970
+ Math.cos(pitch),
2971
+ Math.sin(pitch) * Math.cos(yaw)
2972
+ ).normalize();
2973
+ return new Quaternion3().setFromRotationMatrix(
2974
+ new Matrix43().makeBasis(localUp.clone().cross(inward).normalize(), localUp, inward)
2975
+ );
2976
+ }
2977
+ function makeWorldToLocal(center) {
2978
+ return new Matrix43().compose(
2979
+ panoPositionToVector3(center, POLYGON_RADIUS),
2980
+ surfaceQuaternion(center),
2981
+ new Vector36(1, 1, 1)
2982
+ ).invert();
2983
+ }
2984
+ function projectedPoint(a, b, c, weightB, weightC, radius, worldToLocal) {
2985
+ const weightA = 1 - weightB - weightC;
2986
+ return panoPositionToVector3(
2987
+ {
2988
+ yaw: a.unwrappedYaw * weightA + b.unwrappedYaw * weightB + c.unwrappedYaw * weightC,
2989
+ pitch: a.pitch * weightA + b.pitch * weightB + c.pitch * weightC
2990
+ },
2991
+ radius
2992
+ ).applyMatrix4(worldToLocal);
2993
+ }
2994
+ function polygonSubdivisionSteps(vertices) {
2995
+ let maxAngle = 0;
2996
+ for (let index = 0; index < vertices.length; index += 1) {
2997
+ const start = panoPositionToVector3(vertices[index]).normalize();
2998
+ for (let other = index + 1; other < vertices.length; other += 1) {
2999
+ maxAngle = Math.max(
3000
+ maxAngle,
3001
+ start.angleTo(panoPositionToVector3(vertices[other]).normalize())
3002
+ );
3003
+ }
3004
+ }
3005
+ return Math.min(
3006
+ 8,
3007
+ Math.max(1, Math.ceil(maxAngle * 180 / Math.PI / MAX_FILL_SEGMENT_DEGREES))
3008
+ );
3009
+ }
3010
+ function addSubdividedTriangle(positions, indices, a, b, c, worldToLocal, steps) {
3011
+ const grid = /* @__PURE__ */ new Map();
3012
+ const pointIndex = (i, j) => {
3013
+ const key = `${i}:${j}`;
3014
+ const existing = grid.get(key);
3015
+ if (existing !== void 0) return existing;
3016
+ const point = projectedPoint(a, b, c, i / steps, j / steps, POLYGON_RADIUS, worldToLocal);
3017
+ const index = positions.length / 3;
3018
+ positions.push(point.x, point.y, point.z);
3019
+ grid.set(key, index);
3020
+ return index;
3021
+ };
3022
+ for (let i = 0; i <= steps; i += 1) {
3023
+ for (let j = 0; j <= steps - i; j += 1) pointIndex(i, j);
3024
+ }
3025
+ for (let i = 0; i < steps; i += 1) {
3026
+ for (let j = 0; j < steps - i; j += 1) {
3027
+ indices.push(pointIndex(i, j), pointIndex(i + 1, j), pointIndex(i, j + 1));
3028
+ if (i + j < steps - 1) {
3029
+ indices.push(pointIndex(i + 1, j), pointIndex(i + 1, j + 1), pointIndex(i, j + 1));
3030
+ }
3031
+ }
3032
+ }
3033
+ }
3034
+ function makeFillGeometry(vertices, center) {
3035
+ const unwrapped = unwrapPolygonVertices(vertices);
3036
+ const contour = unwrapped.map((vertex) => new Vector22(vertex.unwrappedYaw, vertex.pitch));
3037
+ const triangles = ShapeUtils.triangulateShape(contour, []);
3038
+ const positions = [];
3039
+ const indices = [];
3040
+ const worldToLocal = makeWorldToLocal(center);
3041
+ const steps = polygonSubdivisionSteps(vertices);
3042
+ for (const [a, b, c] of triangles) {
3043
+ addSubdividedTriangle(
3044
+ positions,
3045
+ indices,
3046
+ unwrapped[a],
3047
+ unwrapped[b],
3048
+ unwrapped[c],
3049
+ worldToLocal,
3050
+ steps
3051
+ );
3052
+ }
3053
+ return new BufferGeometry().setAttribute("position", new Float32BufferAttribute(positions, 3)).setIndex(indices);
3054
+ }
3055
+ function makeStrokePositions(vertices, center) {
3056
+ const positions = [];
3057
+ const worldToLocal = makeWorldToLocal(center);
3058
+ const unwrapped = unwrapPolygonVertices(vertices);
3059
+ const steps = polygonSubdivisionSteps(vertices);
3060
+ for (let edge = 0; edge < unwrapped.length; edge += 1) {
3061
+ const start = unwrapped[edge];
3062
+ const end = unwrapped[(edge + 1) % unwrapped.length];
3063
+ for (let step = 0; step < steps; step += 1) {
3064
+ const progress = step / steps;
3065
+ const point = panoPositionToVector3(
3066
+ {
3067
+ yaw: start.unwrappedYaw + (end.unwrappedYaw - start.unwrappedYaw) * progress,
3068
+ pitch: start.pitch + (end.pitch - start.pitch) * progress
3069
+ },
3070
+ POLYGON_RADIUS
3071
+ ).applyMatrix4(worldToLocal);
3072
+ positions.push(point.x, point.y, point.z);
3073
+ }
3074
+ }
3075
+ positions.push(...positions.slice(0, 3));
3076
+ return positions;
3077
+ }
3078
+ function makeStrokeLine(positions, color, width, opacity) {
3079
+ const geometry = new LineGeometry();
3080
+ geometry.setPositions(positions);
3081
+ const material = new LineMaterial({
3082
+ color,
3083
+ depthTest: true,
3084
+ depthWrite: false,
3085
+ linewidth: Math.max(0.5, width),
3086
+ opacity,
3087
+ transparent: true,
3088
+ worldUnits: false,
3089
+ alphaToCoverage: true
3090
+ });
3091
+ const line = new Line2(geometry, material);
3092
+ line.computeLineDistances();
3093
+ return line;
3094
+ }
3095
+ function disposeStrokeLine(line) {
3096
+ if (!line) return;
3097
+ line.geometry.dispose();
3098
+ line.material.dispose();
3099
+ }
3100
+ function translateVertices(vertices, from, to) {
3101
+ const yawOffset = normalizePanoYaw(to.yaw - from.yaw);
3102
+ const pitchOffset = to.pitch - from.pitch;
3103
+ return vertices.map((vertex) => ({
3104
+ yaw: normalizePanoYaw(vertex.yaw + yawOffset),
3105
+ pitch: clampPanoPitch(vertex.pitch + pitchOffset)
3106
+ }));
3107
+ }
3108
+ function PolygonHotspot({
3109
+ id,
3110
+ vertices,
3111
+ fill = "#df6b42",
3112
+ fillOpacity = 0.34,
3113
+ stroke = "#f5fbfc",
3114
+ strokeWidth = 2,
3115
+ strokeOpacity = 1,
3116
+ interactive = true,
3117
+ renderOrder = 10,
3118
+ draggable = false,
3119
+ onVerticesChange,
3120
+ onDragStart,
3121
+ onDragEnd,
3122
+ onInvalid,
3123
+ ...anchorProps
3124
+ }) {
3125
+ const gl = useThree7((state) => state.gl);
3126
+ const size = useThree7((state) => state.size);
3127
+ const issues = useMemo7(() => validatePolygonVertices(vertices), [vertices]);
3128
+ const valid = issues.length === 0;
3129
+ const center = useMemo7(() => valid ? polygonCenter(vertices) : null, [valid, vertices]);
3130
+ const fillGeometry = useMemo7(
3131
+ () => valid && center ? makeFillGeometry(vertices, center) : null,
3132
+ [center, valid, vertices]
3133
+ );
3134
+ const strokePositions = useMemo7(
3135
+ () => valid && center ? makeStrokePositions(vertices, center) : null,
3136
+ [center, valid, vertices]
3137
+ );
3138
+ const resolvedStrokeOpacity = Math.max(0, Math.min(strokeOpacity, 1));
3139
+ const devicePixelRatio = gl.getPixelRatio();
3140
+ const strokeLine = useMemo7(
3141
+ () => strokePositions ? makeStrokeLine(strokePositions, stroke, strokeWidth * devicePixelRatio, resolvedStrokeOpacity) : null,
3142
+ [devicePixelRatio, resolvedStrokeOpacity, stroke, strokePositions, strokeWidth]
3143
+ );
3144
+ const focusStrokeLine = useMemo7(
3145
+ () => strokePositions ? makeStrokeLine(strokePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3146
+ [devicePixelRatio, strokePositions, strokeWidth]
3147
+ );
3148
+ const startVerticesRef = useRef10(null);
3149
+ const issuesKey = issues.map((issue) => issue.code).join(",");
3150
+ useEffect11(() => {
3151
+ if (!valid) onInvalid?.(issues);
3152
+ }, [issues, issuesKey, onInvalid, valid]);
3153
+ useEffect11(() => () => fillGeometry?.dispose(), [fillGeometry]);
3154
+ useEffect11(() => () => disposeStrokeLine(strokeLine), [strokeLine]);
3155
+ useEffect11(() => () => disposeStrokeLine(focusStrokeLine), [focusStrokeLine]);
3156
+ useEffect11(() => {
3157
+ const width = gl.domElement.width;
3158
+ const height = gl.domElement.height;
3159
+ for (const line of [strokeLine, focusStrokeLine]) {
3160
+ if (line) line.material.resolution.set(width, height);
3161
+ }
3162
+ }, [focusStrokeLine, gl, size.height, size.width, strokeLine]);
3163
+ if (!valid || !center || !fillGeometry || !strokeLine || !focusStrokeLine) return null;
3164
+ const emitVerticesChange = (event, callback) => {
3165
+ const startVertices = startVerticesRef.current ?? cloneVertices(vertices);
3166
+ const nextVertices = translateVertices(startVertices, event.startPosition, event.position);
3167
+ callback?.({
3168
+ id,
3169
+ vertices: nextVertices,
3170
+ startVertices,
3171
+ position: event.position,
3172
+ startPosition: event.startPosition
3173
+ });
3174
+ };
3175
+ return /* @__PURE__ */ jsxs8(
3176
+ HotspotAnchor,
3177
+ {
3178
+ ...anchorProps,
3179
+ id,
3180
+ position: center,
3181
+ width: 1,
3182
+ height: 1,
3183
+ internalPlacement: "floating",
3184
+ distance: POLYGON_RADIUS,
3185
+ internalOrientation: "surface",
3186
+ renderOrder,
3187
+ draggable,
3188
+ interactive,
3189
+ useAngularScale: false,
3190
+ focusContent: /* @__PURE__ */ jsx10("primitive", { object: focusStrokeLine, renderOrder: renderOrder + 2 }),
3191
+ onDragStart: (event) => {
3192
+ startVerticesRef.current = cloneVertices(vertices);
3193
+ emitVerticesChange(event, onDragStart);
3194
+ },
3195
+ onPositionChange: (event) => emitVerticesChange(event, onVerticesChange),
3196
+ onDragEnd: (event) => {
3197
+ emitVerticesChange(event, onDragEnd);
3198
+ startVerticesRef.current = null;
3199
+ },
3200
+ children: [
3201
+ /* @__PURE__ */ jsx10("mesh", { geometry: fillGeometry, renderOrder, children: /* @__PURE__ */ jsx10(
3202
+ "meshBasicMaterial",
3203
+ {
3204
+ color: fill,
3205
+ depthWrite: false,
3206
+ opacity: Math.max(0, Math.min(fillOpacity, 1)),
3207
+ side: DoubleSide3,
3208
+ transparent: true
3209
+ }
3210
+ ) }),
3211
+ /* @__PURE__ */ jsx10("primitive", { object: strokeLine, renderOrder: renderOrder + 1 })
3212
+ ]
3213
+ }
3214
+ );
3215
+ }
3216
+
3217
+ // src/hotspot/polyline-hotspot.tsx
3218
+ import { useThree as useThree8 } from "@react-three/fiber";
3219
+ import { useEffect as useEffect12, useMemo as useMemo8, useRef as useRef11 } from "react";
3220
+ import {
3221
+ MathUtils as MathUtils5,
3222
+ Matrix4 as Matrix44,
3223
+ Quaternion as Quaternion4,
3224
+ Vector3 as Vector37
3225
+ } from "three";
3226
+ import { Line2 as Line22 } from "three/addons/lines/Line2.js";
3227
+ import { LineGeometry as LineGeometry2 } from "three/addons/lines/LineGeometry.js";
3228
+ import { LineMaterial as LineMaterial2 } from "three/addons/lines/LineMaterial.js";
3229
+ import { jsx as jsx11 } from "react/jsx-runtime";
3230
+ var POLYLINE_RADIUS = 990;
3231
+ var MAX_SEGMENT_DEGREES = 7;
3232
+ function cloneVertices2(vertices) {
3233
+ return vertices.map((vertex) => normalizePanoPosition(vertex));
3234
+ }
3235
+ function unwrapVertices(vertices) {
3236
+ const unwrapped = [];
3237
+ for (const vertex of vertices) {
3238
+ const normalized = normalizePanoPosition(vertex);
3239
+ const previous = unwrapped[unwrapped.length - 1];
3240
+ if (!previous) {
3241
+ unwrapped.push({ ...normalized, unwrappedYaw: normalized.yaw });
3242
+ continue;
3243
+ }
3244
+ let yaw = normalized.yaw;
3245
+ while (yaw - previous.unwrappedYaw > 180) yaw -= 360;
3246
+ while (yaw - previous.unwrappedYaw <= -180) yaw += 360;
3247
+ unwrapped.push({ ...normalized, unwrappedYaw: yaw });
3248
+ }
3249
+ return unwrapped;
3250
+ }
3251
+ function polylineCenter(vertices) {
3252
+ const sum = vertices.reduce(
3253
+ (total, vertex) => total.add(panoPositionToVector3(vertex).normalize()),
3254
+ new Vector37()
3255
+ );
3256
+ return sum.lengthSq() > 1e-8 ? vector3ToPanoPosition(sum) : normalizePanoPosition(vertices[0]);
3257
+ }
3258
+ function surfaceQuaternion2(position) {
3259
+ const outward = panoPositionToVector3(position).normalize();
3260
+ const inward = outward.clone().negate();
3261
+ const pitch = MathUtils5.degToRad(position.pitch);
3262
+ const yaw = MathUtils5.degToRad(position.yaw);
3263
+ const localUp = new Vector37(
3264
+ -Math.sin(pitch) * Math.sin(yaw),
3265
+ Math.cos(pitch),
3266
+ Math.sin(pitch) * Math.cos(yaw)
3267
+ ).normalize();
3268
+ return new Quaternion4().setFromRotationMatrix(
3269
+ new Matrix44().makeBasis(localUp.clone().cross(inward).normalize(), localUp, inward)
3270
+ );
3271
+ }
3272
+ function makeWorldToLocal2(center) {
3273
+ return new Matrix44().compose(
3274
+ panoPositionToVector3(center, POLYLINE_RADIUS),
3275
+ surfaceQuaternion2(center),
3276
+ new Vector37(1, 1, 1)
3277
+ ).invert();
3278
+ }
3279
+ function subdivisionSteps(vertices) {
3280
+ let maxAngle = 0;
3281
+ for (let index = 0; index < vertices.length - 1; index += 1) {
3282
+ maxAngle = Math.max(
3283
+ maxAngle,
3284
+ panoPositionToVector3(vertices[index]).angleTo(
3285
+ panoPositionToVector3(vertices[index + 1])
3286
+ )
3287
+ );
3288
+ }
3289
+ return Math.min(
3290
+ 8,
3291
+ Math.max(1, Math.ceil(MathUtils5.radToDeg(maxAngle) / MAX_SEGMENT_DEGREES))
3292
+ );
3293
+ }
3294
+ function makeLinePositions(vertices, center) {
3295
+ const positions = [];
3296
+ const worldToLocal = makeWorldToLocal2(center);
3297
+ const unwrapped = unwrapVertices(vertices);
3298
+ const steps = subdivisionSteps(vertices);
3299
+ for (let edge = 0; edge < unwrapped.length - 1; edge += 1) {
3300
+ const start = unwrapped[edge];
3301
+ const end = unwrapped[edge + 1];
3302
+ for (let step = 0; step < steps; step += 1) {
3303
+ const progress = step / steps;
3304
+ const point = panoPositionToVector3(
3305
+ {
3306
+ yaw: start.unwrappedYaw + (end.unwrappedYaw - start.unwrappedYaw) * progress,
3307
+ pitch: start.pitch + (end.pitch - start.pitch) * progress
3308
+ },
3309
+ POLYLINE_RADIUS
3310
+ ).applyMatrix4(worldToLocal);
3311
+ positions.push(point.x, point.y, point.z);
3312
+ }
3313
+ }
3314
+ const last = unwrapped[unwrapped.length - 1];
3315
+ const lastPoint = panoPositionToVector3(last, POLYLINE_RADIUS).applyMatrix4(worldToLocal);
3316
+ positions.push(lastPoint.x, lastPoint.y, lastPoint.z);
3317
+ return positions;
3318
+ }
3319
+ function makeLine(positions, color, width, opacity) {
3320
+ const geometry = new LineGeometry2();
3321
+ geometry.setPositions(positions);
3322
+ const material = new LineMaterial2({
3323
+ color,
3324
+ depthTest: true,
3325
+ depthWrite: false,
3326
+ linewidth: Math.max(0.5, width),
3327
+ opacity,
3328
+ transparent: true,
3329
+ worldUnits: false,
3330
+ alphaToCoverage: true
3331
+ });
3332
+ const line = new Line22(geometry, material);
3333
+ line.computeLineDistances();
3334
+ return line;
3335
+ }
3336
+ function disposeLine(line) {
3337
+ if (!line) return;
3338
+ line.geometry.dispose();
3339
+ line.material.dispose();
3340
+ }
3341
+ function translateVertices2(vertices, from, to) {
3342
+ const yawOffset = normalizePanoYaw(to.yaw - from.yaw);
3343
+ const pitchOffset = to.pitch - from.pitch;
3344
+ return vertices.map((vertex) => ({
3345
+ yaw: normalizePanoYaw(vertex.yaw + yawOffset),
3346
+ pitch: clampPanoPitch(vertex.pitch + pitchOffset)
3347
+ }));
3348
+ }
3349
+ function validatePolylineVertices(vertices) {
3350
+ return vertices.length < 2 ? [{ code: "too_few_vertices", message: "A polyline needs at least two vertices." }] : [];
3351
+ }
3352
+ function PolylineHotspot({
3353
+ id,
3354
+ vertices,
3355
+ stroke = "#f5fbfc",
3356
+ strokeWidth = 2,
3357
+ strokeOpacity = 1,
3358
+ renderOrder = 10,
3359
+ draggable = false,
3360
+ onVerticesChange,
3361
+ onDragStart,
3362
+ onDragEnd,
3363
+ onInvalid,
3364
+ ...anchorProps
3365
+ }) {
3366
+ const gl = useThree8((state) => state.gl);
3367
+ const size = useThree8((state) => state.size);
3368
+ const issues = useMemo8(() => validatePolylineVertices(vertices), [vertices]);
3369
+ const valid = issues.length === 0;
3370
+ const center = useMemo8(() => valid ? polylineCenter(vertices) : null, [valid, vertices]);
3371
+ const linePositions = useMemo8(
3372
+ () => valid && center ? makeLinePositions(vertices, center) : null,
3373
+ [center, valid, vertices]
3374
+ );
3375
+ const devicePixelRatio = gl.getPixelRatio();
3376
+ const opacity = Math.max(0, Math.min(strokeOpacity, 1));
3377
+ const line = useMemo8(
3378
+ () => linePositions ? makeLine(linePositions, stroke, strokeWidth * devicePixelRatio, opacity) : null,
3379
+ [devicePixelRatio, linePositions, opacity, stroke, strokeWidth]
3380
+ );
3381
+ const focusLine = useMemo8(
3382
+ () => linePositions ? makeLine(linePositions, "#75cbd3", (strokeWidth + 2) * devicePixelRatio, 1) : null,
3383
+ [devicePixelRatio, linePositions, strokeWidth]
3384
+ );
3385
+ const startVerticesRef = useRef11(null);
3386
+ useEffect12(() => {
3387
+ if (!valid) onInvalid?.(issues);
3388
+ }, [issues, onInvalid, valid]);
3389
+ useEffect12(() => () => disposeLine(line), [line]);
3390
+ useEffect12(() => () => disposeLine(focusLine), [focusLine]);
3391
+ useEffect12(() => {
3392
+ const width = gl.domElement.width;
3393
+ const height = gl.domElement.height;
3394
+ for (const nextLine of [line, focusLine]) {
3395
+ if (nextLine) nextLine.material.resolution.set(width, height);
3396
+ }
3397
+ }, [focusLine, gl, line, size.height, size.width]);
3398
+ if (!valid || !center || !line || !focusLine) return null;
3399
+ const emitVerticesChange = (event, callback) => {
3400
+ const startVertices = startVerticesRef.current ?? cloneVertices2(vertices);
3401
+ callback?.({
3402
+ id,
3403
+ vertices: translateVertices2(startVertices, event.startPosition, event.position),
3404
+ startVertices,
3405
+ position: event.position,
3406
+ startPosition: event.startPosition
3407
+ });
3408
+ };
3409
+ return /* @__PURE__ */ jsx11(
3410
+ HotspotAnchor,
3411
+ {
3412
+ ...anchorProps,
3413
+ id,
3414
+ position: center,
3415
+ width: 1,
3416
+ height: 1,
3417
+ internalPlacement: "floating",
3418
+ distance: POLYLINE_RADIUS,
3419
+ internalOrientation: "surface",
3420
+ renderOrder,
3421
+ draggable,
3422
+ useAngularScale: false,
3423
+ focusContent: /* @__PURE__ */ jsx11("primitive", { object: focusLine, renderOrder: renderOrder + 2 }),
3424
+ onDragStart: (event) => {
3425
+ startVerticesRef.current = cloneVertices2(vertices);
3426
+ emitVerticesChange(event, onDragStart);
3427
+ },
3428
+ onPositionChange: (event) => emitVerticesChange(event, onVerticesChange),
3429
+ onDragEnd: (event) => {
3430
+ emitVerticesChange(event, onDragEnd);
3431
+ startVerticesRef.current = null;
3432
+ },
3433
+ children: /* @__PURE__ */ jsx11("primitive", { object: line, renderOrder: renderOrder + 1 })
3434
+ }
3435
+ );
3436
+ }
3437
+
3438
+ // src/hotspot/graphic-hotspot.tsx
3439
+ import { useEffect as useEffect13, useRef as useRef12, useState as useState7 } from "react";
3440
+ import {
3441
+ CanvasTexture,
3442
+ ClampToEdgeWrapping as ClampToEdgeWrapping4,
3443
+ DoubleSide as DoubleSide4,
3444
+ LinearFilter as LinearFilter6,
3445
+ LinearMipmapLinearFilter as LinearMipmapLinearFilter3,
3446
+ SRGBColorSpace as SRGBColorSpace5
3447
+ } from "three";
3448
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
3449
+ var GRAPHIC_TEXTURE_SIZE = 512;
3450
+ function clampOpacity2(opacity) {
3451
+ if (!Number.isFinite(opacity)) {
3452
+ return 1;
3453
+ }
3454
+ return Math.max(0, Math.min(opacity, 1));
3455
+ }
3456
+ function resolveStrokeWidth(value) {
3457
+ return Number.isFinite(value) ? Math.max(value, 0) : 8;
3458
+ }
3459
+ function roundedRectanglePath(context, x, y, width, height, radius) {
3460
+ const safeRadius = Math.min(Math.max(radius, 0), width / 2, height / 2);
3461
+ context.beginPath();
3462
+ context.moveTo(x + safeRadius, y);
3463
+ context.arcTo(x + width, y, x + width, y + height, safeRadius);
3464
+ context.arcTo(x + width, y + height, x, y + height, safeRadius);
3465
+ context.arcTo(x, y + height, x, y, safeRadius);
3466
+ context.arcTo(x, y, x + width, y, safeRadius);
3467
+ context.closePath();
3468
+ }
3469
+ function paintPath(context, graphic, path) {
3470
+ if (graphic.fill) {
3471
+ context.fillStyle = graphic.fill;
3472
+ path ? context.fill(path) : context.fill();
3473
+ }
3474
+ if (graphic.stroke) {
3475
+ context.strokeStyle = graphic.stroke;
3476
+ context.lineWidth = resolveStrokeWidth(graphic.strokeWidth);
3477
+ path ? context.stroke(path) : context.stroke();
3478
+ }
3479
+ }
3480
+ function drawGraphic(context, graphic) {
3481
+ const padding = Math.max(resolveStrokeWidth(graphic.strokeWidth) / 2 + 2, 4);
3482
+ const drawableSize = GRAPHIC_TEXTURE_SIZE - padding * 2;
3483
+ context.clearRect(0, 0, GRAPHIC_TEXTURE_SIZE, GRAPHIC_TEXTURE_SIZE);
3484
+ context.lineJoin = "round";
3485
+ context.lineCap = "round";
3486
+ if (graphic.kind === "circle") {
3487
+ context.beginPath();
3488
+ context.arc(
3489
+ GRAPHIC_TEXTURE_SIZE / 2,
3490
+ GRAPHIC_TEXTURE_SIZE / 2,
3491
+ drawableSize / 2,
3492
+ 0,
3493
+ Math.PI * 2
3494
+ );
3495
+ paintPath(context, graphic);
3496
+ return;
3497
+ }
3498
+ if (graphic.kind === "rectangle") {
3499
+ roundedRectanglePath(
3500
+ context,
3501
+ padding,
3502
+ padding,
3503
+ drawableSize,
3504
+ drawableSize,
3505
+ graphic.cornerRadius ?? 0
3506
+ );
3507
+ paintPath(context, graphic);
3508
+ return;
3509
+ }
3510
+ if (graphic.kind === "ring") {
3511
+ const center = GRAPHIC_TEXTURE_SIZE / 2;
3512
+ const outerRadius = drawableSize / 2;
3513
+ const innerRadius = Math.max(
3514
+ 0,
3515
+ Math.min(outerRadius - 1, (graphic.innerRadius ?? 0.58) * outerRadius)
3516
+ );
3517
+ context.beginPath();
3518
+ context.arc(center, center, outerRadius, 0, Math.PI * 2);
3519
+ context.moveTo(center + innerRadius, center);
3520
+ context.arc(center, center, innerRadius, 0, Math.PI * 2, true);
3521
+ if (graphic.fill ?? graphic.stroke) {
3522
+ context.fillStyle = graphic.fill ?? graphic.stroke ?? "#ffffff";
3523
+ context.fill("evenodd");
3524
+ }
3525
+ if (graphic.stroke) {
3526
+ context.strokeStyle = graphic.stroke;
3527
+ context.lineWidth = resolveStrokeWidth(graphic.strokeWidth);
3528
+ context.stroke();
3529
+ }
3530
+ return;
3531
+ }
3532
+ const [x, y, width, height] = graphic.viewBox;
3533
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
3534
+ throw new Error("SVG path viewBox must have a positive width and height.");
3535
+ }
3536
+ const path = new Path2D(graphic.path);
3537
+ context.save();
3538
+ context.translate(padding, padding);
3539
+ context.scale(drawableSize / width, drawableSize / height);
3540
+ context.translate(-x, -y);
3541
+ paintPath(context, graphic, path);
3542
+ context.restore();
3543
+ }
3544
+ function createGraphicTexture(graphic) {
3545
+ const canvas = document.createElement("canvas");
3546
+ canvas.width = GRAPHIC_TEXTURE_SIZE;
3547
+ canvas.height = GRAPHIC_TEXTURE_SIZE;
3548
+ const context = canvas.getContext("2d");
3549
+ if (!context) {
3550
+ throw new Error("Canvas 2D is unavailable for this graphic hotspot.");
3551
+ }
3552
+ drawGraphic(context, graphic);
3553
+ const texture = new CanvasTexture(canvas);
3554
+ texture.colorSpace = SRGBColorSpace5;
3555
+ texture.wrapS = ClampToEdgeWrapping4;
3556
+ texture.wrapT = ClampToEdgeWrapping4;
3557
+ texture.magFilter = LinearFilter6;
3558
+ texture.minFilter = LinearMipmapLinearFilter3;
3559
+ texture.generateMipmaps = true;
3560
+ texture.needsUpdate = true;
3561
+ return texture;
3562
+ }
3563
+ function useGraphicTexture(graphic, onLoad, onError) {
3564
+ const [texture, setTexture] = useState7(null);
3565
+ const onLoadRef = useRef12(onLoad);
3566
+ const onErrorRef = useRef12(onError);
3567
+ onLoadRef.current = onLoad;
3568
+ onErrorRef.current = onError;
3569
+ useEffect13(() => {
3570
+ let nextTexture = null;
3571
+ try {
3572
+ nextTexture = createGraphicTexture(graphic);
3573
+ setTexture(nextTexture);
3574
+ onLoadRef.current?.(nextTexture);
3575
+ } catch (error) {
3576
+ setTexture(null);
3577
+ onErrorRef.current?.(error);
3578
+ }
3579
+ return () => {
3580
+ nextTexture?.dispose();
3581
+ };
3582
+ }, [graphic]);
3583
+ return texture;
3584
+ }
3585
+ function CanvasGraphicHotspot({
3586
+ graphic,
3587
+ width = 10,
3588
+ height = 10,
3589
+ opacity,
3590
+ onLoad,
3591
+ onError,
3592
+ ...anchorProps
3593
+ }) {
3594
+ const texture = useGraphicTexture(graphic, onLoad, onError);
3595
+ return /* @__PURE__ */ jsx12(HotspotAnchor, { ...anchorProps, height, width, children: /* @__PURE__ */ jsxs9("mesh", { children: [
3596
+ /* @__PURE__ */ jsx12("planeGeometry", { args: [1, 1] }),
3597
+ /* @__PURE__ */ jsx12(
3598
+ "meshBasicMaterial",
3599
+ {
3600
+ map: texture,
3601
+ opacity: texture ? clampOpacity2(opacity) : 0,
3602
+ side: DoubleSide4,
3603
+ toneMapped: false,
3604
+ transparent: true
3605
+ }
3606
+ )
3607
+ ] }) });
3608
+ }
3609
+ function GraphicHotspot(props) {
3610
+ const { graphic, ...restProps } = props;
3611
+ if (graphic.kind === "svg") {
3612
+ const imageProps = restProps;
3613
+ return /* @__PURE__ */ jsx12(ImageHotspot, { ...imageProps, src: graphic.src });
3614
+ }
3615
+ return /* @__PURE__ */ jsx12(CanvasGraphicHotspot, { ...restProps, graphic });
3616
+ }
3617
+
3618
+ // src/hotspot/sequence-hotspot.tsx
3619
+ import { useFrame as useFrame6, useThree as useThree9 } from "@react-three/fiber";
3620
+ import { useEffect as useEffect14, useRef as useRef13, useState as useState8 } from "react";
3621
+ import {
3622
+ ClampToEdgeWrapping as ClampToEdgeWrapping5,
3623
+ DoubleSide as DoubleSide5,
3624
+ LinearFilter as LinearFilter7,
3625
+ LinearMipmapLinearFilter as LinearMipmapLinearFilter4,
3626
+ SRGBColorSpace as SRGBColorSpace6,
3627
+ TextureLoader as TextureLoader5
3628
+ } from "three";
3629
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
3630
+ var DEFAULT_SEQUENCE_FPS = 12;
3631
+ function clampOpacity3(opacity) {
3632
+ if (!Number.isFinite(opacity)) {
3633
+ return 1;
3634
+ }
3635
+ return Math.max(0, Math.min(opacity, 1));
3636
+ }
3637
+ function resolveFps(value) {
3638
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_SEQUENCE_FPS;
3639
+ }
3640
+ function resolveFrameCount(value) {
3641
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
3642
+ }
3643
+ function useSpriteTexture({
3644
+ id,
3645
+ src,
3646
+ onError,
3647
+ onLoadProgress
3648
+ }) {
3649
+ const gl = useThree9((state) => state.gl);
3650
+ const [texture, setTexture] = useState8(null);
3651
+ const onErrorRef = useRef13(onError);
3652
+ const onLoadProgressRef = useRef13(onLoadProgress);
3653
+ onErrorRef.current = onError;
3654
+ onLoadProgressRef.current = onLoadProgress;
3655
+ useEffect14(() => {
3656
+ let active = true;
3657
+ setTexture(null);
3658
+ if (!src) {
3659
+ onLoadProgressRef.current?.({ id, total: 1, loaded: 0, failed: 1 });
3660
+ return () => {
3661
+ active = false;
3662
+ };
3663
+ }
3664
+ const loader = new TextureLoader5();
3665
+ loader.load(
3666
+ src,
3667
+ (loadedTexture) => {
3668
+ if (!active) {
3669
+ loadedTexture.dispose();
3670
+ return;
3671
+ }
3672
+ loadedTexture.colorSpace = SRGBColorSpace6;
3673
+ loadedTexture.wrapS = ClampToEdgeWrapping5;
3674
+ loadedTexture.wrapT = ClampToEdgeWrapping5;
3675
+ loadedTexture.magFilter = LinearFilter7;
3676
+ loadedTexture.minFilter = LinearMipmapLinearFilter4;
3677
+ loadedTexture.generateMipmaps = true;
3678
+ loadedTexture.anisotropy = gl.capabilities.getMaxAnisotropy();
3679
+ loadedTexture.needsUpdate = true;
3680
+ onLoadProgressRef.current?.({ id, total: 1, loaded: 1, failed: 0 });
3681
+ setTexture(loadedTexture);
3682
+ },
3683
+ void 0,
3684
+ (error) => {
3685
+ if (!active) {
3686
+ return;
3687
+ }
3688
+ onLoadProgressRef.current?.({ id, total: 1, loaded: 0, failed: 1 });
3689
+ onErrorRef.current?.({ id, src, error });
3690
+ }
3691
+ );
3692
+ return () => {
3693
+ active = false;
3694
+ };
3695
+ }, [gl, id, src]);
3696
+ useEffect14(
3697
+ () => () => {
3698
+ texture?.dispose();
3699
+ },
3700
+ [texture]
3701
+ );
3702
+ return texture;
3703
+ }
3704
+ function SequenceHotspot({
3705
+ id,
3706
+ src,
3707
+ frameCount: requestedFrameCount,
3708
+ frameDirection = "vertical",
3709
+ playing,
3710
+ fps,
3711
+ loop = true,
3712
+ width = 12,
3713
+ height = 8,
3714
+ opacity,
3715
+ onFrameChange,
3716
+ onLoadProgress,
3717
+ onPlaybackStateChange,
3718
+ onEnded,
3719
+ onError,
3720
+ ...anchorProps
3721
+ }) {
3722
+ const texture = useSpriteTexture({ id, src, onError, onLoadProgress });
3723
+ const frameCount = resolveFrameCount(requestedFrameCount);
3724
+ const safeFps = resolveFps(fps);
3725
+ const [frameIndex, setFrameIndex] = useState8(0);
3726
+ const elapsedRef = useRef13(0);
3727
+ const frameIndexRef = useRef13(0);
3728
+ const endedRef = useRef13(false);
3729
+ const lastPlaybackStateRef = useRef13(null);
3730
+ const onFrameChangeRef = useRef13(onFrameChange);
3731
+ const onPlaybackStateChangeRef = useRef13(onPlaybackStateChange);
3732
+ const onEndedRef = useRef13(onEnded);
3733
+ onFrameChangeRef.current = onFrameChange;
3734
+ onPlaybackStateChangeRef.current = onPlaybackStateChange;
3735
+ onEndedRef.current = onEnded;
3736
+ useEffect14(() => {
3737
+ elapsedRef.current = 0;
3738
+ frameIndexRef.current = 0;
3739
+ endedRef.current = false;
3740
+ setFrameIndex(0);
3741
+ }, [frameCount, frameDirection, src]);
3742
+ useEffect14(() => {
3743
+ if (!texture) {
3744
+ return;
3745
+ }
3746
+ texture.repeat.set(
3747
+ frameDirection === "horizontal" ? 1 / frameCount : 1,
3748
+ frameDirection === "vertical" ? 1 / frameCount : 1
3749
+ );
3750
+ texture.offset.set(
3751
+ frameDirection === "horizontal" ? frameIndex / frameCount : 0,
3752
+ frameDirection === "vertical" ? 1 - (frameIndex + 1) / frameCount : 0
3753
+ );
3754
+ texture.needsUpdate = true;
3755
+ }, [frameCount, frameDirection, frameIndex, texture]);
3756
+ useEffect14(() => {
3757
+ if (!texture) {
3758
+ return;
3759
+ }
3760
+ onFrameChangeRef.current?.({ id, index: 0, src });
3761
+ }, [id, src, texture]);
3762
+ useEffect14(() => {
3763
+ if (!texture) {
3764
+ return;
3765
+ }
3766
+ if (playing && endedRef.current) {
3767
+ elapsedRef.current = 0;
3768
+ frameIndexRef.current = 0;
3769
+ endedRef.current = false;
3770
+ setFrameIndex(0);
3771
+ }
3772
+ if (!playing && endedRef.current) {
3773
+ return;
3774
+ }
3775
+ const nextState = playing ? "playing" : "paused";
3776
+ if (lastPlaybackStateRef.current !== nextState) {
3777
+ lastPlaybackStateRef.current = nextState;
3778
+ onPlaybackStateChangeRef.current?.(nextState);
3779
+ }
3780
+ }, [playing, texture]);
3781
+ useFrame6((_, deltaSeconds) => {
3782
+ if (!texture || !playing || endedRef.current) {
3783
+ return;
3784
+ }
3785
+ elapsedRef.current += deltaSeconds;
3786
+ const steps = Math.floor(elapsedRef.current * safeFps);
3787
+ if (steps === 0) {
3788
+ return;
3789
+ }
3790
+ elapsedRef.current -= steps / safeFps;
3791
+ let nextIndex = frameIndexRef.current + steps;
3792
+ if (nextIndex >= frameCount) {
3793
+ if (loop) {
3794
+ nextIndex %= frameCount;
3795
+ } else {
3796
+ nextIndex = frameCount - 1;
3797
+ endedRef.current = true;
3798
+ if (lastPlaybackStateRef.current !== "ended") {
3799
+ lastPlaybackStateRef.current = "ended";
3800
+ onPlaybackStateChangeRef.current?.("ended");
3801
+ onEndedRef.current?.();
3802
+ }
3803
+ }
3804
+ }
3805
+ if (nextIndex !== frameIndexRef.current) {
3806
+ frameIndexRef.current = nextIndex;
3807
+ setFrameIndex(nextIndex);
3808
+ onFrameChangeRef.current?.({ id, index: nextIndex, src });
3809
+ }
3810
+ });
3811
+ return /* @__PURE__ */ jsx13(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs10("mesh", { children: [
3812
+ /* @__PURE__ */ jsx13("planeGeometry", { args: [1, 1] }),
3813
+ /* @__PURE__ */ jsx13(
3814
+ "meshBasicMaterial",
3815
+ {
3816
+ map: texture,
3817
+ opacity: texture ? clampOpacity3(opacity) : 0,
3818
+ side: DoubleSide5,
3819
+ toneMapped: false,
3820
+ transparent: true
3821
+ }
3822
+ )
3823
+ ] }) });
3824
+ }
3825
+
3826
+ // src/hotspot/video-hotspot.tsx
3827
+ import { useThree as useThree10 } from "@react-three/fiber";
3828
+ import { useEffect as useEffect15, useRef as useRef14, useState as useState9 } from "react";
3829
+ import {
3830
+ ClampToEdgeWrapping as ClampToEdgeWrapping6,
3831
+ DoubleSide as DoubleSide6,
3832
+ LinearFilter as LinearFilter8,
3833
+ LinearMipmapLinearFilter as LinearMipmapLinearFilter5,
3834
+ SRGBColorSpace as SRGBColorSpace7,
3835
+ TextureLoader as TextureLoader6,
3836
+ VideoTexture
3837
+ } from "three";
3838
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
3839
+ function clampOpacity4(opacity) {
3840
+ if (!Number.isFinite(opacity)) {
3841
+ return 1;
3842
+ }
3843
+ return Math.max(0, Math.min(opacity, 1));
3844
+ }
3845
+ function clampVolume(volume) {
3846
+ if (!Number.isFinite(volume)) {
3847
+ return 1;
3848
+ }
3849
+ return Math.max(0, Math.min(volume, 1));
3850
+ }
3851
+ function usePosterTexture(id, poster, onError) {
3852
+ const gl = useThree10((state) => state.gl);
3853
+ const [texture, setTexture] = useState9(null);
3854
+ const onErrorRef = useRef14(onError);
3855
+ onErrorRef.current = onError;
3856
+ useEffect15(() => {
3857
+ let active = true;
3858
+ setTexture(null);
3859
+ if (!poster) {
3860
+ return () => {
3861
+ active = false;
3862
+ };
3863
+ }
3864
+ const loader = new TextureLoader6();
3865
+ loader.load(
3866
+ poster,
3867
+ (loadedTexture) => {
3868
+ if (!active) {
3869
+ loadedTexture.dispose();
3870
+ return;
3871
+ }
3872
+ loadedTexture.colorSpace = SRGBColorSpace7;
3873
+ loadedTexture.wrapS = ClampToEdgeWrapping6;
3874
+ loadedTexture.wrapT = ClampToEdgeWrapping6;
3875
+ loadedTexture.magFilter = LinearFilter8;
3876
+ loadedTexture.minFilter = LinearMipmapLinearFilter5;
3877
+ loadedTexture.generateMipmaps = true;
3878
+ loadedTexture.anisotropy = gl.capabilities.getMaxAnisotropy();
3879
+ loadedTexture.needsUpdate = true;
3880
+ setTexture(loadedTexture);
3881
+ },
3882
+ void 0,
3883
+ (error) => {
3884
+ if (active) {
3885
+ onErrorRef.current?.({ id, source: "poster", error });
3886
+ }
3887
+ }
3888
+ );
3889
+ return () => {
3890
+ active = false;
3891
+ };
3892
+ }, [gl, id, poster]);
3893
+ useEffect15(
3894
+ () => () => {
3895
+ texture?.dispose();
3896
+ },
3897
+ [texture]
3898
+ );
3899
+ return texture;
3900
+ }
3901
+ function useVideoResource({
3902
+ crossOrigin,
3903
+ id,
3904
+ onEnded,
3905
+ onError,
3906
+ onPlaybackStateChange,
3907
+ playsInline,
3908
+ poster,
3909
+ src
3910
+ }) {
3911
+ const [resource, setResource] = useState9(null);
3912
+ const onEndedRef = useRef14(onEnded);
3913
+ const onErrorRef = useRef14(onError);
3914
+ const onPlaybackStateChangeRef = useRef14(onPlaybackStateChange);
3915
+ onEndedRef.current = onEnded;
3916
+ onErrorRef.current = onError;
3917
+ onPlaybackStateChangeRef.current = onPlaybackStateChange;
3918
+ useEffect15(() => {
3919
+ if (!src) {
3920
+ setResource(null);
3921
+ return;
3922
+ }
3923
+ let active = true;
3924
+ const video = document.createElement("video");
3925
+ if (crossOrigin !== void 0) {
3926
+ video.crossOrigin = crossOrigin;
3927
+ }
3928
+ video.playsInline = playsInline ?? true;
3929
+ video.poster = poster ?? "";
3930
+ const texture = new VideoTexture(video);
3931
+ texture.colorSpace = SRGBColorSpace7;
3932
+ texture.wrapS = ClampToEdgeWrapping6;
3933
+ texture.wrapT = ClampToEdgeWrapping6;
3934
+ texture.magFilter = LinearFilter8;
3935
+ texture.minFilter = LinearFilter8;
3936
+ texture.generateMipmaps = false;
3937
+ const notifyState = (state) => {
3938
+ if (active) {
3939
+ onPlaybackStateChangeRef.current?.(state);
3940
+ }
3941
+ };
3942
+ const markReady = () => {
3943
+ if (!active) {
3944
+ return;
3945
+ }
3946
+ setResource(
3947
+ (current) => current?.video === video ? { ...current, ready: true } : current
3948
+ );
3949
+ };
3950
+ const reportError = () => {
3951
+ if (active) {
3952
+ onErrorRef.current?.({
3953
+ id,
3954
+ source: "video",
3955
+ error: video.error ?? new Error(`Unable to load video source: ${src}`)
3956
+ });
3957
+ }
3958
+ };
3959
+ const handleEnded = () => {
3960
+ notifyState("ended");
3961
+ onEndedRef.current?.();
3962
+ };
3963
+ const handlePause = () => notifyState("paused");
3964
+ const handlePlay = () => notifyState("playing");
3965
+ video.addEventListener("canplay", markReady);
3966
+ video.addEventListener("ended", handleEnded);
3967
+ video.addEventListener("error", reportError);
3968
+ video.addEventListener("pause", handlePause);
3969
+ video.addEventListener("play", handlePlay);
3970
+ setResource({ video, texture, ready: false });
3971
+ video.src = src;
3972
+ video.load();
3973
+ return () => {
3974
+ active = false;
3975
+ video.removeEventListener("canplay", markReady);
3976
+ video.removeEventListener("ended", handleEnded);
3977
+ video.removeEventListener("error", reportError);
3978
+ video.removeEventListener("pause", handlePause);
3979
+ video.removeEventListener("play", handlePlay);
3980
+ video.pause();
3981
+ video.removeAttribute("src");
3982
+ video.load();
3983
+ texture.dispose();
3984
+ };
3985
+ }, [crossOrigin, id, playsInline, poster, src]);
3986
+ return resource;
3987
+ }
3988
+ function VideoHotspot({
3989
+ id,
3990
+ src,
3991
+ playing,
3992
+ poster,
3993
+ loop = false,
3994
+ muted = true,
3995
+ volume = 1,
3996
+ playsInline = true,
3997
+ preload = "metadata",
3998
+ crossOrigin,
3999
+ width = 16,
4000
+ height = 9,
4001
+ opacity,
4002
+ onPlaybackStateChange,
4003
+ onPlaybackError,
4004
+ onEnded,
4005
+ onError,
4006
+ ...anchorProps
4007
+ }) {
4008
+ const resource = useVideoResource({
4009
+ crossOrigin,
4010
+ id,
4011
+ onEnded,
4012
+ onError,
4013
+ onPlaybackStateChange,
4014
+ playsInline,
4015
+ poster,
4016
+ src
4017
+ });
4018
+ const posterTexture = usePosterTexture(id, poster, onError);
4019
+ const onPlaybackErrorRef = useRef14(onPlaybackError);
4020
+ const onPlaybackStateChangeRef = useRef14(onPlaybackStateChange);
4021
+ onPlaybackErrorRef.current = onPlaybackError;
4022
+ onPlaybackStateChangeRef.current = onPlaybackStateChange;
4023
+ useEffect15(() => {
4024
+ if (!resource) {
4025
+ return;
4026
+ }
4027
+ resource.video.loop = loop;
4028
+ resource.video.muted = muted;
4029
+ resource.video.volume = clampVolume(volume);
4030
+ resource.video.playsInline = playsInline;
4031
+ resource.video.preload = preload;
4032
+ }, [loop, muted, playsInline, preload, resource, volume]);
4033
+ useEffect15(() => {
4034
+ if (!resource) {
4035
+ return;
4036
+ }
4037
+ let active = true;
4038
+ if (!playing) {
4039
+ resource.video.pause();
4040
+ return () => {
4041
+ active = false;
4042
+ };
4043
+ }
4044
+ void resource.video.play().catch((error) => {
4045
+ if (!active) {
4046
+ return;
4047
+ }
4048
+ onPlaybackStateChangeRef.current?.("blocked");
4049
+ onPlaybackErrorRef.current?.(error);
4050
+ });
4051
+ return () => {
4052
+ active = false;
4053
+ };
4054
+ }, [playing, resource]);
4055
+ const activeTexture = resource?.ready ? resource.texture : posterTexture;
4056
+ return /* @__PURE__ */ jsx14(HotspotAnchor, { ...anchorProps, id, height, width, children: /* @__PURE__ */ jsxs11("mesh", { children: [
4057
+ /* @__PURE__ */ jsx14("planeGeometry", { args: [1, 1] }),
4058
+ /* @__PURE__ */ jsx14(
4059
+ "meshBasicMaterial",
4060
+ {
4061
+ map: activeTexture,
4062
+ opacity: activeTexture ? clampOpacity4(opacity) : 0,
4063
+ side: DoubleSide6,
4064
+ toneMapped: false,
4065
+ transparent: true
4066
+ }
4067
+ )
4068
+ ] }) });
4069
+ }
4070
+ export {
4071
+ AutoRotate,
4072
+ GraphicHotspot,
4073
+ ImageHotspot,
4074
+ MAX_HOTSPOT_PITCH,
4075
+ PanoView,
4076
+ PanoramaScenes,
4077
+ PolygonHotspot,
4078
+ PolylineHotspot,
4079
+ SequenceHotspot,
4080
+ Sphere,
4081
+ Tile,
4082
+ VideoHotspot,
4083
+ clampPanoPitch,
4084
+ normalizePanoPosition,
4085
+ normalizePanoYaw,
4086
+ panoPositionToVector3,
4087
+ unwrapPolygonVertices,
4088
+ validatePolygonVertices,
4089
+ validatePolylineVertices,
4090
+ vector3ToPanoPosition
4091
+ };
4092
+ //# sourceMappingURL=index.mjs.map