打标签
上一章回顾: 三维中的点
你了解 Five SDK 的事件系统,并且尝试开发了小应用,通过点击事件获取到点的三维位置。
本章你可以学习到
在三维空间中设置标签。
准备工作
我们新建一个目录(src/5.tagging
)以及对应的 html 文件 以及 js 或 ts 文件。
携带着上一章的 State 代码,太过于繁琐,那我们从 展示三维空间 章的内容的基础开发。
src/5.tagging/index.html
<!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>
<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>
- JavaScript
- TypeScript
src/5.tagging/useWindowDimensions.js
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 };
src/5.tagging/ModeController.vue
<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 })"
>
全景漫游
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
空间总览
</button>
</div>
</div>
</nav>
</template>
<script setup>
import { useFiveCurrentState } from "@realsee/five/vue";
import { Five } from "@realsee/five";
const [state, setState] = useFiveCurrentState();
</script>
src/5.tagging/App.vue
<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://vrlab-public.ljcdn.com/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>
src/5.tagging/index.js
import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
src/5.tagging/useWindowDimensions.ts
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 };
src/5.tagging/ModeController.vue
<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 })"
>
全景漫游
</button>
<button
:class="
state.mode == 'Panorama'
? 'btn btn-primary'
: 'btn btn-primary active'
"
@click="() => setState({ mode: Five.Mode.Floorplan })"
>
空间总览
</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>
src/5.tagging/App.vue
<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://vrlab-public.ljcdn.com/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>
src/5.tagging/index.ts
import { createApp, h } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/5.tagging/index.html"。
信息
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
开发打标签功能
useFiveProject2d 说明
本章会使用到 useFiveProject2d
方法。他可以将三维的坐标对应到二维屏幕。
useFiveProject2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null
- 将一个三维坐标传入可以得到一个屏幕二维坐标, 原点在左上, 单位为像素。可以作为
{ left: returnValue.x + "px", top: returnValue.y + "px" }
等方式使用。 - 如果三维坐标无法计算到屏幕中(比如在背后或者被遮挡),则返回
null
。 - 第二个参数 testModel 为是否计算模型碰撞,即被模型遮挡的坐标是否为
null
。
编写 TaggingController
- 添加一个 TaggingController 文件,用于编写组件。
- 通过
tags
Vue Reactive 存储标签位置和文字。 - 通过
newTag
Vue Ref 存储当前新建的标签。 - 监听 five 的
intersectionOnModelUpdate
事件,将新建中的标签放在在鼠标的位置。 - 通过 标签调用 useFiveProject2d 产生的
project2d
方法(tagElement
方法中)获取屏幕画布坐标,然后通过改变样式,渲染出来。 - 添加标签样式(样式并非必须,只是为了标签好看些)
- JavaScript
- TypeScript
src/5.tagging/TaggingController.vue
<template>
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary" @click="addTag">打标签</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) => {
// 更新三维点
if (newTag.value) {
intersectPoint.value = intersect.point;
newTag.value.position = intersect.point;
}
});
// 点击更新数组
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("添加标签", "") || "未命名",
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>
src/5.tagging/TaggingController.vue
<template>
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary" @click="addTag">打标签</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) => {
// 更新三维点
if (newTag.value) {
intersectPoint.value = intersect.point;
newTag.value.position = intersect.point;
}
});
// 点击更新数组
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("添加标签", "") || "未命名",
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>
使用打标签组件
插入到 App 文件的 FiveProvider 中
- JavaScript
- TypeScript
src/5.tagging/App.vue
<template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<TaggingController />
</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";
import TaggingController from "./TaggingController.vue";
const work = ref();
const workURL =
"https://vrlab-public.ljcdn.com/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>
src/5.tagging/App.vue
<template>
<FiveProvider :work="work">
<FiveCanvas :width="width" :height="height" />
<ModeController />
<TaggingController />
</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";
import TaggingController from "./TaggingController.vue";
const work = ref();
const workURL =
"https://vrlab-public.ljcdn.com/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>
回到你的浏览器查看,会发现你的页面左上角出现打标签按钮,点击,填写标签名称后,移动鼠标,在你需要的位置点击,然后放置标签即可。
嗯,确实是一个实用的功能 🥳 。