打标签
上一章回顾: 三维中的点
你了解 Five SDK 的事件系统,并且尝试开发了小应用,通过点击事件获取到点的三维位置。
本章你可以学习到
在三维空间中设置标签。
准备工作
我们新建一个目录(src/5.tagging
)以及对应的 html 文件 以及 js 或 ts 文件。
携带着上一章的 State 代码,太过于繁琐,那我们从 展示三维空间 章的内容的基础开发。
<!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>
<!-- 模式切换 -->
<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">全景漫游</button>
<button class="btn btn-primary js-Floorplan">空间总览</button>
</div>
</div>
</nav>
<script type="module" src="./index"></script>
</body>
</html>
- JavaScript
- TypeScript
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);
{// === 模式切换 ===
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);
{// === 模式切换 ===
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 {};
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/5.tagging/index.html"。
信息
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
开发打标签功能
添加标签样式
在 html 文件中添加标签样式。
样式并非是必须的,只是为了标签好看一些。
<!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 */
/* 打标签 */
.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>
<!-- 模式切换 -->
<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">全景漫游</button>
<button class="btn btn-primary js-Floorplan">空间总览</button>
</div>
</div>
</nav>
<!-- highlight-start -->
<!-- 打标签 -->
<div class="card position-fixed m-2 top-0 start-0">
<button class="btn btn-primary js-add-tag">打标签</button>
</div>
<!-- highlight-end -->
<script type="module" src="./index"></script>
</body>
</html>
project2d 说明
本章会使用到 project2d
方法。他可以将三维的坐标对应到而为屏幕。
five.project2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null
- 将一个三维坐标传入可以得到一个屏幕二维坐标, 原点在左上, 单位为像素。可以作为
{ left: returnValue.x + "px", top: returnValue.y + "px" }
等方式使用。 - 如果三维坐标无法计算到屏幕中(比如在背后或者被遮挡),则返回
null
。 - 第二个参数 testModel 为是否计算模型碰撞,即被模型遮挡的坐标是否为
null
。
编写逻辑代码
newTag
为新增状态下的标签,需要跟随鼠标位置预定。tags
已经固定的标签,跟随镜头移动。tagToElement
存储 tag 对应的 DOM 结构。- 相机移动触发
cameraUpdate
事件,在回调函数中,通过 标签调用$five.project2d
方法获取屏幕画布坐标,然后通过改变样式,渲染出来。
在 模式切换 的代码后追加:
- JavaScript
- TypeScript
{ // === 打标签 ===
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("添加标签", "") || "未命名" };
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);
}
{ // === 打标签 ===
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("添加标签", "") || "未命名" };
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);
}
回到你的浏览器查看,会发现你的页面左上角出现打标签按钮,点击,填写标签名称后,后移动鼠标,在你需要的位置点击。便可放置标签。
嗯,确实是一个实用的功能🥳 。