Recap of the previous chapter: Points in 3D Space
You learned about the Five SDK's event system and built a small app that reads the 3D position of a point from a click event.
In this chapter you will learn
How to place tags in 3D space.
Getting Ready
Create a new directory (src/5.tagging) along with its html file and jsx or tsx file. Carrying over the State code from the previous chapter would be too cumbersome, so we will build on the content of the Displaying a 3D Space chapter instead.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tagging</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body #app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index"></script>
</body>
</html>import { useState, useEffect } from "react";
import { parseWork } from "@realsee/five";
/**
* React Hook: fetch the work object from a work.json URL
* @param url the data URL of work.json
* @returns the work object, or null while it is still loading
*/
function useFetchWork(url) {
const [work, setWork] = useState(null);
useEffect(() => {
setWork(null);
fetch(url)
.then((response) => response.text())
.then((text) => setWork(parseWork(text)));
}, [url]);
return work;
}
export { useFetchWork };import { useState, useEffect } from "react";
/**
* Get the current window dimensions
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: get the current window dimensions
*/
function useWindowDimensions() {
const [size, setSize] = useState(getWindowDimensions);
useEffect(() => {
const listener = () => setSize(getWindowDimensions());
window.addEventListener("resize", listener, false);
return () => window.removeEventListener("resize", listener, false);
});
return size;
}
export { useWindowDimensions };import React from "react";
import { Five } from "@realsee/five";
import { useFiveCurrentState } from "@realsee/five/react";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
/**
* React Component: mode control
*/
const ModeController = () => {
const [state, setState] = useFiveCurrentState();
return (
<Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={state.mode}
onChange={(_, newValue) => {
setState({ mode: newValue });
}}
>
<BottomNavigationAction
label="Panorama Roaming"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="Floorplan Overview"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
};
export { ModeController };import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
/** the data URL of work.json */
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return (
work && (
<FiveProvider initialWork={work}>
<FiveCanvas {...size} />
<ModeController />
</FiveProvider>
)
);
};
export { App };import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};import { useState, useEffect } from "react";
import { Work, parseWork } from "@realsee/five";
/**
* React Hook: fetch the work object from a work.json URL
* @param url the data URL of work.json
* @returns the work object, or null while it is still loading
*/
function useFetchWork(url: string) {
const [work, setWork] = useState<Work | null>(null);
useEffect(() => {
setWork(null);
fetch(url)
.then((response) => response.text())
.then((text) => setWork(parseWork(text)));
}, [url]);
return work;
}
export { useFetchWork };import { useState, useEffect } from "react";
/**
* Get the current window dimensions
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: get the current window dimensions
*/
function useWindowDimensions() {
const [size, setSize] = useState(getWindowDimensions);
useEffect(() => {
const listener = () => setSize(getWindowDimensions());
window.addEventListener("resize", listener, false);
return () => window.removeEventListener("resize", listener, false);
});
return size;
}
export { useWindowDimensions };import React, { FC } from "react";
import { Five, Mode } from "@realsee/five";
import { useFiveCurrentState } from "@realsee/five/react";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
/**
* React Component: mode control
*/
const ModeController: FC = () => {
const [state, setState] = useFiveCurrentState();
return (
<Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={state.mode}
onChange={(_, newValue: Mode) => {
setState({ mode: newValue });
}}
>
<BottomNavigationAction
label="Panorama Roaming"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="Floorplan Overview"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
};
export { ModeController };import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
/** the data URL of work.json */
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App: FC = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return (
work && (
<FiveProvider initialWork={work}>
<FiveCanvas {...size} />
<ModeController />
</FiveProvider>
)
);
};
export { App };import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};Start the dev server with npm run dev, then navigate to the page at "http://localhost:3000/src/5.tagging/index.html".
Info
Check your console: the port number may change depending on your configuration and whichever ports are currently in use, so always trust the value printed in the console. If you use a different build tool, start the server according to that tool's requirements.
Building the Tagging Feature
Adding Tag Styles
Add the tag styles to the html file.
The styles are not strictly required; they just make the tags look nicer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tagging</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body #app {
width: 100%;
height: 100%;
overflow: hidden;
}
/* highlight-start */
.tag {
position: absolute;
width: 0;
height: 0;
transform: translateZ(0);
}
.tag-pannel {
position: absolute;
width: 100px;
min-height: 20px;
transform: translate(-50%, 0);
left: 50%;
bottom: 10px;
background: #333;
color: #fff;
border-radius: 2px;
text-align: center;
line-height: 20px;
padding: 8px;
font-size: 14px;
}
.tag-pannel:after {
content: "";
display: block;
position: absolute;
width: 10px;
height: 10px;
left: 50%;
bottom: -5px;
transform: translate(-50%, 0) rotate(45deg);
background: #333;
pointer-events: none;
}
/* highlight-end */
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index"></script>
</body>
</html>About useFiveProject2d
This chapter uses the `useFiveProject2d` method. It maps a 3D coordinate to a 2D screen coordinate.
useFiveProject2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null
- Pass in a 3D coordinate to get back a 2D screen coordinate, with the origin at the top-left and the unit in pixels. You can use it like
{ left: returnValue.x + "px", top: returnValue.y + "px" }. - If the 3D coordinate cannot be projected onto the screen (for example, it is behind the camera or occluded), it returns
null. - The second parameter, testModel, controls whether model collision is taken into account, i.e. whether a coordinate occluded by the model returns
null.
Writing the TaggingController
- Add a TaggingController file for the component.
- Store the tag positions and text in the
tagsReact state. - Store the tag currently being created in the
newTagReact state. - Listen for Five SDK's
intersectionOnModelUpdateevent to keep the tag being created positioned under the cursor. - Use the
project2dmethod produced by useFiveProject2d (inside thetagElementmethod) to obtain the canvas screen coordinate of each tag, then render it by adjusting its style.
import React, { useState, useCallback } from "react";
import { useFiveEventCallback, useFiveProject2d } from "@realsee/five/react";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
/**
* React Component: tagging
*/
const TaggingController = () => {
const project2d = useFiveProject2d();
const [tags, setTags] = useState([]);
const [newTag, setNewTag] = useState(null);
const tagElement = useCallback((tag, key) => {
const position = tag.position && project2d(tag.position, true);
const style = position
? { left: position.x, top: position.y }
: { display: "none" };
return (
<div className="tag" style={style} key={key}>
<div className="tag-pannel">
<span className="tag-content">{tag.label}</span>
</div>
</div>
);
}, []);
const addTag = useCallback(() => {
setNewTag({ label: window.prompt("Add a tag", "") || "Untitled" });
}, []);
useFiveEventCallback(
"intersectionOnModelUpdate",
(intersect) => {
if (newTag) setNewTag({ position: intersect.point, label: newTag.label });
},
[newTag]
);
useFiveEventCallback(
"wantsTapGesture",
(raycaster) => {
if (newTag && newTag.position) {
setTags((tags) => tags.concat(newTag));
setNewTag(null);
return false;
}
},
[newTag]
);
return (
<React.Fragment>
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button onClick={addTag}>Add Tag</Button>
</Paper>
{newTag && tagElement(newTag)}
{tags.map((tag, index) => tagElement(tag, index))}
</React.Fragment>
);
};
export { TaggingController };import * as THREE from "three";
import React, { FC, useState, useCallback } from "react";
import { useFiveEventCallback, useFiveProject2d } from "@realsee/five/react";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
/**
* React Component: tagging
*/
const TaggingController: FC = () => {
type Tag = { position?: THREE.Vector3; label: string };
const project2d = useFiveProject2d();
const [tags, setTags] = useState<Tag[]>([]);
const [newTag, setNewTag] = useState<Tag | null>(null);
const tagElement = useCallback((tag, key?: number | string) => {
const position = tag.position && project2d(tag.position, true);
const style = position
? { left: position.x, top: position.y }
: { display: "none" };
return (
<div className="tag" style={style} key={key}>
<div className="tag-pannel">
<span className="tag-content">{tag.label}</span>
</div>
</div>
);
}, []);
const addTag = useCallback(() => {
setNewTag({ label: window.prompt("Add a tag", "") || "Untitled" });
}, []);
useFiveEventCallback(
"intersectionOnModelUpdate",
(intersect) => {
if (newTag) setNewTag({ position: intersect.point, label: newTag.label });
},
[newTag]
);
useFiveEventCallback(
"wantsTapGesture",
(raycaster) => {
if (newTag && newTag.position) {
setTags((tags) => tags.concat(newTag));
setNewTag(null);
return false;
}
},
[newTag]
);
return (
<React.Fragment>
<Paper sx={{ position: "fixed", top: 10, left: 10 }}>
<Button onClick={addTag}>Add Tag</Button>
</Paper>
{newTag && tagElement(newTag)}
{tags.map((tag, index) => tagElement(tag, index))}
</React.Fragment>
);
};
export { TaggingController };Using the Tagging Component
Insert it into the FiveProvider in the App file.
import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { TaggingController } from "./TaggingController";
// highlight-end
/** the data URL of work.json */
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return (
work && (
<FiveProvider initialWork={work}>
<FiveCanvas {...size} />
<ModeController />
// highlight-start
<TaggingController />
// highlight-end
</FiveProvider>
)
);
};
export { App };import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { TaggingController } from "./TaggingController";
// highlight-end
/** the data URL of work.json */
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App: FC = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return (
work && (
<FiveProvider initialWork={work}>
<FiveCanvas {...size} />
<ModeController />
// highlight-start
<TaggingController />
// highlight-end
</FiveProvider>
)
);
};
export { App };Go back to your browser, and you will see an "Add Tag" button in the top-left corner of the page. Click it, enter a tag name, then move your mouse, click where you want the tag, and the tag is placed.
A handy little feature indeed 🥳.
