Recap of the previous chapter: Points in 3D Space
You learned about the Five SDK event system and built a small app that retrieves the 3D position of a point through a click event.
In this chapter you will learn
How to place tags in 3D space.
Getting Ready
Let's create a new directory (src/5.tagging) along with the corresponding html file and js or ts file. Carrying over the State code from the previous chapter would be too cumbersome, so we'll build on top of the content from the Displaying a 3D Space chapter.
<!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>Adding Tags</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>
* {
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 { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const listener = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
window.addEventListener("resize", listener, false);
onBeforeUnmount(() => {
window.removeEventListener("resize", listener, false);
});
return { width, height };
}
export { useWindowDimensions };<template>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary active'
: 'btn btn-primary'
"
@click="() => setState({ mode: Five.Mode.Panorama })"
>
Panorama Walkthrough
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
Space Overview
</button>
</div>
</div>
</nav>
</template>
<script setup>
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
</FiveProvider>
</template>
<script setup>
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");import { ref, onBeforeUnmount } from "vue";
function useWindowDimensions() {
const width = ref<number>(window.innerWidth);
const height = ref<number>(window.innerHeight);
const listener = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
window.addEventListener("resize", listener, false);
onBeforeUnmount(() => {
window.removeEventListener("resize", listener, false);
});
return { width, height };
}
export { useWindowDimensions };<template>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid justify-content-center">
<div class="btn-group">
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary active'
: 'btn btn-primary'
"
@click="() => setState({ mode: Five.Mode.Panorama })"
>
Panorama Walkthrough
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
Space Overview
</button>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
</FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/5.tagging/index.html".
Info
Check your console for the port number — it may change depending on your configuration and which ports are currently in use, so always rely on what the console prints. If you use a different build tool, start the server according to that tool's requirements.
Building the Tagging Feature
About useFiveProject2d
This chapter uses the `useFiveProject2d` method. It maps 3D coordinates onto the 2D screen.
useFiveProject2d(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 the unit in pixels. It can be used in forms such as
{ 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 testing is performed — that is, whether a coordinate occluded by the model returns
null.
Writing the TaggingController
- Add a TaggingController file to write the component.
- Store the tag positions and text in a
tagsVue Reactive. - Store the tag currently being created in a
newTagVue Ref. - Listen for Five SDK's
intersectionOnModelUpdateevent to place the tag being created at the mouse position. - Use the
project2dmethod produced by useFiveProject2d (inside thetagElementmethod) to get the screen canvas coordinates, then render the tag by updating its style. - Add tag styling (styling is not required; it just makes the tags look nicer).
<template>
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary" @click="addTag">Add Tag</button>
</div>
<div v-for="(tag, index) in tags" class="tag" :style="tagStyle(tag)">
<div class="tag-pannel">
<span class="tag-content">{{ tag.label }}</span>
</div>
</div>
<div class="tag" :style="newTagStyle(newTag)">
<div class="tag-pannel">
<span class="tag-content">{{ newTag?.label }}</span>
</div>
</div>
</template>
<script setup>
import { useFiveEventCallback, useFiveProject2d } from "@realsee/five/vue";
import { ref, reactive } from "vue";
import { Vector3 } from "three";
let newTag = ref(null);
let tags = reactive([]);
const project2d = useFiveProject2d();
const intersectPoint = ref(new Vector3(0, 0, 0));
useFiveEventCallback("intersectionOnModelUpdate", (intersect) => {
// Update the 3D point
if (newTag.value) {
intersectPoint.value = intersect.point;
newTag.value.position = intersect.point;
}
});
// Update the array on click
useFiveEventCallback("wantsTapGesture", () => {
if (newTag.value && newTag.value.position) {
tags.push(newTag.value);
newTag.value = null;
return false;
}
});
const addTag = () => {
if (!newTag.value) {
newTag.value = {
label: window.prompt("Add a tag", "") || "Untitled",
position: new Vector3(0, 0, 0),
};
}
};
const tagStyle = (tag) => {
return {
left: project2d(tag.position, false).value?.x + "px",
top: project2d(tag.position, false).value?.y + "px",
};
};
const newTagStyle = (tag) => {
if (tag) {
return {
left: project2d(tag.position, false).value?.x + "px",
top: project2d(tag.position, false).value?.y + "px",
display: "block",
};
}
return {
display: "none",
};
};
</script>
<style>
.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;
}
</style><template>
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary" @click="addTag">Add Tag</button>
</div>
<div v-for="(tag, index) in tags" class="tag" :style="tagStyle(tag)">
<div class="tag-pannel">
<span class="tag-content">{{ tag.label }}</span>
</div>
</div>
<div class="tag" :style="newTagStyle(newTag)">
<div class="tag-pannel">
<span class="tag-content">{{ newTag?.label }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { useFiveEventCallback, useFiveProject2d } from "@realsee/five/vue";
import { ref, Ref, reactive } from "vue";
import { Vector3 } from "three";
type Tag = {
position: THREE.Vector3;
label: string;
};
let newTag: Ref<Tag | null> = ref(null);
let tags: Tag[] = reactive([]);
const project2d = useFiveProject2d();
const intersectPoint = ref<Vector3>(new Vector3(0, 0, 0));
useFiveEventCallback("intersectionOnModelUpdate", (intersect) => {
// Update the 3D point
if (newTag.value) {
intersectPoint.value = intersect.point;
newTag.value.position = intersect.point;
}
});
// Update the array on click
useFiveEventCallback("wantsTapGesture", () => {
if (newTag.value && newTag.value.position) {
tags.push(newTag.value);
newTag.value = null;
return false;
}
});
const addTag = () => {
if (!newTag.value) {
newTag.value = {
label: window.prompt("Add a tag", "") || "Untitled",
position: new Vector3(0, 0, 0),
};
}
};
const tagStyle = (tag: Tag) => {
return {
left: project2d(tag.position, false).value?.x + "px",
top: project2d(tag.position, false).value?.y + "px",
};
};
const newTagStyle = (tag: Tag | null) => {
if (tag) {
return {
left: project2d(tag.position, false).value?.x + "px",
top: project2d(tag.position, false).value?.y + "px",
display: "block",
};
}
return {
display: "none",
};
};
</script>
<style>
.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;
}
</style>Using the Tagging Component
Insert it inside the FiveProvider in the App file.
<template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
// highlight-start
<TaggingController />
// highlight-end
</FiveProvider>
</template>
<script setup>
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
// highlight-start
import TaggingController from "./TaggingController.vue";
// highlight-end
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script><template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
// highlight-start
<TaggingController />
// highlight-end
</FiveProvider>
</template>
<script setup lang="ts">
import { FiveProvider, FiveCanvas } from "@realsee/five/vue";
import { parseWork } from "@realsee/five";
import { ref } from "vue";
import { useWindowDimensions } from "./useWindowDimensions";
import ModeController from "./ModeController.vue";
// highlight-start
import TaggingController from "./TaggingController.vue";
// highlight-end
const work = ref();
const workURL =
"https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
fetch(workURL)
.then((response) => response.text())
.then((text) => (work.value = parseWork(text)));
const { width, height } = useWindowDimensions();
</script>Switch back to your browser, and you'll see an "Add Tag" button appear in the top-left corner of the page. Click it, enter a tag name, then move the mouse, click at the position you want, and the tag is placed.
A genuinely useful feature, indeed 🥳.
