| "use client" |
|
|
| import { useEffect, useRef, useState } from "react" |
| import { PanoramaPosition, PluginConstructor, Point, Position, SphericalPosition, Viewer } from "@photo-sphere-viewer/core" |
| import { LensflarePlugin, ReactPhotoSphereViewer } from "react-photo-sphere-viewer" |
|
|
| import { MouseEventHandler, RenderedScene } from "@/types" |
|
|
| import { useImageDimension } from "@/lib/useImageDimension" |
| import { lightSourceNames } from "@/lib/lightSourceNames" |
|
|
| type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any]) |
|
|
| export function SphericalImage({ |
| rendered, |
| onEvent, |
| className, |
| debug, |
| }: { |
| rendered: RenderedScene |
| onEvent: MouseEventHandler |
| className?: string |
| debug?: boolean |
| }) { |
|
|
|
|
| const imageDimension = useImageDimension(rendered.assetUrl) |
| const maskDimension = useImageDimension(rendered.maskUrl) |
|
|
| const sceneConfig = JSON.stringify({ rendered, debug, imageDimension, maskDimension }) |
| const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig) |
| const rootContainerRef = useRef<HTMLDivElement>(null) |
| const viewerContainerRef = useRef<HTMLElement>() |
| const viewerRef = useRef<Viewer>() |
| const [mouseMoved, setMouseMoved] = useState<boolean>(false) |
|
|
| const defaultZoomLvl = 1 |
|
|
| const options = { |
| defaultZoomLvl, |
| fisheye: false, |
| overlay: rendered.maskUrl || undefined, |
| overlayOpacity: debug ? 0.5 : 0, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } |
|
|
|
|
| const cacheRef = useRef("") |
| useEffect(() => { |
| const listener = (e: DragEvent) => { |
| if (!rootContainerRef.current) { return } |
|
|
| |
| |
| const boundingRect = rootContainerRef.current.getBoundingClientRect() |
|
|
| |
| if (e.clientX < boundingRect.left) { return } |
| if (e.clientX > (boundingRect.left + boundingRect.width)) { return } |
| if (e.clientY < boundingRect.top) { return } |
| if (e.clientY > (boundingRect.top + boundingRect.height)) { return } |
|
|
| const containerX = e.clientX - boundingRect.left |
| const containerY = e.clientY - boundingRect.top |
| |
| const relativeX = containerX / boundingRect.width |
| const relativeY = containerY / boundingRect.height |
|
|
| const key = `${relativeX},${relativeY}` |
|
|
| |
| if (cacheRef.current === key) { |
| return |
| } |
| |
|
|
| cacheRef.current = key |
| onEvent("hover", relativeX, relativeY) |
| } |
|
|
| document.addEventListener('drag', listener) |
|
|
| return () => { |
| document.removeEventListener('drag', listener) |
| } |
| }, [onEvent]) |
|
|
| useEffect(() => { |
| const task = async () => { |
| |
| if (sceneConfig !== lastSceneConfig) { |
| |
| |
| if (!viewerRef.current) { |
| |
| setLastSceneConfig(sceneConfig) |
| return |
| } |
| const viewer = viewerRef.current |
|
|
| const newOptions = { |
| ...options, |
| } |
|
|
| const lensflares: { id: string; position: SphericalPosition; type: number }[] = [] |
| |
| if (maskDimension.width && imageDimension.width) { |
|
|
| |
| |
| rendered.segments |
| .filter(segment => lightSourceNames.includes(segment.label)) |
| .forEach(light => { |
| |
| const [x1, y1, x2, y2] = light.box |
| const [centerX, centerY] = [(x1 + x2) / 2, (y1 + y2) / 2] |
| |
| const [relativeX, relativeY] = [centerX / maskDimension.width, centerY/ maskDimension.height] |
| |
|
|
| const panoramaPosition: PanoramaPosition = { |
| textureX: relativeX * imageDimension.width, |
| textureY: relativeY * imageDimension.height |
| } |
| |
|
|
| const position = viewer.dataHelper.textureCoordsToSphericalCoords(panoramaPosition) |
| |
| if ( |
| !isNaN(position.pitch) && isFinite(position.pitch) && |
| !isNaN(position.yaw) && isFinite(position.yaw)) { |
| lensflares.push({ |
| id: `flare_${lensflares.length}`, |
| position, |
| type: 0, |
| }) |
| } |
| }) |
| } |
|
|
| |
| const lensFlarePlugin = viewer.getPlugin<LensflarePlugin>("lensflare") |
| lensFlarePlugin.setLensflares(lensflares) |
|
|
| |
| |
| |
| await viewer.setPanorama(rendered.assetUrl, { |
| ...newOptions, |
| showLoader: false, |
| }) |
|
|
| |
| viewer.setOptions(newOptions) |
| |
|
|
| |
| viewerRef.current.needsUpdate() |
|
|
| setLastSceneConfig(sceneConfig) |
| } |
| } |
| task() |
| }, [sceneConfig, rendered.assetUrl, viewerRef.current, maskDimension.width, imageDimension]) |
|
|
| const handleEvent = async (event: React.MouseEvent<HTMLDivElement, MouseEvent>, isClick: boolean) => { |
| const rootContainer = rootContainerRef.current |
| const viewer = viewerRef.current |
| const viewerContainer = viewerContainerRef.current |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (!viewer || !rootContainer || !viewerContainer || !imageDimension.width || !rendered.maskUrl) { |
| return |
| } |
|
|
| const containerRect = viewerContainer.getBoundingClientRect() |
| |
|
|
| const containerY = event.clientY - containerRect.top |
| |
| |
| const position: Position = viewer.getPosition() |
|
|
| const viewerPosition: Point = viewer.dataHelper.sphericalCoordsToViewerCoords(position) |
| |
|
|
| |
| |
| |
| if (isClick && containerY > (containerRect.height - 40)) { |
| |
| return |
| } |
|
|
| const panoramaPosition: PanoramaPosition = viewer.dataHelper.sphericalCoordsToTextureCoords(position) |
|
|
| if (typeof panoramaPosition.textureX !== "number" || typeof panoramaPosition.textureY !== "number") { |
| return |
| } |
| |
| const relativeX = panoramaPosition.textureX / imageDimension.width |
| const relativeY = panoramaPosition.textureY / imageDimension.height |
|
|
| onEvent(isClick ? "click" : "hover", relativeX, relativeY) |
| } |
|
|
| if (!rendered.assetUrl) { |
| return null |
| } |
|
|
| return ( |
| <div |
| ref={rootContainerRef} |
| onMouseMove={(event) => { |
| handleEvent(event, false) |
| setMouseMoved(true) |
| }} |
| onMouseUp={(event) => { |
| if (!mouseMoved) { |
| handleEvent(event, true) |
| } |
| setMouseMoved(false) |
| }} |
| onMouseDown={() => { |
| setMouseMoved(false) |
| }} |
| > |
| <ReactPhotoSphereViewer |
| src={rendered.assetUrl} |
| container="" |
| containerClass={className} |
| |
| height="100vh" |
| width="100%" |
| |
| // to access a plugin we must use viewer.getPlugin() |
| plugins={[[LensflarePlugin, { lensflares: [] }]]} |
| |
| {...options} |
| |
| // note: photo sphere viewer performs an aggressive caching of our callbacks, |
| // so we aggressively disable it by using a ref |
| onClick={() => { |
| // nothing to do here |
| }} |
| |
| onReady={(instance) => { |
| viewerRef.current = instance |
| viewerContainerRef.current = instance.container |
| |
| /* |
| const markersPlugs = instance.getPlugin(MarkersPlugin); |
| if (!markersPlugs) |
| return; |
| markersPlugs.addMarker({ |
| id: "imageLayer2", |
| imageLayer: "drone.png", |
| size: { width: 220, height: 220 }, |
| position: { yaw: '130.5deg', pitch: '-0.1deg' }, |
| tooltip: "Image embedded in the scene" |
| }); |
| markersPlugs.addEventListener("select-marker", () => { |
| console.log("asd"); |
| }); |
| */ |
| }} |
| |
| /> |
| </div> |
| ) |
| } |