Spaces:
Runtime error
Runtime error
Commit
·
1e2c870
1
Parent(s):
a60cb50
improvements
Browse files- src/app/games/index.ts +1 -1
- src/app/main.tsx +7 -3
- src/app/queries/getActionnables.ts +4 -19
- src/components/business/cartesian-image.tsx +8 -11
- src/components/business/cartesian-video.tsx +3 -11
- src/components/business/renderer.tsx +12 -6
- src/components/business/spherical-image.tsx +175 -63
- src/lib/getCoordinatesFromMousePosition.ts +26 -19
- src/lib/lightSourceNames.ts +11 -0
- src/lib/normalizeActionnables.ts +42 -0
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 = "
|
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: (
|
|
|
78 |
? nextActionnables
|
79 |
: game.initialActionnables
|
80 |
-
)
|
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[])
|
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
|
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
|
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,
|
46 |
}
|
47 |
|
48 |
if (!rendered.assetUrl) {
|
@@ -50,7 +43,11 @@ export function CartesianImage({
|
|
50 |
}
|
51 |
return (
|
52 |
<div
|
53 |
-
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
|
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,
|
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 |
-
|
|
|
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
|
|
|
|
|
|
|
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-
|
183 |
-
: "w-full
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
21 |
const [lastSceneConfig, setLastSceneConfig] = useState<string>(sceneConfig)
|
22 |
-
const
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
27 |
|
28 |
const options = {
|
29 |
-
defaultZoomLvl
|
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 (!
|
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
|
64 |
-
...
|
65 |
showLoader: false,
|
66 |
})
|
67 |
-
|
|
|
|
|
|
|
|
|
68 |
// console.log("SphericalImage: asking to re-render")
|
69 |
-
|
70 |
|
71 |
setLastSceneConfig(sceneConfig)
|
72 |
}
|
73 |
}
|
74 |
task()
|
75 |
-
}, [sceneConfig, rendered.assetUrl,
|
76 |
|
77 |
-
const
|
|
|
|
|
|
|
78 |
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
}
|
89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
|
91 |
if (!rendered.assetUrl) {
|
92 |
return null
|
93 |
}
|
94 |
|
95 |
return (
|
96 |
-
<
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
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 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
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 |
-
//
|
23 |
const relativeX = 2 * (mouseX / containerWidth) - 1;
|
24 |
const relativeY = 2 * (mouseY / containerHeight) - 1;
|
25 |
|
26 |
-
//
|
27 |
-
const deltaYaw = relativeX * fov /
|
28 |
-
const deltaPitch = relativeY * fov /
|
|
|
|
|
|
|
|
|
29 |
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
|
34 |
-
//
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
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 |
+
}
|