Recap of the previous chapter: Points in 3D
You learned about the Five SDK event system and built a small app that obtains the 3D position of a point through click events.
In this chapter you will learn
How to place tags in 3D space.
Getting ready
Create a new directory (src/5.tagging) together with the corresponding html file and a js or ts file. Carrying over the State code from the previous chapter would be too cumbersome, so we will build on top of the content from the Displaying 3D Space chapter.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Points in 3d</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
html, body, #app { width: 100%; height: 100%; overflow: hidden; }
</style>
</head>
<body>
<div id="app"></div>
<!-- Mode switching -->
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button class="btn btn-primary active js-Panorama">Panorama walkthrough</button>
<button class="btn btn-primary js-Floorplan">Space overview</button>
</div>
</div>
</nav>
<script type="module" src="./index"></script>
</body>
</html>import { Five, parseWork } from "@realsee/five";
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const five = new Five();
five.appendTo(document.querySelector("#app"));
fetch(workURL).then(res => res.json()).then((json) => {
const work = parseWork(json);
five.load(work);
});
window.addEventListener("resize", () => five.refresh(), false);
{// === Mode switching ===
const buttons = {
"Panorama": document.querySelector(".js-Panorama"),
"Floorplan": document.querySelector(".js-Floorplan")
};
for (const [modeName, element] of Object.entries(buttons)) {
element.addEventListener("click", () => {
five.setState({ mode: modeName });
}, false);
}
five.on("stateChange", state => {
for (const [modeName, element] of Object.entries(buttons)) {
if (modeName === state.mode) {
element.classList.add("active");
} else {
element.classList.remove("active");
}
};
});
}
export {};import { Five, Mode, parseWork } from "@realsee/five";
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const five = new Five();
five.appendTo(document.querySelector("#app")!);
fetch(workURL).then(res => res.json()).then((json) => {
const work = parseWork(json);
five.load(work);
});
window.addEventListener("resize", () => five.refresh(), false);
{// === Mode switching ===
const buttons: Partial<Record<Mode, Element>> = {
"Panorama": document.querySelector(".js-Panorama")!,
"Floorplan": document.querySelector(".js-Floorplan")!
};
for (const [modeName, element] of Object.entries(buttons)) {
element.addEventListener("click", () => {
five.setState({ mode: modeName as Mode });
}, false);
}
five.on("stateChange", state => {
for (const [modeName, element] of Object.entries(buttons)) {
if (modeName === state.mode) {
element.classList.add("active");
} else {
element.classList.remove("active");
}
};
});
}
export {};Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/5.tagging/index.html".
Info
Please check your console: the port number may change depending on your configuration and which ports are currently in use, so always rely on 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">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Displaying work</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
html, body, #app { width: 100%; height: 100%; overflow: hidden; }
/* highlight-start */
/* Tagging */
.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>
<!-- Mode switching -->
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button class="btn btn-primary active js-Panorama">Panorama walkthrough</button>
<button class="btn btn-primary js-Floorplan">Space overview</button>
</div>
</div>
</nav>
<!-- highlight-start -->
<!-- Tagging -->
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary js-add-tag">Add tag</button>
</div>
<!-- highlight-end -->
<script type="module" src="./index"></script>
</body>
</html>About project2d
This chapter uses the `project2d` method. It maps a 3D coordinate to its position on the 2D screen.
five.project2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null
- Pass in a 3D coordinate to obtain a 2D screen coordinate, with the origin at the top-left and units in pixels. You can use it for things 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), the method returns
null. - The second parameter, testModel, controls whether model collision is taken into account, i.e. whether coordinates occluded by the model return
null.
Writing the logic
newTagis the tag in the "adding" state; it needs to follow the mouse position for placement.tagsholds the tags that have already been fixed in place; they follow the camera as it moves.tagToElementstores the DOM structure corresponding to each tag.- When the camera moves it fires the
cameraUpdateevent. In the callback, call$five.project2dfor each tag to obtain its screen canvas coordinate, then update the styles to render it.
Append the following after the mode switching code:
{ // === Tagging ===
const app = document.querySelector("#app");
const addButton = document.querySelector(".js-add-tag");
let newTag = null;
let tags = [];
const tagToElement = new WeakMap();
const createTagElement = tag => {
const div = document.createElement("div");
div.className = "tag";
div.style.display = "none";
div.innerHTML = `<div class="tag-pannel"><span class="tag-content">${tag.label}</span></div>`;
app.appendChild(div);
return div;
};
const renderTags = () => {
for (const tag of [newTag, ...tags]) {
if (!tag) continue;
if (!tag.position) continue;
const element = tagToElement.get(tag);
if (!element) continue;
const position = five.project2d(tag.position, true);
if (position === null) {
element.style.display = "none";
} else {
element.style.display = "";
element.style.left = position.x + "px";
element.style.top = position.y + "px";
}
}
};
addButton.addEventListener("click", () => {
newTag = { label: window.prompt("Add tag", "") || "Untitled" };
tagToElement.set(newTag, createTagElement(newTag));
}, false);
five.on("intersectionOnModelUpdate", intersect => {
if (newTag) newTag.position = intersect.point;
renderTags();
});
five.on("wantsTapGesture", () => {
if (newTag && newTag.position) {
tags.push(newTag);
newTag = null;
renderTags();
return false;
}
});
five.on("cameraUpdate", renderTags);
}{ // === Tagging ===
type Tag = { position?: THREE.Vector3, label: string };
const app = document.querySelector("#app")!;
const addButton = document.querySelector(".js-add-tag")!;
let newTag: Tag | null = null;
let tags: Tag[] = [];
const tagToElement = new WeakMap<Tag, HTMLElement>();
const createTagElement = (tag: Tag) => {
const div = document.createElement("div");
div.className = "tag";
div.style.display = "none";
div.innerHTML = `<div class="tag-pannel"><span class="tag-content">${tag.label}</span></div>`;
app.appendChild(div);
return div;
};
const renderTags = () => {
for (const tag of [newTag, ...tags]) {
if (!tag) continue;
if (!tag.position) continue;
const element = tagToElement.get(tag);
if (!element) continue;
const position = five.project2d(tag.position, true);
if (position === null) {
element.style.display = "none";
} else {
element.style.display = "";
element.style.left = position.x + "px";
element.style.top = position.y + "px";
}
}
};
addButton.addEventListener("click", () => {
newTag = { label: window.prompt("Add tag", "") || "Untitled" };
tagToElement.set(newTag, createTagElement(newTag));
}, false);
five.on("intersectionOnModelUpdate", intersect => {
if (newTag) newTag.position = intersect.point;
renderTags();
});
five.on("wantsTapGesture", () => {
if (newTag && newTag.position) {
tags.push(newTag);
newTag = null;
renderTags();
return false;
}
});
five.on("cameraUpdate", renderTags);
}Go back to your browser. You will see an "Add tag" button appear in the top-left corner of the page. Click it, fill in a tag name, then move the mouse and click at the spot where you want it. The tag is now placed.
A genuinely handy feature indeed 🥳.
