File size: 9,391 Bytes
a60cb50
1e2c870
a60cb50
40fde09
fd2aa6b
a60cb50
ab75c71
1e2c870
 
 
 
40fde09
4251a28
6311de2
40fde09
 
819e443
40fde09
6311de2
ab75c71
40fde09
819e443
4251a28
1e2c870
 
 
 
 
 
fd6f6bd
1e2c870
 
 
 
 
 
fd6f6bd
 
1e2c870
a60cb50
7064b36
1e2c870
fd6f6bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e2c870
ab75c71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd6f6bd
 
 
 
 
 
1e2c870
fd6f6bd
 
 
 
1e2c870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd6f6bd
 
 
 
1e2c870
 
fd6f6bd
 
1e2c870
 
 
7064b36
1e2c870
fd6f6bd
1e2c870
fd6f6bd
 
 
 
 
1e2c870
40fde09
1e2c870
 
 
 
a60cb50
1e2c870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a60cb50
3370e14
 
 
40fde09
 
1e2c870
 
 
 
 
40fde09
1e2c870
 
 
 
 
40fde09
1e2c870
 
 
 
 
 
 
 
 
fd2aa6b
1e2c870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40fde09
1e2c870
 
40fde09
4251a28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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 { RenderedScene } from "@/types"

import { MouseEventHandler } 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 // 0 = 180 fov, 100 = 1 fov

  const options = {
    defaultZoomLvl,
    fisheye: false, // ..no!
    overlay: rendered.maskUrl || undefined,
    overlayOpacity: debug ? 0.5 : 0,
    /*
    panoData: {
      fullWidth: 2000,
      fullHeight: 1200,
      croppedWidth: 1024,
      croppedHeight: 512,
      croppedX: 0,
      croppedY: 200,
      // poseHeading: 0, // 0 to 360
      posePitch: 0, // -90 to 90
      // poseRoll: 0, // -180 to 180
    }
    */
  }


  const cacheRef = useRef("")
  useEffect(() => {
    const listener = (e: DragEvent) => {
      if (!rootContainerRef.current) { return }

      // TODO: check if we are currently dragging an object
      // if yes, then we should check if clientX and clientY are matching the 
      const boundingRect = rootContainerRef.current.getBoundingClientRect()

      // abort if we are not currently dragging over our display area
      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}`

      // to avoid use
      if (cacheRef.current === key) {
        return
      }
      // console.log(`DRAG: calling onEvent("hover", ${relativeX}, ${relativeY})`)

      cacheRef.current = key
      onEvent("hover", relativeX, relativeY)
    }

    document.addEventListener('drag', listener)

    return () => {
      document.removeEventListener('drag', listener)
    }
  }, [onEvent])

  useEffect(() => {
    const task = async () => {
      // console.log("SphericalImage: useEffect")
      if (sceneConfig !== lastSceneConfig) {
        // console.log("SphericalImage: scene config changed!")
    
        if (!viewerRef.current) {
          // console.log("SphericalImage: no ref!")
          setLastSceneConfig(sceneConfig)
          return
        }
        const viewer = viewerRef.current

        const newOptions = {
          ...options,
        }

        const lensflares: { id: string; position: SphericalPosition; type: number }[] = []
          
        if (maskDimension.width && imageDimension.width) {

          // console.log("rendered.segments:", rendered.segments)
   
          rendered.segments
            .filter(segment => lightSourceNames.includes(segment.label))
            .forEach(light => {
            // console.log("light detected", light)
            const [x1, y1, x2, y2] = light.box
            const [centerX, centerY] = [(x1 + x2) / 2, (y1 + y2) / 2]
            // console.log("center:", { centerX, centerY })
            const [relativeX, relativeY] = [centerX / maskDimension.width, centerY/ maskDimension.height]
            // console.log("relative:", { relativeX, relativeY})

            const panoramaPosition: PanoramaPosition = {
              textureX: relativeX * imageDimension.width,
              textureY: relativeY * imageDimension.height
            }
            // console.log("panoramaPosition:", panoramaPosition)

            const position = viewer.dataHelper.textureCoordsToSphericalCoords(panoramaPosition)
            // console.log("sphericalPosition:", position)
            if ( // make sure coordinates are valid
              !isNaN(position.pitch) && isFinite(position.pitch) &&
              !isNaN(position.yaw) && isFinite(position.yaw)) {
              lensflares.push({
                id: `flare_${lensflares.length}`,
                position,
                type: 0,
              })
            }
          })
        }

        // console.log("lensflares:", lensflares)
        const lensFlarePlugin = viewer.getPlugin<LensflarePlugin>("lensflare")
        lensFlarePlugin.setLensflares(lensflares)

        // console.log("SphericalImage: calling setOptions")
        // console.log("SphericalImage: changing the panorama to: " + rendered.assetUrl.slice(0, 120))
      
        await viewer.setPanorama(rendered.assetUrl, {
          ...newOptions,
          showLoader: false,
        })

        // TODO we should separate all those updates, probaby
        viewer.setOptions(newOptions)
        // viewer.setOverlay(rendered.maskUrl || undefined)

        // console.log("SphericalImage: asking to re-render")
        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 (isClick) console.log(`handleEvent(${isClick})`, {
      " imageDimension.width": imageDimension.width,
      "rendered.maskUrl": rendered.maskUrl
    })
    */
  
    if (!viewer || !rootContainer || !viewerContainer || !imageDimension.width || !rendered.maskUrl) {
      return
    }

    const containerRect = viewerContainer.getBoundingClientRect()
    // if (isClick) console.log("containerRect:", containerRect)

    const containerY = event.clientY - containerRect.top
    // console.log("containerY:", containerY)
    
    const position: Position = viewer.getPosition()

    const viewerPosition: Point = viewer.dataHelper.sphericalCoordsToViewerCoords(position)
    // if (isClick) console.log("viewerPosition:", viewerPosition)

    // we want to ignore events that are happening in the toolbar
    // note that we will probably hide this toolbar at some point,
    // to implement our own UI
    if (isClick && containerY > (containerRect.height - 40)) {
      // console.log("we are in the toolbar.. ignoring the click")
      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>
  )
}