@mmmmzxe/react-360-viewer 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/README.md +214 -0
- package/dist/index.d.ts +377 -0
- package/dist/index.js +1237 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ui/Badge/index.tsx +44 -0
- package/src/components/ui/Button/index.tsx +68 -0
- package/src/components/ui/Card/index.tsx +74 -0
- package/src/components/ui/Item/index.tsx +136 -0
- package/src/components/ui/Label/index.tsx +20 -0
- package/src/components/ui/Popover/index.tsx +56 -0
- package/src/components/ui/Separator/index.tsx +30 -0
- package/src/components/ui/Spinner/index.tsx +11 -0
- package/src/components/utils/index.ts +6 -0
- package/src/constants/viewer360ClassNames.ts +41 -0
- package/src/constants/viewer360Config.ts +11 -0
- package/src/constants/viewer360Labels.ts +14 -0
- package/src/constants/viewer360MarkerLabels.ts +3 -0
- package/src/feature/Viewer360.test.tsx +47 -0
- package/src/feature/Viewer360.tsx +218 -0
- package/src/feature/Viewer360AddModeBanner.tsx +20 -0
- package/src/feature/Viewer360FrameIndicator.tsx +20 -0
- package/src/feature/Viewer360HotspotOverlay.tsx +70 -0
- package/src/feature/Viewer360LoadingOverlay.tsx +28 -0
- package/src/feature/Viewer360MarkerPin.tsx +115 -0
- package/src/feature/Viewer360Toolbar.tsx +75 -0
- package/src/helpers/adjustViewerZoom.test.ts +29 -0
- package/src/helpers/adjustViewerZoom.ts +64 -0
- package/src/helpers/computeDragFrameIndex.test.ts +20 -0
- package/src/helpers/computeDragFrameIndex.ts +23 -0
- package/src/helpers/computeViewerImageLayout.test.ts +48 -0
- package/src/helpers/computeViewerImageLayout.ts +114 -0
- package/src/helpers/computeViewerPanOffset.ts +18 -0
- package/src/helpers/markerHelpers.test.ts +38 -0
- package/src/helpers/markerHelpers.ts +33 -0
- package/src/helpers/viewer360PropsHelpers.ts +46 -0
- package/src/helpers/viewerHelpers.ts +74 -0
- package/src/hooks/useViewer360.ts +306 -0
- package/src/index.ts +68 -0
- package/src/types/Viewer360Hotspot.ts +66 -0
- package/src/types/Viewer360Marker.ts +51 -0
- package/src/types/Viewer360Props.ts +108 -0
- package/src/types/index.ts +30 -0
- package/src/utils/index.ts +6 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1237 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/feature/Viewer360.tsx
|
|
4
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useState as useState3 } from "react";
|
|
5
|
+
|
|
6
|
+
// src/constants/viewer360Labels.ts
|
|
7
|
+
var defaultViewer360Labels = {
|
|
8
|
+
loading: "Loading images\u2026",
|
|
9
|
+
dragHint: "Drag to rotate \u2022 Scroll to zoom",
|
|
10
|
+
frameIndicator: ({ current, total, label }) => label ? `${label} (${current}/${total})` : `${current} / ${total}`,
|
|
11
|
+
zoom: (percent) => `${percent}%`,
|
|
12
|
+
hotspotModeActive: "Click on the image to place a hotspot",
|
|
13
|
+
addHotspot: "Add hotspot",
|
|
14
|
+
zoomIn: "Zoom in",
|
|
15
|
+
zoomOut: "Zoom out",
|
|
16
|
+
resetView: "Reset view",
|
|
17
|
+
deleteMarker: "Remove marker"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/constants/viewer360ClassNames.ts
|
|
21
|
+
var viewer360ClassNames = {
|
|
22
|
+
root: "overflow-hidden rounded-lg border bg-card text-card-foreground",
|
|
23
|
+
viewport: "relative aspect-[16/10] w-full touch-none select-none bg-muted",
|
|
24
|
+
canvas: "absolute inset-0 size-full",
|
|
25
|
+
overlay: "pointer-events-none absolute inset-0 overflow-hidden",
|
|
26
|
+
loading: "absolute inset-0 flex items-center justify-center bg-muted/80",
|
|
27
|
+
loadingText: "text-sm text-muted-foreground",
|
|
28
|
+
frameIndicator: "pointer-events-none absolute bottom-4 start-1/2 z-20 -translate-x-1/2 rounded-full border bg-background px-4 py-1.5 text-xs font-medium shadow-sm",
|
|
29
|
+
hotspotModeBanner: "pointer-events-none absolute top-4 start-1/2 z-20 -translate-x-1/2 rounded-full border border-amber-200 bg-amber-50 px-4 py-1.5 text-xs font-medium text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400",
|
|
30
|
+
toolbar: "flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3",
|
|
31
|
+
dragHint: "hidden text-xs text-muted-foreground sm:block",
|
|
32
|
+
controls: "ms-auto flex items-center gap-1.5",
|
|
33
|
+
controlButton: "",
|
|
34
|
+
controlButtonActive: "",
|
|
35
|
+
controlButtonDisabled: "",
|
|
36
|
+
zoomDisplay: "flex min-w-[3rem] items-center justify-center gap-1 rounded-md border bg-background px-2 py-1 text-xs font-medium",
|
|
37
|
+
divider: "mx-1 h-6 w-px bg-border"
|
|
38
|
+
};
|
|
39
|
+
var viewer360MarkerPinClassNames = {
|
|
40
|
+
root: "pointer-events-auto absolute z-30 -translate-x-1/2 -translate-y-1/2",
|
|
41
|
+
ping: "absolute inline-flex size-6 -translate-x-1/4 -translate-y-1/4 animate-ping rounded-full bg-destructive opacity-60",
|
|
42
|
+
dot: "relative flex size-4 items-center justify-center rounded-full border-2 border-background bg-destructive shadow-md transition-transform duration-200 hover:scale-125 focus:outline-none",
|
|
43
|
+
tooltip: "absolute bottom-6 left-1/2 z-40 w-64 -translate-x-1/2 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md",
|
|
44
|
+
tooltipHeader: "flex items-start justify-between gap-2",
|
|
45
|
+
tooltipBody: "flex min-w-0 flex-col gap-1",
|
|
46
|
+
tooltipTitle: "text-sm font-medium",
|
|
47
|
+
tooltipDescription: "mt-2 line-clamp-3 text-xs text-muted-foreground",
|
|
48
|
+
deleteButton: ""
|
|
49
|
+
};
|
|
50
|
+
var defaultViewer360ClassNames = viewer360ClassNames;
|
|
51
|
+
var defaultViewer360MarkerPinClassNames = viewer360MarkerPinClassNames;
|
|
52
|
+
|
|
53
|
+
// src/components/utils/index.ts
|
|
54
|
+
import { clsx } from "clsx";
|
|
55
|
+
import { twMerge } from "tailwind-merge";
|
|
56
|
+
function cn(...inputs) {
|
|
57
|
+
return twMerge(clsx(inputs));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/helpers/viewer360PropsHelpers.ts
|
|
61
|
+
function mergeViewer360Labels(labels) {
|
|
62
|
+
return {
|
|
63
|
+
loading: labels?.loading ?? defaultViewer360Labels.loading,
|
|
64
|
+
dragHint: labels?.dragHint ?? defaultViewer360Labels.dragHint,
|
|
65
|
+
frameIndicator: labels?.frameIndicator ?? defaultViewer360Labels.frameIndicator,
|
|
66
|
+
zoom: labels?.zoom ?? defaultViewer360Labels.zoom,
|
|
67
|
+
hotspotModeActive: labels?.hotspotModeActive ?? defaultViewer360Labels.hotspotModeActive,
|
|
68
|
+
addHotspot: labels?.addHotspot ?? defaultViewer360Labels.addHotspot,
|
|
69
|
+
zoomIn: labels?.zoomIn ?? defaultViewer360Labels.zoomIn,
|
|
70
|
+
zoomOut: labels?.zoomOut ?? defaultViewer360Labels.zoomOut,
|
|
71
|
+
resetView: labels?.resetView ?? defaultViewer360Labels.resetView,
|
|
72
|
+
deleteMarker: labels?.deleteMarker ?? defaultViewer360Labels.deleteMarker
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function mergeViewer360ClassNames(classNames) {
|
|
76
|
+
return {
|
|
77
|
+
root: cn(viewer360ClassNames.root, classNames?.root),
|
|
78
|
+
viewport: cn(viewer360ClassNames.viewport, classNames?.viewport),
|
|
79
|
+
canvas: cn(viewer360ClassNames.canvas, classNames?.canvas),
|
|
80
|
+
overlay: cn(viewer360ClassNames.overlay, classNames?.overlay),
|
|
81
|
+
loading: cn(viewer360ClassNames.loading, classNames?.loading),
|
|
82
|
+
loadingText: cn(viewer360ClassNames.loadingText, classNames?.loadingText),
|
|
83
|
+
frameIndicator: cn(viewer360ClassNames.frameIndicator, classNames?.frameIndicator),
|
|
84
|
+
hotspotModeBanner: cn(viewer360ClassNames.hotspotModeBanner, classNames?.hotspotModeBanner),
|
|
85
|
+
toolbar: cn(viewer360ClassNames.toolbar, classNames?.toolbar),
|
|
86
|
+
dragHint: cn(viewer360ClassNames.dragHint, classNames?.dragHint),
|
|
87
|
+
controls: cn(viewer360ClassNames.controls, classNames?.controls),
|
|
88
|
+
controlButton: cn(viewer360ClassNames.controlButton, classNames?.controlButton),
|
|
89
|
+
controlButtonActive: cn(viewer360ClassNames.controlButtonActive, classNames?.controlButtonActive),
|
|
90
|
+
controlButtonDisabled: cn(viewer360ClassNames.controlButtonDisabled, classNames?.controlButtonDisabled),
|
|
91
|
+
zoomDisplay: cn(viewer360ClassNames.zoomDisplay, classNames?.zoomDisplay),
|
|
92
|
+
divider: cn(viewer360ClassNames.divider, classNames?.divider)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function buildViewer360ThemeStyle(theme) {
|
|
96
|
+
return theme ? theme : {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/hooks/useViewer360.ts
|
|
100
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
101
|
+
|
|
102
|
+
// src/helpers/adjustViewerZoom.ts
|
|
103
|
+
function resolveViewer360Config(config) {
|
|
104
|
+
return {
|
|
105
|
+
minZoom: config?.minZoom ?? 1,
|
|
106
|
+
maxZoom: config?.maxZoom ?? 3,
|
|
107
|
+
zoomStep: config?.zoomStep ?? 0.15,
|
|
108
|
+
dragSensitivity: config?.dragSensitivity ?? 8,
|
|
109
|
+
autoRotate: config?.autoRotate ?? false,
|
|
110
|
+
autoRotateIntervalMs: config?.autoRotateIntervalMs ?? 100,
|
|
111
|
+
autoRotateDirection: config?.autoRotateDirection ?? "forward"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function clampZoom(zoom, config) {
|
|
115
|
+
return Math.min(config.maxZoom, Math.max(config.minZoom, zoom));
|
|
116
|
+
}
|
|
117
|
+
function applyWheelZoom(currentZoom, deltaY, currentPan, config) {
|
|
118
|
+
const delta = deltaY > 0 ? -config.zoomStep : config.zoomStep;
|
|
119
|
+
const zoom = clampZoom(currentZoom + delta, config);
|
|
120
|
+
return {
|
|
121
|
+
zoom,
|
|
122
|
+
pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function stepZoomIn(currentZoom, config) {
|
|
126
|
+
return clampZoom(currentZoom + config.zoomStep, config);
|
|
127
|
+
}
|
|
128
|
+
function stepZoomOut(currentZoom, currentPan, config) {
|
|
129
|
+
const zoom = clampZoom(currentZoom - config.zoomStep, config);
|
|
130
|
+
return {
|
|
131
|
+
zoom,
|
|
132
|
+
pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function isResetDisabled(zoom, pan, config) {
|
|
136
|
+
return zoom === config.minZoom && pan.panX === 0 && pan.panY === 0;
|
|
137
|
+
}
|
|
138
|
+
function getViewerCursorClass(isHotspotMode, zoom, isDragging, minZoom) {
|
|
139
|
+
if (isHotspotMode) return "cursor-crosshair";
|
|
140
|
+
if (isDragging) return "cursor-grabbing";
|
|
141
|
+
if (zoom > minZoom) return "cursor-grab";
|
|
142
|
+
return "cursor-ew-resize";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/helpers/computeDragFrameIndex.ts
|
|
146
|
+
function clampFrameIndex(index, frameCount) {
|
|
147
|
+
if (frameCount === 0) return 0;
|
|
148
|
+
return (index % frameCount + frameCount) % frameCount;
|
|
149
|
+
}
|
|
150
|
+
function computeDragFrameIndex(dragStart, clientX, frameCount, dragSensitivity) {
|
|
151
|
+
const deltaX = clientX - dragStart.pointerX;
|
|
152
|
+
const frameDelta = Math.round(-deltaX / dragSensitivity);
|
|
153
|
+
if (frameDelta === 0) return null;
|
|
154
|
+
return clampFrameIndex(dragStart.frameIndex + frameDelta, frameCount);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/helpers/computeViewerImageLayout.ts
|
|
158
|
+
function computeViewerImageLayout({
|
|
159
|
+
containerWidth,
|
|
160
|
+
containerHeight,
|
|
161
|
+
imageWidth,
|
|
162
|
+
imageHeight,
|
|
163
|
+
pan = { panX: 0, panY: 0 }
|
|
164
|
+
}) {
|
|
165
|
+
const imgAspect = imageWidth / imageHeight;
|
|
166
|
+
const containerAspect = containerWidth / containerHeight;
|
|
167
|
+
let drawWidth;
|
|
168
|
+
let drawHeight;
|
|
169
|
+
if (imgAspect > containerAspect) {
|
|
170
|
+
drawWidth = containerWidth;
|
|
171
|
+
drawHeight = containerWidth / imgAspect;
|
|
172
|
+
} else {
|
|
173
|
+
drawHeight = containerHeight;
|
|
174
|
+
drawWidth = containerHeight * imgAspect;
|
|
175
|
+
}
|
|
176
|
+
const offsetX = (containerWidth - drawWidth) / 2 + pan.panX;
|
|
177
|
+
const offsetY = (containerHeight - drawHeight) / 2 + pan.panY;
|
|
178
|
+
return {
|
|
179
|
+
width: containerWidth,
|
|
180
|
+
height: containerHeight,
|
|
181
|
+
centerX: containerWidth / 2,
|
|
182
|
+
centerY: containerHeight / 2,
|
|
183
|
+
drawWidth,
|
|
184
|
+
drawHeight,
|
|
185
|
+
offsetX,
|
|
186
|
+
offsetY
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function computeHotspotScreenPosition(hotspotX, hotspotY, layout, zoom) {
|
|
190
|
+
const baseOffsetX = (layout.width - layout.drawWidth) / 2;
|
|
191
|
+
const baseOffsetY = (layout.height - layout.drawHeight) / 2;
|
|
192
|
+
const containerX = hotspotX / 100 * layout.width;
|
|
193
|
+
const containerY = hotspotY / 100 * layout.height;
|
|
194
|
+
const imageLocalX = (containerX - baseOffsetX) / layout.drawWidth;
|
|
195
|
+
const imageLocalY = (containerY - baseOffsetY) / layout.drawHeight;
|
|
196
|
+
const imagePointX = layout.offsetX + imageLocalX * layout.drawWidth;
|
|
197
|
+
const imagePointY = layout.offsetY + imageLocalY * layout.drawHeight;
|
|
198
|
+
const screenX = layout.centerX + zoom * (imagePointX - layout.centerX);
|
|
199
|
+
const screenY = layout.centerY + zoom * (imagePointY - layout.centerY);
|
|
200
|
+
return {
|
|
201
|
+
leftPercent: screenX / layout.width * 100,
|
|
202
|
+
topPercent: screenY / layout.height * 100
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function computeHotspotPositionFromClick(clientX, clientY, containerRect, layout, zoom) {
|
|
206
|
+
const clickX = clientX - containerRect.left;
|
|
207
|
+
const clickY = clientY - containerRect.top;
|
|
208
|
+
const unzoomedX = layout.centerX + (clickX - layout.centerX) / zoom;
|
|
209
|
+
const unzoomedY = layout.centerY + (clickY - layout.centerY) / zoom;
|
|
210
|
+
const baseOffsetX = (layout.width - layout.drawWidth) / 2;
|
|
211
|
+
const baseOffsetY = (layout.height - layout.drawHeight) / 2;
|
|
212
|
+
const imageLocalX = Math.min(1, Math.max(0, (unzoomedX - layout.offsetX) / layout.drawWidth));
|
|
213
|
+
const imageLocalY = Math.min(1, Math.max(0, (unzoomedY - layout.offsetY) / layout.drawHeight));
|
|
214
|
+
const storedX = baseOffsetX + imageLocalX * layout.drawWidth;
|
|
215
|
+
const storedY = baseOffsetY + imageLocalY * layout.drawHeight;
|
|
216
|
+
return {
|
|
217
|
+
positionX: Math.min(100, Math.max(0, storedX / layout.width * 100)),
|
|
218
|
+
positionY: Math.min(100, Math.max(0, storedY / layout.height * 100))
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/helpers/computeViewerPanOffset.ts
|
|
223
|
+
function computeViewerPanOffset(panStart, clientX, clientY) {
|
|
224
|
+
const deltaX = clientX - panStart.pointerX;
|
|
225
|
+
const deltaY = clientY - panStart.pointerY;
|
|
226
|
+
return {
|
|
227
|
+
panX: panStart.panX + deltaX,
|
|
228
|
+
panY: panStart.panY + deltaY
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/helpers/viewerHelpers.ts
|
|
233
|
+
function drawFrameOnCanvas({ canvas, container, image, zoom, pan }) {
|
|
234
|
+
if (!image.complete || !image.naturalWidth) return;
|
|
235
|
+
const rect = container.getBoundingClientRect();
|
|
236
|
+
const dpr = window.devicePixelRatio || 1;
|
|
237
|
+
canvas.width = rect.width * dpr;
|
|
238
|
+
canvas.height = rect.height * dpr;
|
|
239
|
+
canvas.style.width = `${rect.width}px`;
|
|
240
|
+
canvas.style.height = `${rect.height}px`;
|
|
241
|
+
const ctx = canvas.getContext("2d");
|
|
242
|
+
if (!ctx) return;
|
|
243
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
244
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
245
|
+
const layout = computeViewerImageLayout({
|
|
246
|
+
containerWidth: rect.width,
|
|
247
|
+
containerHeight: rect.height,
|
|
248
|
+
imageWidth: image.naturalWidth,
|
|
249
|
+
imageHeight: image.naturalHeight,
|
|
250
|
+
pan
|
|
251
|
+
});
|
|
252
|
+
ctx.save();
|
|
253
|
+
ctx.translate(layout.centerX, layout.centerY);
|
|
254
|
+
ctx.scale(zoom, zoom);
|
|
255
|
+
ctx.translate(-layout.centerX, -layout.centerY);
|
|
256
|
+
ctx.drawImage(image, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight);
|
|
257
|
+
ctx.restore();
|
|
258
|
+
}
|
|
259
|
+
function getFramesSignature(frames) {
|
|
260
|
+
return frames.map((frame) => frame.id).join("-");
|
|
261
|
+
}
|
|
262
|
+
function preloadFrameImage(frame) {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const img = new Image();
|
|
265
|
+
img.onload = () => resolve(img);
|
|
266
|
+
img.onerror = () => reject(new Error(`Failed to load frame: ${frame.src}`));
|
|
267
|
+
img.src = frame.src;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async function preloadViewerFrames(frames) {
|
|
271
|
+
const results = await Promise.allSettled(frames.map(preloadFrameImage));
|
|
272
|
+
return results.map((result) => result.status === "fulfilled" ? result.value : new Image());
|
|
273
|
+
}
|
|
274
|
+
function hasLoadedViewerFrame(images) {
|
|
275
|
+
return images.some((image) => image.complete && image.naturalWidth > 0);
|
|
276
|
+
}
|
|
277
|
+
function filterHotspotsByFrame(hotspots, frameIndex) {
|
|
278
|
+
return hotspots.filter((hotspot) => hotspot.frameIndex === frameIndex);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/hooks/useViewer360.ts
|
|
282
|
+
function useViewer360({
|
|
283
|
+
frames,
|
|
284
|
+
hotspots = [],
|
|
285
|
+
currentFrameIndex,
|
|
286
|
+
onFrameChange,
|
|
287
|
+
config,
|
|
288
|
+
hotspotMode: hotspotModeProp = false,
|
|
289
|
+
onHotspotAdd
|
|
290
|
+
}) {
|
|
291
|
+
const resolvedConfig = useMemo(() => resolveViewer360Config(config), [config]);
|
|
292
|
+
const { minZoom, maxZoom } = resolvedConfig;
|
|
293
|
+
const canvasRef = useRef(null);
|
|
294
|
+
const containerRef = useRef(null);
|
|
295
|
+
const imagesRef = useRef([]);
|
|
296
|
+
const dragStartRef = useRef(null);
|
|
297
|
+
const panStartRef = useRef(null);
|
|
298
|
+
const framesSignature = getFramesSignature(frames);
|
|
299
|
+
const [loadedSignature, setLoadedSignature] = useState(null);
|
|
300
|
+
const [zoom, setZoom] = useState(minZoom);
|
|
301
|
+
const [pan, setPan] = useState({ panX: 0, panY: 0 });
|
|
302
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
303
|
+
const isHotspotMode = hotspotModeProp;
|
|
304
|
+
const imagesLoaded = loadedSignature === framesSignature;
|
|
305
|
+
const currentFrameHotspots = filterHotspotsByFrame(hotspots, currentFrameIndex);
|
|
306
|
+
const currentFrame = frames[currentFrameIndex];
|
|
307
|
+
const viewerCursorClass = getViewerCursorClass(isHotspotMode, zoom, isDragging, minZoom);
|
|
308
|
+
const resetDisabled = isResetDisabled(zoom, pan, resolvedConfig);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
let cancelled = false;
|
|
311
|
+
imagesRef.current = [];
|
|
312
|
+
async function loadFrames() {
|
|
313
|
+
const loadedImages = await preloadViewerFrames(frames);
|
|
314
|
+
if (!cancelled && hasLoadedViewerFrame(loadedImages)) {
|
|
315
|
+
imagesRef.current = loadedImages;
|
|
316
|
+
setLoadedSignature(framesSignature);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
void loadFrames();
|
|
320
|
+
return () => {
|
|
321
|
+
cancelled = true;
|
|
322
|
+
};
|
|
323
|
+
}, [frames, framesSignature]);
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!imagesLoaded) return;
|
|
326
|
+
function renderFrame() {
|
|
327
|
+
const canvas = canvasRef.current;
|
|
328
|
+
const container = containerRef.current;
|
|
329
|
+
const img = imagesRef.current[currentFrameIndex];
|
|
330
|
+
if (!canvas || !container) return;
|
|
331
|
+
drawFrameOnCanvas({ canvas, container, image: img, zoom, pan });
|
|
332
|
+
}
|
|
333
|
+
renderFrame();
|
|
334
|
+
window.addEventListener("resize", renderFrame);
|
|
335
|
+
return () => window.removeEventListener("resize", renderFrame);
|
|
336
|
+
}, [imagesLoaded, currentFrameIndex, zoom, pan]);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!resolvedConfig.autoRotate || frames.length <= 1 || isDragging || isHotspotMode || zoom > minZoom) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const direction = resolvedConfig.autoRotateDirection === "backward" ? -1 : 1;
|
|
342
|
+
const interval = window.setInterval(() => {
|
|
343
|
+
onFrameChange((currentFrameIndex + direction + frames.length) % frames.length);
|
|
344
|
+
}, resolvedConfig.autoRotateIntervalMs);
|
|
345
|
+
return () => window.clearInterval(interval);
|
|
346
|
+
}, [
|
|
347
|
+
resolvedConfig.autoRotate,
|
|
348
|
+
resolvedConfig.autoRotateDirection,
|
|
349
|
+
resolvedConfig.autoRotateIntervalMs,
|
|
350
|
+
frames.length,
|
|
351
|
+
currentFrameIndex,
|
|
352
|
+
isDragging,
|
|
353
|
+
isHotspotMode,
|
|
354
|
+
zoom,
|
|
355
|
+
minZoom,
|
|
356
|
+
onFrameChange
|
|
357
|
+
]);
|
|
358
|
+
function getCurrentImageLayout() {
|
|
359
|
+
const container = containerRef.current;
|
|
360
|
+
const image = imagesRef.current[currentFrameIndex];
|
|
361
|
+
if (!container || !image || !image.complete || !image.naturalWidth) return null;
|
|
362
|
+
const rect = container.getBoundingClientRect();
|
|
363
|
+
return computeViewerImageLayout({
|
|
364
|
+
containerWidth: rect.width,
|
|
365
|
+
containerHeight: rect.height,
|
|
366
|
+
imageWidth: image.naturalWidth,
|
|
367
|
+
imageHeight: image.naturalHeight,
|
|
368
|
+
pan
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function getHotspotScreenPosition(hotspot) {
|
|
372
|
+
const layout = getCurrentImageLayout();
|
|
373
|
+
if (!layout) {
|
|
374
|
+
return { leftPercent: hotspot.positionX, topPercent: hotspot.positionY };
|
|
375
|
+
}
|
|
376
|
+
return computeHotspotScreenPosition(hotspot.positionX, hotspot.positionY, layout, zoom);
|
|
377
|
+
}
|
|
378
|
+
function handlePointerDown(event) {
|
|
379
|
+
if (isHotspotMode) return;
|
|
380
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
381
|
+
if (zoom > minZoom) {
|
|
382
|
+
panStartRef.current = { pointerX: event.clientX, pointerY: event.clientY, panX: pan.panX, panY: pan.panY };
|
|
383
|
+
} else {
|
|
384
|
+
dragStartRef.current = { pointerX: event.clientX, frameIndex: currentFrameIndex };
|
|
385
|
+
}
|
|
386
|
+
setIsDragging(true);
|
|
387
|
+
}
|
|
388
|
+
function handlePointerMove(event) {
|
|
389
|
+
if (!isDragging) return;
|
|
390
|
+
if (panStartRef.current && zoom > minZoom) {
|
|
391
|
+
setPan(computeViewerPanOffset(panStartRef.current, event.clientX, event.clientY));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (dragStartRef.current && zoom <= minZoom) {
|
|
395
|
+
const nextFrameIndex = computeDragFrameIndex(
|
|
396
|
+
dragStartRef.current,
|
|
397
|
+
event.clientX,
|
|
398
|
+
frames.length,
|
|
399
|
+
resolvedConfig.dragSensitivity
|
|
400
|
+
);
|
|
401
|
+
if (nextFrameIndex !== null) {
|
|
402
|
+
onFrameChange(nextFrameIndex);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function handlePointerUp(event) {
|
|
407
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
408
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
409
|
+
}
|
|
410
|
+
dragStartRef.current = null;
|
|
411
|
+
panStartRef.current = null;
|
|
412
|
+
setIsDragging(false);
|
|
413
|
+
}
|
|
414
|
+
function handleWheel(event) {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
const { zoom: nextZoom, pan: nextPan } = applyWheelZoom(zoom, event.deltaY, pan, resolvedConfig);
|
|
417
|
+
setZoom(nextZoom);
|
|
418
|
+
setPan(nextPan);
|
|
419
|
+
}
|
|
420
|
+
function handleCanvasClick(event) {
|
|
421
|
+
if (!isHotspotMode || isDragging || !onHotspotAdd) return;
|
|
422
|
+
const frame = frames[currentFrameIndex];
|
|
423
|
+
if (!frame) return;
|
|
424
|
+
const layout = getCurrentImageLayout();
|
|
425
|
+
if (!layout) return;
|
|
426
|
+
const { positionX, positionY } = computeHotspotPositionFromClick(
|
|
427
|
+
event.clientX,
|
|
428
|
+
event.clientY,
|
|
429
|
+
event.currentTarget.getBoundingClientRect(),
|
|
430
|
+
layout,
|
|
431
|
+
zoom
|
|
432
|
+
);
|
|
433
|
+
onHotspotAdd({
|
|
434
|
+
frameIndex: currentFrameIndex,
|
|
435
|
+
frameId: frame.id,
|
|
436
|
+
positionX,
|
|
437
|
+
positionY
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function handleResetView() {
|
|
441
|
+
setZoom(minZoom);
|
|
442
|
+
setPan({ panX: 0, panY: 0 });
|
|
443
|
+
}
|
|
444
|
+
function handleZoomIn() {
|
|
445
|
+
setZoom(stepZoomIn(zoom, resolvedConfig));
|
|
446
|
+
}
|
|
447
|
+
function handleZoomOut() {
|
|
448
|
+
const { zoom: nextZoom, pan: nextPan } = stepZoomOut(zoom, pan, resolvedConfig);
|
|
449
|
+
setZoom(nextZoom);
|
|
450
|
+
setPan(nextPan);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
canvasRef,
|
|
454
|
+
containerRef,
|
|
455
|
+
currentFrame,
|
|
456
|
+
currentFrameHotspots,
|
|
457
|
+
imagesLoaded,
|
|
458
|
+
isHotspotMode,
|
|
459
|
+
isResetDisabled: resetDisabled,
|
|
460
|
+
maxZoom,
|
|
461
|
+
minZoom,
|
|
462
|
+
viewerCursorClass,
|
|
463
|
+
zoom,
|
|
464
|
+
getCurrentImageLayout,
|
|
465
|
+
getHotspotScreenPosition,
|
|
466
|
+
handleCanvasClick,
|
|
467
|
+
handlePointerDown,
|
|
468
|
+
handlePointerMove,
|
|
469
|
+
handlePointerUp,
|
|
470
|
+
handleResetView,
|
|
471
|
+
handleWheel,
|
|
472
|
+
handleZoomIn,
|
|
473
|
+
handleZoomOut
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/components/ui/Card/index.tsx
|
|
478
|
+
import { jsx } from "react/jsx-runtime";
|
|
479
|
+
function Card({ className, size = "default", ...props }) {
|
|
480
|
+
return /* @__PURE__ */ jsx(
|
|
481
|
+
"div",
|
|
482
|
+
{
|
|
483
|
+
"data-slot": "card",
|
|
484
|
+
"data-size": size,
|
|
485
|
+
className: cn(
|
|
486
|
+
"ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
|
|
487
|
+
className
|
|
488
|
+
),
|
|
489
|
+
...props
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function CardFooter({ className, ...props }) {
|
|
494
|
+
return /* @__PURE__ */ jsx(
|
|
495
|
+
"div",
|
|
496
|
+
{
|
|
497
|
+
"data-slot": "card-footer",
|
|
498
|
+
className: cn(
|
|
499
|
+
"rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center",
|
|
500
|
+
className
|
|
501
|
+
),
|
|
502
|
+
...props
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/components/ui/Badge/index.tsx
|
|
508
|
+
import { cva } from "class-variance-authority";
|
|
509
|
+
import { Slot } from "radix-ui";
|
|
510
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
511
|
+
var badgeVariants = cva(
|
|
512
|
+
"h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge",
|
|
513
|
+
{
|
|
514
|
+
variants: {
|
|
515
|
+
variant: {
|
|
516
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
517
|
+
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
518
|
+
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
519
|
+
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
520
|
+
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
521
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
522
|
+
info: "bg-blue-500 text-white hover:bg-blue-600",
|
|
523
|
+
warning: "bg-amber-500 text-white hover:bg-amber-600",
|
|
524
|
+
success: "bg-green-600 text-white hover:bg-green-700"
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
defaultVariants: {
|
|
528
|
+
variant: "default"
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
function Badge({
|
|
533
|
+
className,
|
|
534
|
+
variant = "default",
|
|
535
|
+
asChild = false,
|
|
536
|
+
...props
|
|
537
|
+
}) {
|
|
538
|
+
const Comp = asChild ? Slot.Root : "span";
|
|
539
|
+
return /* @__PURE__ */ jsx2(Comp, { "data-slot": "badge", "data-variant": variant, className: cn(badgeVariants({ variant }), className), ...props });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/feature/Viewer360AddModeBanner.tsx
|
|
543
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
544
|
+
function Viewer360AddModeBanner({ className, label }) {
|
|
545
|
+
return /* @__PURE__ */ jsx3(Badge, { variant: "outline", className: cn("pointer-events-none", className), children: label });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/feature/Viewer360FrameIndicator.tsx
|
|
549
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
550
|
+
function Viewer360FrameIndicator({ className, label }) {
|
|
551
|
+
return /* @__PURE__ */ jsx4(Badge, { variant: "outline", className: cn("pointer-events-none shadow-sm", className), children: label });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/helpers/markerHelpers.ts
|
|
555
|
+
function hotspotToViewer360Marker(hotspot) {
|
|
556
|
+
const data = hotspot.data;
|
|
557
|
+
if (data && typeof data === "object" && "title" in data && typeof data.title === "string") {
|
|
558
|
+
const marker = data;
|
|
559
|
+
return {
|
|
560
|
+
id: marker.id ?? hotspot.id,
|
|
561
|
+
title: marker.title,
|
|
562
|
+
description: marker.description
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
id: hotspot.id,
|
|
567
|
+
title: hotspot.id
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function toViewer360Hotspots(markers, mapData) {
|
|
571
|
+
return markers.map((marker) => ({
|
|
572
|
+
id: marker.id,
|
|
573
|
+
frameIndex: marker.frameIndex,
|
|
574
|
+
positionX: marker.positionX,
|
|
575
|
+
positionY: marker.positionY,
|
|
576
|
+
data: mapData ? mapData(marker) : marker
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/components/ui/Button/index.tsx
|
|
581
|
+
import { cva as cva2 } from "class-variance-authority";
|
|
582
|
+
import { Slot as Slot2 } from "radix-ui";
|
|
583
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
584
|
+
var buttonVariants = cva2(
|
|
585
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
586
|
+
{
|
|
587
|
+
variants: {
|
|
588
|
+
variant: {
|
|
589
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
590
|
+
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
|
591
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
592
|
+
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
593
|
+
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
594
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
595
|
+
},
|
|
596
|
+
size: {
|
|
597
|
+
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
|
|
598
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
599
|
+
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
600
|
+
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
|
|
601
|
+
icon: "size-9",
|
|
602
|
+
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md",
|
|
603
|
+
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
|
604
|
+
"icon-lg": "size-10"
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
defaultVariants: {
|
|
608
|
+
variant: "default",
|
|
609
|
+
size: "default"
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
function Button({
|
|
614
|
+
className,
|
|
615
|
+
variant = "default",
|
|
616
|
+
size = "default",
|
|
617
|
+
asChild = false,
|
|
618
|
+
...props
|
|
619
|
+
}) {
|
|
620
|
+
const Comp = asChild ? Slot2.Root : "button";
|
|
621
|
+
return /* @__PURE__ */ jsx5(
|
|
622
|
+
Comp,
|
|
623
|
+
{
|
|
624
|
+
"data-slot": "button",
|
|
625
|
+
"data-variant": variant,
|
|
626
|
+
"data-size": size,
|
|
627
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
628
|
+
...props
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/components/ui/Item/index.tsx
|
|
634
|
+
import { cva as cva3 } from "class-variance-authority";
|
|
635
|
+
import { Slot as Slot3 } from "radix-ui";
|
|
636
|
+
|
|
637
|
+
// src/components/ui/Separator/index.tsx
|
|
638
|
+
import { Separator as SeparatorPrimitive } from "radix-ui";
|
|
639
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
640
|
+
function Separator({
|
|
641
|
+
className,
|
|
642
|
+
orientation = "horizontal",
|
|
643
|
+
decorative = true,
|
|
644
|
+
...props
|
|
645
|
+
}) {
|
|
646
|
+
return /* @__PURE__ */ jsx6(
|
|
647
|
+
SeparatorPrimitive.Root,
|
|
648
|
+
{
|
|
649
|
+
"data-slot": "separator",
|
|
650
|
+
decorative,
|
|
651
|
+
orientation,
|
|
652
|
+
className: cn(
|
|
653
|
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
|
|
654
|
+
className
|
|
655
|
+
),
|
|
656
|
+
...props
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/components/ui/Item/index.tsx
|
|
662
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
663
|
+
var itemVariants = cva3(
|
|
664
|
+
"[a]:hover:bg-muted rounded-md border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors",
|
|
665
|
+
{
|
|
666
|
+
variants: {
|
|
667
|
+
variant: {
|
|
668
|
+
default: "border-transparent",
|
|
669
|
+
outline: "border-border",
|
|
670
|
+
muted: "bg-muted/50 border-transparent"
|
|
671
|
+
},
|
|
672
|
+
size: {
|
|
673
|
+
default: "gap-3.5 px-4 py-3.5",
|
|
674
|
+
sm: "gap-2.5 px-3 py-2.5",
|
|
675
|
+
xs: "gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0"
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
defaultVariants: {
|
|
679
|
+
variant: "default",
|
|
680
|
+
size: "default"
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
function Item({
|
|
685
|
+
className,
|
|
686
|
+
variant = "default",
|
|
687
|
+
size = "default",
|
|
688
|
+
asChild = false,
|
|
689
|
+
...props
|
|
690
|
+
}) {
|
|
691
|
+
const Comp = asChild ? Slot3.Root : "div";
|
|
692
|
+
return /* @__PURE__ */ jsx7(
|
|
693
|
+
Comp,
|
|
694
|
+
{
|
|
695
|
+
"data-slot": "item",
|
|
696
|
+
"data-variant": variant,
|
|
697
|
+
"data-size": size,
|
|
698
|
+
className: cn(itemVariants({ variant, size, className })),
|
|
699
|
+
...props
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
var itemMediaVariants = cva3(
|
|
704
|
+
"gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
|
|
705
|
+
{
|
|
706
|
+
variants: {
|
|
707
|
+
variant: {
|
|
708
|
+
default: "bg-transparent",
|
|
709
|
+
icon: "[&_svg]:size-4",
|
|
710
|
+
image: "size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover"
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
defaultVariants: {
|
|
714
|
+
variant: "default"
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
function ItemContent({ className, ...props }) {
|
|
719
|
+
return /* @__PURE__ */ jsx7(
|
|
720
|
+
"div",
|
|
721
|
+
{
|
|
722
|
+
"data-slot": "item-content",
|
|
723
|
+
className: cn("gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none", className),
|
|
724
|
+
...props
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
function ItemTitle({ className, ...props }) {
|
|
729
|
+
return /* @__PURE__ */ jsx7(
|
|
730
|
+
"div",
|
|
731
|
+
{
|
|
732
|
+
"data-slot": "item-title",
|
|
733
|
+
className: cn("gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center", className),
|
|
734
|
+
...props
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
function ItemDescription({ className, ...props }) {
|
|
739
|
+
return /* @__PURE__ */ jsx7(
|
|
740
|
+
"p",
|
|
741
|
+
{
|
|
742
|
+
"data-slot": "item-description",
|
|
743
|
+
className: cn(
|
|
744
|
+
"text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4",
|
|
745
|
+
className
|
|
746
|
+
),
|
|
747
|
+
...props
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
function ItemActions({ className, ...props }) {
|
|
752
|
+
return /* @__PURE__ */ jsx7("div", { "data-slot": "item-actions", className: cn("gap-2 flex items-center", className), ...props });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/feature/Viewer360MarkerPin.tsx
|
|
756
|
+
import { useState as useState2 } from "react";
|
|
757
|
+
import { Trash2 } from "lucide-react";
|
|
758
|
+
|
|
759
|
+
// src/constants/viewer360MarkerLabels.ts
|
|
760
|
+
var defaultViewer360MarkerPinLabels = {
|
|
761
|
+
delete: "Remove marker"
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// src/components/ui/Popover/index.tsx
|
|
765
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
766
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
767
|
+
function Popover({ ...props }) {
|
|
768
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
|
|
769
|
+
}
|
|
770
|
+
function PopoverTrigger({ ...props }) {
|
|
771
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
|
|
772
|
+
}
|
|
773
|
+
function PopoverContent({
|
|
774
|
+
className,
|
|
775
|
+
align = "center",
|
|
776
|
+
sideOffset = 4,
|
|
777
|
+
...props
|
|
778
|
+
}) {
|
|
779
|
+
return /* @__PURE__ */ jsx8(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx8(
|
|
780
|
+
PopoverPrimitive.Content,
|
|
781
|
+
{
|
|
782
|
+
"data-slot": "popover-content",
|
|
783
|
+
align,
|
|
784
|
+
sideOffset,
|
|
785
|
+
className: cn(
|
|
786
|
+
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--radix-popover-content-transform-origin) outline-hidden",
|
|
787
|
+
className
|
|
788
|
+
),
|
|
789
|
+
...props
|
|
790
|
+
}
|
|
791
|
+
) });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/feature/Viewer360MarkerPin.tsx
|
|
795
|
+
import { jsx as jsx9, jsxs } from "react/jsx-runtime";
|
|
796
|
+
function Viewer360MarkerPin({
|
|
797
|
+
marker,
|
|
798
|
+
hotspot,
|
|
799
|
+
leftPercent,
|
|
800
|
+
topPercent,
|
|
801
|
+
onDelete,
|
|
802
|
+
isDeletePending = false,
|
|
803
|
+
renderTag,
|
|
804
|
+
classNames,
|
|
805
|
+
labels
|
|
806
|
+
}) {
|
|
807
|
+
const [isOpen, setIsOpen] = useState2(false);
|
|
808
|
+
const deleteLabel = labels?.delete ?? defaultViewer360MarkerPinLabels.delete;
|
|
809
|
+
return /* @__PURE__ */ jsx9(Popover, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs(
|
|
810
|
+
Item,
|
|
811
|
+
{
|
|
812
|
+
size: "xs",
|
|
813
|
+
variant: "default",
|
|
814
|
+
className: cn(viewer360MarkerPinClassNames.root, classNames?.root, "w-auto border-transparent p-0"),
|
|
815
|
+
style: { left: `${leftPercent}%`, top: `${topPercent}%` },
|
|
816
|
+
onMouseEnter: () => setIsOpen(true),
|
|
817
|
+
onMouseLeave: () => setIsOpen(false),
|
|
818
|
+
children: [
|
|
819
|
+
/* @__PURE__ */ jsx9(
|
|
820
|
+
Badge,
|
|
821
|
+
{
|
|
822
|
+
variant: "destructive",
|
|
823
|
+
className: cn(viewer360MarkerPinClassNames.ping, classNames?.ping, "absolute size-6 border-0 bg-destructive opacity-60"),
|
|
824
|
+
"aria-hidden": "true"
|
|
825
|
+
}
|
|
826
|
+
),
|
|
827
|
+
/* @__PURE__ */ jsx9(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx9(
|
|
828
|
+
Button,
|
|
829
|
+
{
|
|
830
|
+
type: "button",
|
|
831
|
+
variant: "destructive",
|
|
832
|
+
size: "icon-xs",
|
|
833
|
+
className: cn(
|
|
834
|
+
viewer360MarkerPinClassNames.dot,
|
|
835
|
+
classNames?.dot,
|
|
836
|
+
"size-4 min-h-4 min-w-4 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:scale-125 hover:bg-destructive"
|
|
837
|
+
),
|
|
838
|
+
"aria-label": marker.title
|
|
839
|
+
}
|
|
840
|
+
) }),
|
|
841
|
+
/* @__PURE__ */ jsx9(
|
|
842
|
+
PopoverContent,
|
|
843
|
+
{
|
|
844
|
+
className: cn(viewer360MarkerPinClassNames.tooltip, classNames?.tooltip, "w-64 p-0"),
|
|
845
|
+
side: "top",
|
|
846
|
+
align: "center",
|
|
847
|
+
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
848
|
+
children: /* @__PURE__ */ jsxs(
|
|
849
|
+
Item,
|
|
850
|
+
{
|
|
851
|
+
size: "sm",
|
|
852
|
+
variant: "default",
|
|
853
|
+
className: cn(viewer360MarkerPinClassNames.tooltipHeader, classNames?.tooltipHeader, "w-full border-transparent"),
|
|
854
|
+
children: [
|
|
855
|
+
/* @__PURE__ */ jsxs(ItemContent, { className: cn(viewer360MarkerPinClassNames.tooltipBody, classNames?.tooltipBody), children: [
|
|
856
|
+
/* @__PURE__ */ jsx9(ItemTitle, { className: cn(viewer360MarkerPinClassNames.tooltipTitle, classNames?.tooltipTitle), children: marker.title }),
|
|
857
|
+
renderTag?.({ marker, hotspot }),
|
|
858
|
+
marker.description && /* @__PURE__ */ jsx9(ItemDescription, { className: cn(viewer360MarkerPinClassNames.tooltipDescription, classNames?.tooltipDescription), children: marker.description })
|
|
859
|
+
] }),
|
|
860
|
+
onDelete && /* @__PURE__ */ jsx9(ItemActions, { children: /* @__PURE__ */ jsx9(
|
|
861
|
+
Button,
|
|
862
|
+
{
|
|
863
|
+
variant: "ghost",
|
|
864
|
+
size: "icon-sm",
|
|
865
|
+
className: classNames?.deleteButton,
|
|
866
|
+
disabled: isDeletePending,
|
|
867
|
+
"aria-label": deleteLabel,
|
|
868
|
+
onClick: (event) => {
|
|
869
|
+
event.stopPropagation();
|
|
870
|
+
onDelete(marker.id);
|
|
871
|
+
},
|
|
872
|
+
children: /* @__PURE__ */ jsx9(Trash2, { className: "size-4" })
|
|
873
|
+
}
|
|
874
|
+
) })
|
|
875
|
+
]
|
|
876
|
+
}
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
) });
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/feature/Viewer360HotspotOverlay.tsx
|
|
886
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
887
|
+
function Viewer360HotspotOverlay({
|
|
888
|
+
hotspot,
|
|
889
|
+
leftPercent,
|
|
890
|
+
topPercent,
|
|
891
|
+
hotspotPin,
|
|
892
|
+
renderHotspot,
|
|
893
|
+
onHotspotClick
|
|
894
|
+
}) {
|
|
895
|
+
if (renderHotspot) {
|
|
896
|
+
return /* @__PURE__ */ jsx10(Item, { size: "xs", variant: "default", className: "pointer-events-auto w-auto border-transparent p-0", children: renderHotspot({ hotspot, leftPercent, topPercent }) });
|
|
897
|
+
}
|
|
898
|
+
if (hotspotPin) {
|
|
899
|
+
const marker = hotspotPin.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
900
|
+
return /* @__PURE__ */ jsx10(
|
|
901
|
+
Viewer360MarkerPin,
|
|
902
|
+
{
|
|
903
|
+
marker,
|
|
904
|
+
hotspot,
|
|
905
|
+
leftPercent,
|
|
906
|
+
topPercent,
|
|
907
|
+
onDelete: hotspotPin.onDelete,
|
|
908
|
+
isDeletePending: hotspotPin.deletingMarkerId === hotspot.id,
|
|
909
|
+
renderTag: hotspotPin.renderTag,
|
|
910
|
+
classNames: hotspotPin.classNames,
|
|
911
|
+
labels: hotspotPin.labels
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return /* @__PURE__ */ jsx10(
|
|
916
|
+
Button,
|
|
917
|
+
{
|
|
918
|
+
type: "button",
|
|
919
|
+
variant: "destructive",
|
|
920
|
+
size: "icon-xs",
|
|
921
|
+
className: cn(
|
|
922
|
+
"pointer-events-auto absolute z-30 size-4 min-h-4 min-w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:bg-destructive"
|
|
923
|
+
),
|
|
924
|
+
style: { left: `${leftPercent}%`, top: `${topPercent}%` },
|
|
925
|
+
"aria-label": `Hotspot ${hotspot.id}`,
|
|
926
|
+
onClick: (event) => onHotspotClick?.(hotspot, event)
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/components/ui/Label/index.tsx
|
|
932
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
933
|
+
function Label({ className, ...props }) {
|
|
934
|
+
return /* @__PURE__ */ jsx11(
|
|
935
|
+
"label",
|
|
936
|
+
{
|
|
937
|
+
"data-slot": "label",
|
|
938
|
+
className: cn(
|
|
939
|
+
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
|
940
|
+
className
|
|
941
|
+
),
|
|
942
|
+
...props
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/components/ui/Spinner/index.tsx
|
|
948
|
+
import { Loader2Icon } from "lucide-react";
|
|
949
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
950
|
+
function Spinner({ className, ...props }) {
|
|
951
|
+
return /* @__PURE__ */ jsx12(Loader2Icon, { role: "status", "aria-label": "Loading", className: cn("size-4 animate-spin", className), ...props });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/feature/Viewer360LoadingOverlay.tsx
|
|
955
|
+
import { jsx as jsx13, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
956
|
+
function Viewer360LoadingOverlay({ className, textClassName, label }) {
|
|
957
|
+
return /* @__PURE__ */ jsxs2(
|
|
958
|
+
Item,
|
|
959
|
+
{
|
|
960
|
+
size: "sm",
|
|
961
|
+
variant: "muted",
|
|
962
|
+
className: cn("pointer-events-none w-auto justify-center border-transparent bg-muted/80", className),
|
|
963
|
+
children: [
|
|
964
|
+
/* @__PURE__ */ jsx13(Spinner, { className: "size-5 text-muted-foreground" }),
|
|
965
|
+
/* @__PURE__ */ jsx13(Label, { className: cn("font-normal text-muted-foreground", textClassName), children: label })
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/feature/Viewer360Toolbar.tsx
|
|
972
|
+
import { Crosshair, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react";
|
|
973
|
+
import { Fragment, jsx as jsx14, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
974
|
+
function Viewer360Toolbar({
|
|
975
|
+
showDragHint,
|
|
976
|
+
showHotspotModeControl,
|
|
977
|
+
showZoomControls,
|
|
978
|
+
showResetControl,
|
|
979
|
+
labels,
|
|
980
|
+
isHotspotMode,
|
|
981
|
+
zoom,
|
|
982
|
+
minZoom,
|
|
983
|
+
maxZoom,
|
|
984
|
+
isResetDisabled: isResetDisabled2,
|
|
985
|
+
onHotspotModeChange,
|
|
986
|
+
onZoomIn,
|
|
987
|
+
onZoomOut,
|
|
988
|
+
onResetView
|
|
989
|
+
}) {
|
|
990
|
+
return /* @__PURE__ */ jsxs3(CardFooter, { className: cn(viewer360ClassNames.toolbar, "gap-2 border-t px-4 py-3 pt-3"), children: [
|
|
991
|
+
showDragHint && /* @__PURE__ */ jsx14(Label, { className: cn(viewer360ClassNames.dragHint, "font-normal text-muted-foreground"), children: labels.dragHint }),
|
|
992
|
+
/* @__PURE__ */ jsxs3("div", { className: viewer360ClassNames.controls, children: [
|
|
993
|
+
showHotspotModeControl && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
994
|
+
/* @__PURE__ */ jsxs3(Button, { variant: isHotspotMode ? "default" : "outline", size: "sm", onClick: () => onHotspotModeChange(!isHotspotMode), children: [
|
|
995
|
+
/* @__PURE__ */ jsx14(Crosshair, { className: "me-1.5 size-4" }),
|
|
996
|
+
labels.addHotspot
|
|
997
|
+
] }),
|
|
998
|
+
/* @__PURE__ */ jsx14(Separator, { orientation: "vertical", className: cn(viewer360ClassNames.divider, "h-6") })
|
|
999
|
+
] }),
|
|
1000
|
+
showZoomControls && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1001
|
+
/* @__PURE__ */ jsx14(Button, { variant: "outline", size: "icon-sm", disabled: zoom <= minZoom, "aria-label": labels.zoomOut, onClick: onZoomOut, children: /* @__PURE__ */ jsx14(Minus, { className: "size-4" }) }),
|
|
1002
|
+
/* @__PURE__ */ jsxs3(Badge, { variant: "outline", className: cn(viewer360ClassNames.zoomDisplay, "h-8 gap-1 px-2 py-1"), children: [
|
|
1003
|
+
/* @__PURE__ */ jsx14(ZoomIn, { className: "size-3 text-muted-foreground" }),
|
|
1004
|
+
labels.zoom(Math.round(zoom * 100))
|
|
1005
|
+
] }),
|
|
1006
|
+
/* @__PURE__ */ jsx14(Button, { variant: "outline", size: "icon-sm", disabled: zoom >= maxZoom, "aria-label": labels.zoomIn, onClick: onZoomIn, children: /* @__PURE__ */ jsx14(Plus, { className: "size-4" }) })
|
|
1007
|
+
] }),
|
|
1008
|
+
showResetControl && /* @__PURE__ */ jsx14(Button, { variant: "outline", size: "icon-sm", disabled: isResetDisabled2, "aria-label": labels.resetView, onClick: onResetView, children: /* @__PURE__ */ jsx14(RotateCcw, { className: "size-4" }) })
|
|
1009
|
+
] })
|
|
1010
|
+
] });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/feature/Viewer360.tsx
|
|
1014
|
+
import { jsx as jsx15, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1015
|
+
function Viewer360({
|
|
1016
|
+
frames,
|
|
1017
|
+
currentFrameIndex: controlledFrameIndex,
|
|
1018
|
+
defaultFrameIndex = 0,
|
|
1019
|
+
onFrameChange,
|
|
1020
|
+
config,
|
|
1021
|
+
className,
|
|
1022
|
+
classNames,
|
|
1023
|
+
style,
|
|
1024
|
+
theme,
|
|
1025
|
+
labels,
|
|
1026
|
+
aspectRatio = "16 / 10",
|
|
1027
|
+
showZoomControls = true,
|
|
1028
|
+
showResetControl = true,
|
|
1029
|
+
showFrameIndicator = true,
|
|
1030
|
+
showDragHint = true,
|
|
1031
|
+
showHotspotModeControl = false,
|
|
1032
|
+
hotspotPin,
|
|
1033
|
+
hotspots = [],
|
|
1034
|
+
renderHotspot,
|
|
1035
|
+
renderLoading,
|
|
1036
|
+
renderFrameIndicator,
|
|
1037
|
+
renderHotspotModeBanner,
|
|
1038
|
+
renderToolbar,
|
|
1039
|
+
onHotspotClick,
|
|
1040
|
+
hotspotMode: controlledHotspotMode,
|
|
1041
|
+
defaultHotspotMode = false,
|
|
1042
|
+
onHotspotModeChange,
|
|
1043
|
+
onHotspotAdd,
|
|
1044
|
+
children
|
|
1045
|
+
}) {
|
|
1046
|
+
const mergedLabels = useMemo2(() => mergeViewer360Labels(labels), [labels]);
|
|
1047
|
+
const mergedClassNames = useMemo2(() => mergeViewer360ClassNames(classNames), [classNames]);
|
|
1048
|
+
const themeStyle = useMemo2(() => buildViewer360ThemeStyle(theme), [theme]);
|
|
1049
|
+
const [internalFrameIndex, setInternalFrameIndex] = useState3(defaultFrameIndex);
|
|
1050
|
+
const [internalHotspotMode, setInternalHotspotMode] = useState3(defaultHotspotMode);
|
|
1051
|
+
const currentFrameIndex = controlledFrameIndex ?? internalFrameIndex;
|
|
1052
|
+
const hotspotMode = controlledHotspotMode ?? internalHotspotMode;
|
|
1053
|
+
function handleFrameChange(index) {
|
|
1054
|
+
if (controlledFrameIndex === void 0) {
|
|
1055
|
+
setInternalFrameIndex(index);
|
|
1056
|
+
}
|
|
1057
|
+
onFrameChange?.(index);
|
|
1058
|
+
}
|
|
1059
|
+
function handleHotspotModeChange(active) {
|
|
1060
|
+
if (controlledHotspotMode === void 0) {
|
|
1061
|
+
setInternalHotspotMode(active);
|
|
1062
|
+
}
|
|
1063
|
+
onHotspotModeChange?.(active);
|
|
1064
|
+
}
|
|
1065
|
+
const {
|
|
1066
|
+
canvasRef,
|
|
1067
|
+
containerRef,
|
|
1068
|
+
currentFrame,
|
|
1069
|
+
currentFrameHotspots,
|
|
1070
|
+
imagesLoaded,
|
|
1071
|
+
isHotspotMode,
|
|
1072
|
+
isResetDisabled: isResetDisabled2,
|
|
1073
|
+
maxZoom,
|
|
1074
|
+
minZoom,
|
|
1075
|
+
viewerCursorClass,
|
|
1076
|
+
zoom,
|
|
1077
|
+
getHotspotScreenPosition,
|
|
1078
|
+
handleCanvasClick,
|
|
1079
|
+
handlePointerDown,
|
|
1080
|
+
handlePointerMove,
|
|
1081
|
+
handlePointerUp,
|
|
1082
|
+
handleWheel,
|
|
1083
|
+
handleResetView,
|
|
1084
|
+
handleZoomIn,
|
|
1085
|
+
handleZoomOut
|
|
1086
|
+
} = useViewer360({
|
|
1087
|
+
frames,
|
|
1088
|
+
hotspots,
|
|
1089
|
+
currentFrameIndex,
|
|
1090
|
+
onFrameChange: handleFrameChange,
|
|
1091
|
+
config,
|
|
1092
|
+
hotspotMode,
|
|
1093
|
+
onHotspotAdd
|
|
1094
|
+
});
|
|
1095
|
+
useEffect2(() => {
|
|
1096
|
+
if (controlledHotspotMode === void 0) return;
|
|
1097
|
+
if (controlledHotspotMode !== internalHotspotMode) {
|
|
1098
|
+
setInternalHotspotMode(controlledHotspotMode);
|
|
1099
|
+
}
|
|
1100
|
+
}, [controlledHotspotMode, internalHotspotMode]);
|
|
1101
|
+
const frameLabel = currentFrame?.label ?? frames[currentFrameIndex]?.label;
|
|
1102
|
+
const overlayProps = {
|
|
1103
|
+
currentFrameIndex,
|
|
1104
|
+
frameCount: frames.length,
|
|
1105
|
+
frameLabel,
|
|
1106
|
+
isHotspotMode,
|
|
1107
|
+
labels: mergedLabels
|
|
1108
|
+
};
|
|
1109
|
+
const toolbarProps = {
|
|
1110
|
+
zoom,
|
|
1111
|
+
minZoom,
|
|
1112
|
+
maxZoom,
|
|
1113
|
+
isResetDisabled: isResetDisabled2,
|
|
1114
|
+
isHotspotMode,
|
|
1115
|
+
showHotspotModeControl,
|
|
1116
|
+
showZoomControls,
|
|
1117
|
+
showResetControl,
|
|
1118
|
+
showDragHint,
|
|
1119
|
+
labels: mergedLabels,
|
|
1120
|
+
onZoomIn: handleZoomIn,
|
|
1121
|
+
onZoomOut: handleZoomOut,
|
|
1122
|
+
onResetView: handleResetView,
|
|
1123
|
+
onHotspotModeChange: handleHotspotModeChange
|
|
1124
|
+
};
|
|
1125
|
+
const showDefaultToolbar = showZoomControls || showResetControl || showHotspotModeControl || showDragHint;
|
|
1126
|
+
return /* @__PURE__ */ jsxs4(Card, { className: cn(mergedClassNames.root, "gap-0 py-0 shadow-none ring-0", className), style: { ...themeStyle, ...style }, children: [
|
|
1127
|
+
/* @__PURE__ */ jsxs4(
|
|
1128
|
+
"div",
|
|
1129
|
+
{
|
|
1130
|
+
ref: containerRef,
|
|
1131
|
+
className: cn(mergedClassNames.viewport, viewerCursorClass),
|
|
1132
|
+
style: { aspectRatio },
|
|
1133
|
+
onPointerDown: handlePointerDown,
|
|
1134
|
+
onPointerMove: handlePointerMove,
|
|
1135
|
+
onPointerUp: handlePointerUp,
|
|
1136
|
+
onPointerLeave: handlePointerUp,
|
|
1137
|
+
onWheel: handleWheel,
|
|
1138
|
+
onClick: handleCanvasClick,
|
|
1139
|
+
children: [
|
|
1140
|
+
/* @__PURE__ */ jsx15("canvas", { ref: canvasRef, className: mergedClassNames.canvas }),
|
|
1141
|
+
/* @__PURE__ */ jsxs4("div", { className: mergedClassNames.overlay, children: [
|
|
1142
|
+
currentFrameHotspots.map((hotspot) => {
|
|
1143
|
+
const position = getHotspotScreenPosition(hotspot);
|
|
1144
|
+
return /* @__PURE__ */ jsx15(
|
|
1145
|
+
Viewer360HotspotOverlay,
|
|
1146
|
+
{
|
|
1147
|
+
hotspot,
|
|
1148
|
+
leftPercent: position.leftPercent,
|
|
1149
|
+
topPercent: position.topPercent,
|
|
1150
|
+
hotspotPin,
|
|
1151
|
+
renderHotspot,
|
|
1152
|
+
onHotspotClick
|
|
1153
|
+
},
|
|
1154
|
+
hotspot.id
|
|
1155
|
+
);
|
|
1156
|
+
}),
|
|
1157
|
+
children
|
|
1158
|
+
] }),
|
|
1159
|
+
!imagesLoaded && (renderLoading ? renderLoading() : /* @__PURE__ */ jsx15(
|
|
1160
|
+
Viewer360LoadingOverlay,
|
|
1161
|
+
{
|
|
1162
|
+
className: mergedClassNames.loading,
|
|
1163
|
+
textClassName: mergedClassNames.loadingText,
|
|
1164
|
+
label: mergedLabels.loading
|
|
1165
|
+
}
|
|
1166
|
+
)),
|
|
1167
|
+
showFrameIndicator && frames.length > 0 && (renderFrameIndicator ? renderFrameIndicator(overlayProps) : /* @__PURE__ */ jsx15(
|
|
1168
|
+
Viewer360FrameIndicator,
|
|
1169
|
+
{
|
|
1170
|
+
className: mergedClassNames.frameIndicator,
|
|
1171
|
+
label: mergedLabels.frameIndicator({
|
|
1172
|
+
current: currentFrameIndex + 1,
|
|
1173
|
+
total: frames.length,
|
|
1174
|
+
label: frameLabel
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
)),
|
|
1178
|
+
isHotspotMode && onHotspotAdd && (renderHotspotModeBanner ? renderHotspotModeBanner({ labels: mergedLabels }) : /* @__PURE__ */ jsx15(Viewer360AddModeBanner, { className: mergedClassNames.hotspotModeBanner, label: mergedLabels.hotspotModeActive }))
|
|
1179
|
+
]
|
|
1180
|
+
}
|
|
1181
|
+
),
|
|
1182
|
+
renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? /* @__PURE__ */ jsx15(Viewer360Toolbar, { ...toolbarProps }) : null
|
|
1183
|
+
] });
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/constants/viewer360Config.ts
|
|
1187
|
+
var viewer360Config = {
|
|
1188
|
+
minZoom: 1,
|
|
1189
|
+
maxZoom: 3,
|
|
1190
|
+
zoomStep: 0.15,
|
|
1191
|
+
dragSensitivity: 8,
|
|
1192
|
+
autoRotate: false,
|
|
1193
|
+
autoRotateIntervalMs: 100,
|
|
1194
|
+
autoRotateDirection: "forward"
|
|
1195
|
+
};
|
|
1196
|
+
var defaultViewer360Config = viewer360Config;
|
|
1197
|
+
export {
|
|
1198
|
+
Button,
|
|
1199
|
+
Viewer360,
|
|
1200
|
+
Viewer360AddModeBanner,
|
|
1201
|
+
Viewer360FrameIndicator,
|
|
1202
|
+
Viewer360LoadingOverlay,
|
|
1203
|
+
Viewer360MarkerPin,
|
|
1204
|
+
Viewer360Toolbar,
|
|
1205
|
+
applyWheelZoom,
|
|
1206
|
+
buttonVariants,
|
|
1207
|
+
clampFrameIndex,
|
|
1208
|
+
clampZoom,
|
|
1209
|
+
cn,
|
|
1210
|
+
computeDragFrameIndex,
|
|
1211
|
+
computeHotspotPositionFromClick,
|
|
1212
|
+
computeHotspotScreenPosition,
|
|
1213
|
+
computeViewerImageLayout,
|
|
1214
|
+
computeViewerPanOffset,
|
|
1215
|
+
defaultViewer360ClassNames,
|
|
1216
|
+
defaultViewer360Config,
|
|
1217
|
+
defaultViewer360Labels,
|
|
1218
|
+
defaultViewer360MarkerPinClassNames,
|
|
1219
|
+
defaultViewer360MarkerPinLabels,
|
|
1220
|
+
drawFrameOnCanvas,
|
|
1221
|
+
filterHotspotsByFrame,
|
|
1222
|
+
getFramesSignature,
|
|
1223
|
+
getViewerCursorClass,
|
|
1224
|
+
hasLoadedViewerFrame,
|
|
1225
|
+
hotspotToViewer360Marker,
|
|
1226
|
+
isResetDisabled,
|
|
1227
|
+
preloadFrameImage,
|
|
1228
|
+
preloadViewerFrames,
|
|
1229
|
+
resolveViewer360Config,
|
|
1230
|
+
stepZoomIn,
|
|
1231
|
+
stepZoomOut,
|
|
1232
|
+
toViewer360Hotspots,
|
|
1233
|
+
useViewer360,
|
|
1234
|
+
viewer360ClassNames,
|
|
1235
|
+
viewer360MarkerPinClassNames
|
|
1236
|
+
};
|
|
1237
|
+
//# sourceMappingURL=index.js.map
|