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