jbilcke-hf HF staff commited on
Commit
1e2c870
·
1 Parent(s): a60cb50

improvements

Browse files
src/app/games/index.ts CHANGED
@@ -13,6 +13,6 @@ import { game as nexus } from "./nexus"
13
 
14
  export const games = { pirates, city, dungeon, doom, vernian, enchanters, flamenco, pharaoh, tensor, nexus}
15
 
16
- export const defaultGame: GameType = "tensor"
17
 
18
  export const getGame = (type?: GameType) => games[type || defaultGame] || games[defaultGame]
 
13
 
14
  export const games = { pirates, city, dungeon, doom, vernian, enchanters, flamenco, pharaoh, tensor, nexus}
15
 
16
+ export const defaultGame: GameType = "dungeon"
17
 
18
  export const getGame = (type?: GameType) => games[type || defaultGame] || games[defaultGame]
src/app/main.tsx CHANGED
@@ -24,6 +24,7 @@ import { getBackground } from "@/app/queries/getBackground"
24
  import { getDialogue } from "@/app/queries/getDialogue"
25
  import { getActionnables } from "@/app/queries/getActionnables"
26
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
 
27
 
28
  const getInitialRenderedScene = (): RenderedScene => ({
29
  renderId: "",
@@ -74,10 +75,11 @@ export default function Main() {
74
  prompt: game.getScenePrompt(nextSituation).join(", "),
75
 
76
  // ACTIONNABLES
77
- actionnables: (Array.isArray(nextActionnables) && nextActionnables.length
 
78
  ? nextActionnables
79
  : game.initialActionnables
80
- ).slice(0, 10) // too many can slow us down it seems
81
  })
82
 
83
  console.log("got the first version of our scene!", newRendered)
@@ -170,7 +172,7 @@ export default function Main() {
170
  let newDialogue = ""
171
  try {
172
  newDialogue = await getDialogue({ game, situation, actionnable })
173
- console.log(`newDialogue:`, newDialogue)
174
  setDialogue(newDialogue)
175
  } catch (err) {
176
  console.log(`failed to generate dialogue (but it's only a nice to have, so..)`)
@@ -197,6 +199,8 @@ export default function Main() {
197
 
198
  const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
199
 
 
 
200
  const handleToggleDebug = (isToggledOn: boolean) => {
201
  const current = new URLSearchParams(Array.from(searchParams.entries()))
202
  current.set("debug", `${isToggledOn}`)
 
24
  import { getDialogue } from "@/app/queries/getDialogue"
25
  import { getActionnables } from "@/app/queries/getActionnables"
26
  import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
27
+ import { normalizeActionnables } from "@/lib/normalizeActionnables"
28
 
29
  const getInitialRenderedScene = (): RenderedScene => ({
30
  renderId: "",
 
75
  prompt: game.getScenePrompt(nextSituation).join(", "),
76
 
77
  // ACTIONNABLES
78
+ actionnables: normalizeActionnables(
79
+ Array.isArray(nextActionnables) && nextActionnables.length
80
  ? nextActionnables
81
  : game.initialActionnables
82
+ )
83
  })
84
 
85
  console.log("got the first version of our scene!", newRendered)
 
172
  let newDialogue = ""
173
  try {
174
  newDialogue = await getDialogue({ game, situation, actionnable })
175
+ // console.log(`newDialogue:`, newDialogue)
176
  setDialogue(newDialogue)
177
  } catch (err) {
178
  console.log(`failed to generate dialogue (but it's only a nice to have, so..)`)
 
199
 
200
  const clickables = Array.from(new Set(rendered.segments.map(s => s.label)).values())
201
 
202
+ // console.log("segments:", rendered.segments)
203
+
204
  const handleToggleDebug = (isToggledOn: boolean) => {
205
  const current = new URLSearchParams(Array.from(searchParams.entries()))
206
  current.set("debug", `${isToggledOn}`)
src/app/queries/getActionnables.ts CHANGED
@@ -4,6 +4,7 @@ import { parseJsonList } from "@/lib/parseJsonList"
4
 
5
  import { getBase } from "./getBase"
6
  import { predict } from "./predict"
 
7
 
8
  export const getActionnables = async ({
9
  game,
@@ -55,7 +56,7 @@ export const getActionnables = async ({
55
  }
56
  }
57
 
58
- let result = []
59
 
60
  try {
61
  result = parseJsonList(rawStringOutput)
@@ -68,33 +69,17 @@ export const getActionnables = async ({
68
 
69
  try {
70
  const sanitized = rawStringOutput.replaceAll("[", "").replaceAll("]", "")
71
- result = (JSON.parse(`[${sanitized}]`) as string[]).map(item =>
72
- // clean the words to remove any punctuation
73
- item.replace(/\W/g, '').trim()
74
- )
75
 
76
  if (!result.length) {
77
  throw new Error("no actionnables")
78
  }
79
  } catch (err) {
80
  console.log("failed to repair and recover a valid JSON! Using a generic fallback..")
81
-
82
- return [
83
- "door",
84
- "rock",
85
- "window",
86
- "table",
87
- "ground",
88
- "sky",
89
- "object",
90
- "tree",
91
- "wall",
92
- "floor"
93
- ]
94
 
95
  // throw new Error("failed to parse the actionnables")
96
  }
97
  }
98
 
99
- return result
100
  }
 
4
 
5
  import { getBase } from "./getBase"
6
  import { predict } from "./predict"
7
+ import { normalizeActionnables } from "@/lib/normalizeActionnables"
8
 
9
  export const getActionnables = async ({
10
  game,
 
56
  }
57
  }
58
 
59
+ let result: string[] = []
60
 
61
  try {
62
  result = parseJsonList(rawStringOutput)
 
69
 
70
  try {
71
  const sanitized = rawStringOutput.replaceAll("[", "").replaceAll("]", "")
72
+ result = (JSON.parse(`[${sanitized}]`) as string[])
 
 
 
73
 
74
  if (!result.length) {
75
  throw new Error("no actionnables")
76
  }
77
  } catch (err) {
78
  console.log("failed to repair and recover a valid JSON! Using a generic fallback..")
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  // throw new Error("failed to parse the actionnables")
81
  }
82
  }
83
 
84
+ return normalizeActionnables(result)
85
  }
src/components/business/cartesian-image.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import { useRef } from "react"
2
  import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
- import { useImageDimension } from "@/lib/useImageDimension"
5
 
6
  export function CartesianImage({
7
  rendered,
@@ -15,13 +14,11 @@ export function CartesianImage({
15
  debug?: boolean
16
  }) {
17
 
18
- const maskDimension = useImageDimension(rendered.maskUrl)
19
-
20
  const ref = useRef<HTMLImageElement>(null)
21
  const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
22
 
23
  if (!ref.current) {
24
- console.log("element isn't ready")
25
  return
26
  }
27
 
@@ -35,14 +32,10 @@ export function CartesianImage({
35
 
36
  // then we convert them to relative coordinates
37
  const relativeX = containerX / boundingRect.width
38
- const relativey = containerY / boundingRect.height
39
-
40
- // finally, we convert back to coordinates within the input image
41
- const imageX = relativeX * maskDimension.width
42
- const imageY = relativey * maskDimension.height
43
 
44
  const eventType = isClick ? "click" : "hover"
45
- onEvent(eventType, imageX, imageY)
46
  }
47
 
48
  if (!rendered.assetUrl) {
@@ -50,7 +43,11 @@ export function CartesianImage({
50
  }
51
  return (
52
  <div
53
- className={className}
 
 
 
 
54
  >
55
  <img
56
  src={rendered.assetUrl || undefined}
 
1
  import { useRef } from "react"
2
  import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
 
4
 
5
  export function CartesianImage({
6
  rendered,
 
14
  debug?: boolean
15
  }) {
16
 
 
 
17
  const ref = useRef<HTMLImageElement>(null)
18
  const handleEvent = async (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
19
 
20
  if (!ref.current) {
21
+ // console.log("element isn't ready")
22
  return
23
  }
24
 
 
32
 
33
  // then we convert them to relative coordinates
34
  const relativeX = containerX / boundingRect.width
35
+ const relativeY = containerY / boundingRect.height
 
 
 
 
36
 
37
  const eventType = isClick ? "click" : "hover"
38
+ onEvent(eventType, relativeX, relativeY)
39
  }
40
 
41
  if (!rendered.assetUrl) {
 
43
  }
44
  return (
45
  <div
46
+ className={[
47
+ "h-[512px]",
48
+ className
49
+ ].join(" ")
50
+ }
51
  >
52
  <img
53
  src={rendered.assetUrl || undefined}
src/components/business/cartesian-video.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import { useRef } from "react"
2
  import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
4
- import { useImageDimension } from "@/lib/useImageDimension"
5
 
6
  export function CartesianVideo({
7
  rendered,
@@ -14,13 +13,11 @@ export function CartesianVideo({
14
  className?: string
15
  debug?: boolean
16
  }) {
17
- const maskDimension = useImageDimension(rendered.maskUrl)
18
-
19
  const ref = useRef<HTMLVideoElement>(null)
20
  const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
21
 
22
  if (!ref.current) {
23
- console.log("element isn't ready")
24
  return
25
  }
26
 
@@ -34,15 +31,10 @@ export function CartesianVideo({
34
 
35
  // then we convert them to relative coordinates
36
  const relativeX = containerX / boundingRect.width
37
- const relativey = containerY / boundingRect.height
38
-
39
- // finally, we convert back to coordinates within the input image
40
- // TODO: go read the video size
41
- const contentX = relativeX * maskDimension.width
42
- const contentY = relativey * maskDimension.height
43
 
44
  const eventType = isClick ? "click" : "hover"
45
- onEvent(eventType, contentX, contentY)
46
  }
47
 
48
  return (
 
1
  import { useRef } from "react"
2
  import { SceneEventHandler } from "./types"
3
  import { RenderedScene } from "@/app/types"
 
4
 
5
  export function CartesianVideo({
6
  rendered,
 
13
  className?: string
14
  debug?: boolean
15
  }) {
 
 
16
  const ref = useRef<HTMLVideoElement>(null)
17
  const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
18
 
19
  if (!ref.current) {
20
+ // console.log("element isn't ready")
21
  return
22
  }
23
 
 
31
 
32
  // then we convert them to relative coordinates
33
  const relativeX = containerX / boundingRect.width
34
+ const relativeY = containerY / boundingRect.height
 
 
 
 
 
35
 
36
  const eventType = isClick ? "click" : "hover"
37
+ onEvent(eventType, relativeX, relativeY)
38
  }
39
 
40
  return (
src/components/business/renderer.tsx CHANGED
@@ -8,6 +8,7 @@ import { CartesianImage } from "./cartesian-image"
8
  import { SceneEventHandler, SceneEventType } from "./types"
9
  import { CartesianVideo } from "./cartesian-video"
10
  import { SphericalImage } from "./spherical-image"
 
11
 
12
  export const Renderer = ({
13
  rendered,
@@ -33,6 +34,7 @@ export const Renderer = ({
33
  const [progressPercent, setProcessPercent] = useState(0)
34
  const progressRef = useRef(0)
35
  const isLoadingRef = useRef(isLoading)
 
36
 
37
  useEffect(() => {
38
  if (!rendered.maskUrl) {
@@ -104,7 +106,8 @@ export const Renderer = ({
104
  return closestSegment;
105
  }
106
 
107
- const handleMouseEvent: SceneEventHandler = async (type: SceneEventType, x: number, y: number) => {
 
108
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
109
 
110
  if (isLoading) {
@@ -120,13 +123,16 @@ export const Renderer = ({
120
  return
121
  }
122
 
123
- const newSegment = getSegmentAt(x, y)
 
 
 
124
 
125
  if (actionnable !== newSegment.label) {
126
  if (newSegment.label) {
127
- console.log(`User is hovering "${newSegment.label}"`)
128
  } else {
129
- console.log(`Nothing in the area`)
130
  }
131
 
132
  // update the actionnable immediately, so we can show the hand / finger cursor pointer
@@ -179,8 +185,8 @@ export const Renderer = ({
179
  "relative border-2 border-gray-50 rounded-xl overflow-hidden",
180
  engine.type === "cartesian_video"
181
  || engine.type === "cartesian_image"
182
- ? " w-[1024px] h-[512px]" // w-[1024px] h-[512px]"
183
- : "w-full h-screen",
184
 
185
  isLoading
186
  ? "cursor-wait"
 
8
  import { SceneEventHandler, SceneEventType } from "./types"
9
  import { CartesianVideo } from "./cartesian-video"
10
  import { SphericalImage } from "./spherical-image"
11
+ import { useImageDimension } from "@/lib/useImageDimension"
12
 
13
  export const Renderer = ({
14
  rendered,
 
34
  const [progressPercent, setProcessPercent] = useState(0)
35
  const progressRef = useRef(0)
36
  const isLoadingRef = useRef(isLoading)
37
+ const maskDimension = useImageDimension(rendered.maskUrl)
38
 
39
  useEffect(() => {
40
  if (!rendered.maskUrl) {
 
106
  return closestSegment;
107
  }
108
 
109
+ // note: coordinates must be between 0 and 1
110
+ const handleMouseEvent: SceneEventHandler = async (type: SceneEventType, relativeX: number, relativeY: number) => {
111
  if (!contextRef.current) return; // Return early if mask image has not been loaded yet
112
 
113
  if (isLoading) {
 
123
  return
124
  }
125
 
126
+ const imageX = relativeX * maskDimension.width
127
+ const imageY = relativeY * maskDimension.height
128
+
129
+ const newSegment = getSegmentAt(imageX, imageY)
130
 
131
  if (actionnable !== newSegment.label) {
132
  if (newSegment.label) {
133
+ // console.log(`User is hovering "${newSegment.label}"`)
134
  } else {
135
+ // console.log(`Nothing in the area`)
136
  }
137
 
138
  // update the actionnable immediately, so we can show the hand / finger cursor pointer
 
185
  "relative border-2 border-gray-50 rounded-xl overflow-hidden",
186
  engine.type === "cartesian_video"
187
  || engine.type === "cartesian_image"
188
+ ? " w-full" // w-[1024px] h-[512px]"
189
+ : "w-full",
190
 
191
  isLoading
192
  ? "cursor-wait"
src/components/business/spherical-image.tsx CHANGED
@@ -1,10 +1,14 @@
1
  import { useEffect, useRef, useState } from "react"
2
- import { PluginConstructor } from "@photo-sphere-viewer/core"
3
  import { LensflarePlugin, ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
4
 
5
  import { RenderedScene } from "@/app/types"
6
 
7
  import { SceneEventHandler } from "./types"
 
 
 
 
8
 
9
  export function SphericalImage({
10
  rendered,
@@ -17,19 +21,24 @@ export function SphericalImage({
17
  className?: string
18
  debug?: boolean
19
  }) {
20
- const sceneConfig = JSON.stringify({ rendered, debug })
 
 
 
 
 
21
  const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig)
22
- const ref = useRef<{
23
- needsUpdate: () => void
24
- setPanorama: (src: string, options: Record<string, any>) => Promise<void>
25
- setOptions: (options: Record<string, any>) => void
26
- }>(null)
 
27
 
28
  const options = {
29
- defaultZoomLvl: 1,
30
- overlay: rendered.maskUrl || undefined,
31
- overlayOpacity: debug ? 0.5 : 0,
32
  fisheye: false, // ..no!
 
33
  /*
34
  panoData: {
35
  fullWidth: 2000,
@@ -45,91 +54,194 @@ export function SphericalImage({
45
  */
46
  }
47
 
 
48
  useEffect(() => {
49
  const task = async () => {
50
  // console.log("SphericalImage: useEffect")
51
  if (sceneConfig !== lastSceneConfig) {
52
  // console.log("SphericalImage: scene config changed!")
53
 
54
- if (!ref.current) {
55
  // console.log("SphericalImage: no ref!")
56
  setLastSceneConfig(sceneConfig)
57
  return
58
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  // console.log("SphericalImage: calling setOptions")
61
  // console.log("SphericalImage: changing the panorama to: " + rendered.assetUrl.slice(0, 120))
62
 
63
- await ref.current.setPanorama(rendered.assetUrl, {
64
- ...options,
65
  showLoader: false,
66
  })
67
- ref.current.setOptions(options)
 
 
 
 
68
  // console.log("SphericalImage: asking to re-render")
69
- ref.current.needsUpdate()
70
 
71
  setLastSceneConfig(sceneConfig)
72
  }
73
  }
74
  task()
75
- }, [sceneConfig, rendered.assetUrl, ref.current])
76
 
77
- const plugins: (PluginConstructor | [PluginConstructor, any])[] = [
 
 
 
78
 
79
- // for the lensflare plugin we are also gonna need aw ay to determine the position
80
- [LensflarePlugin, {
81
- lensflares: [
82
- {
83
- id: 'sun',
84
- position: { yaw: '145deg', pitch: '2deg' },
85
- type: 0,
86
- }
87
- ]
88
- }]
89
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  if (!rendered.assetUrl) {
92
  return null
93
  }
94
 
95
  return (
96
- <ReactPhotoSphereViewer
97
- src={rendered.assetUrl}
98
- ref={ref}
99
- container=""
100
- containerClass={className}
101
- //
102
- height="100vh"
103
- width="100%"
104
- // plugins={plugins}
105
-
106
- {...options}
107
-
108
- onClick={(data, instance) => {
109
- console.log("on click:")
110
- const position = data.target.getPosition()
111
- console.log("position:", position)
112
  }}
113
-
114
- onReady={(instance) => {
115
- console.log("spherical image display is ready")
116
- /*
117
- const markersPlugs = instance.getPlugin(MarkersPlugin);
118
- if (!markersPlugs)
119
- return;
120
- markersPlugs.addMarker({
121
- id: "imageLayer2",
122
- imageLayer: "drone.png",
123
- size: { width: 220, height: 220 },
124
- position: { yaw: '130.5deg', pitch: '-0.1deg' },
125
- tooltip: "Image embedded in the scene"
126
- });
127
- markersPlugs.addEventListener("select-marker", () => {
128
- console.log("asd");
129
- });
130
- */
131
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- />
 
134
  )
135
  }
 
1
  import { useEffect, useRef, useState } from "react"
2
+ import { PanoramaPosition, PluginConstructor, Point, Position, SphericalPosition, Viewer } from "@photo-sphere-viewer/core"
3
  import { LensflarePlugin, ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
4
 
5
  import { RenderedScene } from "@/app/types"
6
 
7
  import { SceneEventHandler } from "./types"
8
+ import { useImageDimension } from "@/lib/useImageDimension"
9
+ import { lightSourceNames } from "@/lib/lightSourceNames"
10
+
11
+ type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any])
12
 
13
  export function SphericalImage({
14
  rendered,
 
21
  className?: string
22
  debug?: boolean
23
  }) {
24
+
25
+
26
+ const imageDimension = useImageDimension(rendered.assetUrl)
27
+ const maskDimension = useImageDimension(rendered.maskUrl)
28
+
29
+ const sceneConfig = JSON.stringify({ rendered, debug, imageDimension, maskDimension })
30
  const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig)
31
+ const rootContainerRef = useRef<HTMLDivElement>(null)
32
+ const viewerContainerRef = useRef<HTMLElement>()
33
+ const viewerRef = useRef<Viewer>()
34
+ const [mouseMoved, setMouseMoved] = useState<boolean>(false)
35
+
36
+ const defaultZoomLvl = 1 // 0 = 180 fov, 100 = 1 fov
37
 
38
  const options = {
39
+ defaultZoomLvl,
 
 
40
  fisheye: false, // ..no!
41
+ overlayOpacity: debug ? 0.5 : 0,
42
  /*
43
  panoData: {
44
  fullWidth: 2000,
 
54
  */
55
  }
56
 
57
+
58
  useEffect(() => {
59
  const task = async () => {
60
  // console.log("SphericalImage: useEffect")
61
  if (sceneConfig !== lastSceneConfig) {
62
  // console.log("SphericalImage: scene config changed!")
63
 
64
+ if (!viewerRef.current) {
65
  // console.log("SphericalImage: no ref!")
66
  setLastSceneConfig(sceneConfig)
67
  return
68
  }
69
+ const viewer = viewerRef.current
70
+
71
+ const newOptions = {
72
+ ...options,
73
+ }
74
+
75
+ const lensflares: { id: string; position: SphericalPosition; type: number }[] = []
76
+
77
+ if (maskDimension.width && imageDimension.width) {
78
+
79
+ // console.log("rendered.segments:", rendered.segments)
80
+
81
+ rendered.segments
82
+ .filter(segment => lightSourceNames.includes(segment.label))
83
+ .forEach(light => {
84
+ // console.log("light detected", light)
85
+ const [x1, y1, x2, y2] = light.box
86
+ const [centerX, centerY] = [(x1 + x2) / 2, (y1 + y2) / 2]
87
+ // console.log("center:", { centerX, centerY })
88
+ const [relativeX, relativeY] = [centerX / maskDimension.width, centerY/ maskDimension.height]
89
+ // console.log("relative:", { relativeX, relativeY})
90
+
91
+ const panoramaPosition: PanoramaPosition = {
92
+ textureX: relativeX * imageDimension.width,
93
+ textureY: relativeY * imageDimension.height
94
+ }
95
+ // console.log("panoramaPosition:", panoramaPosition)
96
+
97
+ const position = viewer.dataHelper.textureCoordsToSphericalCoords(panoramaPosition)
98
+ // console.log("sphericalPosition:", position)
99
+ if ( // make sure coordinates are valid
100
+ !isNaN(position.pitch) && isFinite(position.pitch) &&
101
+ !isNaN(position.yaw) && isFinite(position.yaw)) {
102
+ lensflares.push({
103
+ id: `flare_${lensflares.length}`,
104
+ position,
105
+ type: 0,
106
+ })
107
+ }
108
+ })
109
+ }
110
+
111
+ // console.log("lensflares:", lensflares)
112
+ const lensFlarePlugin = viewer.getPlugin<LensflarePlugin>("lensflare")
113
+ lensFlarePlugin.setLensflares(lensflares)
114
 
115
  // console.log("SphericalImage: calling setOptions")
116
  // console.log("SphericalImage: changing the panorama to: " + rendered.assetUrl.slice(0, 120))
117
 
118
+ await viewer.setPanorama(rendered.assetUrl, {
119
+ ...newOptions,
120
  showLoader: false,
121
  })
122
+
123
+ // TODO we should separate all those updates, probaby
124
+ viewer.setOptions(newOptions)
125
+ viewer.setOverlay(rendered.maskUrl || undefined)
126
+
127
  // console.log("SphericalImage: asking to re-render")
128
+ viewerRef.current.needsUpdate()
129
 
130
  setLastSceneConfig(sceneConfig)
131
  }
132
  }
133
  task()
134
+ }, [sceneConfig, rendered.assetUrl, viewerRef.current, maskDimension.width, imageDimension])
135
 
136
+ const handleEvent = async (event: React.MouseEvent<HTMLDivElement, MouseEvent>, isClick: boolean) => {
137
+ const rootContainer = rootContainerRef.current
138
+ const viewer = viewerRef.current
139
+ const viewerContainer = viewerContainerRef.current
140
 
141
+ /*
142
+ if (isClick) console.log(`handleEvent(${isClick})`, {
143
+ " imageDimension.width": imageDimension.width,
144
+ "rendered.maskUrl": rendered.maskUrl
145
+ })
146
+ */
147
+
148
+ if (!viewer || !rootContainer || !viewerContainer || !imageDimension.width || !rendered.maskUrl) {
149
+ return
150
+ }
151
+
152
+ const containerRect = viewerContainer.getBoundingClientRect()
153
+ // if (isClick) console.log("containerRect:", containerRect)
154
+
155
+ const containerY = event.clientY - containerRect.top
156
+ // console.log("containerY:", containerY)
157
+
158
+ const position: Position = viewer.getPosition()
159
+
160
+ const viewerPosition: Point = viewer.dataHelper.sphericalCoordsToViewerCoords(position)
161
+ // if (isClick) console.log("viewerPosition:", viewerPosition)
162
+
163
+ // we want to ignore events that are happening in the toolbar
164
+ // note that we will probably hide this toolbar at some point,
165
+ // to implement our own UI
166
+ if (isClick && containerY > (containerRect.height - 40)) {
167
+ // console.log("we are in the toolbar.. ignoring the click")
168
+ return
169
+ }
170
+
171
+ const panoramaPosition: PanoramaPosition = viewer.dataHelper.sphericalCoordsToTextureCoords(position)
172
+
173
+ if (typeof panoramaPosition.textureX !== "number" || typeof panoramaPosition.textureY !== "number") {
174
+ return
175
+ }
176
+
177
+ const relativeX = panoramaPosition.textureX / imageDimension.width
178
+ const relativeY = panoramaPosition.textureY / imageDimension.height
179
+
180
+ onEvent(isClick ? "click" : "hover", relativeX, relativeY)
181
+ }
182
 
183
  if (!rendered.assetUrl) {
184
  return null
185
  }
186
 
187
  return (
188
+ <div
189
+ ref={rootContainerRef}
190
+ onMouseMove={(event) => {
191
+ handleEvent(event, false)
192
+ setMouseMoved(true)
 
 
 
 
 
 
 
 
 
 
 
193
  }}
194
+ onMouseUp={(event) => {
195
+ if (!mouseMoved) {
196
+ handleEvent(event, true)
197
+ }
198
+ setMouseMoved(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }}
200
+ onMouseDown={() => {
201
+ setMouseMoved(false)
202
+ }}
203
+ >
204
+ <ReactPhotoSphereViewer
205
+ src={rendered.assetUrl}
206
+ container=""
207
+ containerClass={className}
208
+
209
+ height="60vh"
210
+ width="100%"
211
+
212
+ // to access a plugin we must use viewer.getPlugin()
213
+ plugins={[[LensflarePlugin, { lensflares: [] }]]}
214
+
215
+ {...options}
216
+
217
+ // note: photo sphere viewer performs an aggressive caching of our callbacks,
218
+ // so we aggressively disable it by using a ref
219
+ onClick={() => {
220
+ // nothing to do here
221
+ }}
222
+
223
+ onReady={(instance) => {
224
+ viewerRef.current = instance
225
+ viewerContainerRef.current = instance.container
226
+
227
+ /*
228
+ const markersPlugs = instance.getPlugin(MarkersPlugin);
229
+ if (!markersPlugs)
230
+ return;
231
+ markersPlugs.addMarker({
232
+ id: "imageLayer2",
233
+ imageLayer: "drone.png",
234
+ size: { width: 220, height: 220 },
235
+ position: { yaw: '130.5deg', pitch: '-0.1deg' },
236
+ tooltip: "Image embedded in the scene"
237
+ });
238
+ markersPlugs.addEventListener("select-marker", () => {
239
+ console.log("asd");
240
+ });
241
+ */
242
+ }}
243
 
244
+ />
245
+ </div>
246
  )
247
  }
src/lib/getCoordinatesFromMousePosition.ts CHANGED
@@ -9,31 +9,38 @@ export function getCoordinatesFromMousePosition({
9
  imageHeight,
10
  fov,
11
  }: {
12
- mouseX: number
13
- mouseY: number
14
- containerWidth: number
15
- containerHeight: number
16
- currentYaw: number
17
- currentPitch: number
18
- imageWidth: number
19
- imageHeight: number
20
- fov: number
21
  }): { x: number; y: number } {
22
- // Convert mouse position to relative to the viewer's center
23
  const relativeX = 2 * (mouseX / containerWidth) - 1;
24
  const relativeY = 2 * (mouseY / containerHeight) - 1;
25
 
26
- // Calculate angle differences (in degrees)
27
- const deltaYaw = relativeX * fov / 2;
28
- const deltaPitch = relativeY * fov / 2;
 
 
 
 
29
 
30
- // Calculate new yaw and pitch
31
- const newYaw = currentYaw + deltaYaw;
32
- const newPitch = currentPitch + deltaPitch;
33
 
34
- // Now convert these yaw, pitch back to (x, y) on the image
35
- const x = ((newYaw + 180) / 360) * imageWidth;
36
- const y = ((newPitch + 90) / 180) * imageHeight;
 
 
 
37
 
38
  return { x, y };
39
  }
 
9
  imageHeight,
10
  fov,
11
  }: {
12
+ mouseX: number,
13
+ mouseY: number,
14
+ containerWidth: number,
15
+ containerHeight: number,
16
+ currentYaw: number,
17
+ currentPitch: number,
18
+ imageWidth: number,
19
+ imageHeight: number,
20
+ fov: number,
21
  }): { x: number; y: number } {
22
+ // Considering the full width/height of FOV
23
  const relativeX = 2 * (mouseX / containerWidth) - 1;
24
  const relativeY = 2 * (mouseY / containerHeight) - 1;
25
 
26
+ // yaw varies with FOV over width and pitch over height
27
+ const deltaYaw = relativeX * fov * (Math.PI / 180);
28
+ const deltaPitch = relativeY * fov * (Math.PI / 180);
29
+
30
+ let newYaw = currentYaw + deltaYaw;
31
+ while (newYaw < 0) newYaw += 2 * Math.PI;
32
+ while (newYaw > 2 * Math.PI) newYaw -= 2 * Math.PI;
33
 
34
+ let newPitch = currentPitch + deltaPitch;
35
+ if (newPitch < -Math.PI / 2) newPitch = -Math.PI / 2;
36
+ if (newPitch > Math.PI / 2) newPitch = Math.PI / 2;
37
 
38
+ // Changing origin for the yaw rotation to bring image center to screen center
39
+ newYaw = (newYaw + Math.PI) % (2 * Math.PI);
40
+
41
+ // Image X corresponds to Yaw and Y corresponds to Pitch
42
+ const x = ((newYaw / (2 * Math.PI)) * imageWidth) % imageWidth;
43
+ const y = (((newPitch + Math.PI / 2) / Math.PI) * imageHeight) % imageHeight;
44
 
45
  return { x, y };
46
  }
src/lib/lightSourceNames.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const lightSourceNames = [
2
+ "window",
3
+ "light",
4
+ "sun",
5
+ "torch",
6
+ "fire",
7
+ "lights",
8
+ "torches",
9
+ "fires",
10
+ "fireplace"
11
+ ]
src/lib/normalizeActionnables.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { lightSourceNames } from "./lightSourceNames"
2
+
3
+ export function normalizeActionnables(rawActionnables: string[]) {
4
+
5
+ const tmp = rawActionnables.map(item =>
6
+ // clean the words to remove any punctuation
7
+ item.replace(/\W/g, '').trim()
8
+ )
9
+
10
+ const deduplicated = new Set<string>([
11
+ ...tmp,
12
+ // in case result is too small, we add a reserve of useful words here
13
+ "door",
14
+ "rock",
15
+ "window",
16
+ "table",
17
+ "ground",
18
+ "sky",
19
+ "object",
20
+ "tree",
21
+ "wall",
22
+ "floor"
23
+ // but we still only want 10 here
24
+ ].slice(0, 10)
25
+ )
26
+
27
+ console.log("deduplicated:", deduplicated)
28
+
29
+ let actionnables = Array.from(deduplicated.values())
30
+
31
+ // if we are missing a light source, we add one (the generic "light")
32
+ if (!actionnables.some(actionnable => lightSourceNames.includes(actionnable))) {
33
+ actionnables.push("light")
34
+ }
35
+
36
+ // if ground surfaces aren't in the list, we add at least one (the most generic)
37
+ // if (!actionnables.includes("floor") || !actionnables.includes("ground")) {
38
+ // actionnables.push("floor")
39
+ // }
40
+
41
+ return actionnables
42
+ }