打标签
上一章回顾: 三维中的点
你了解 Five SDK 的事件系统,并且尝试开发了小应用,通过点击事件获取到点的三维位置。
本章你可以学习到
在三维空间中设置标签。
准备工作
我们新建一个目录(src/5.tagging
)以及对应的 html 文件 以及 jsx 或 tsx 文件。
携带着上一章的 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>
<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/useFetchWork.js
import { useState, useEffect } from "react";
import { parseWork } from "@realsee/five";
/**
* React Hook: 通过 work.json 的地址 获取 work 对象
* @param url work.json 的数据地址
* @returns work 对象,如果获取中,返回 null
*/
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 };
src/5.tagging/useWindowDimensions.js
import { useState, useEffect } from "react";
/**
* 获取当前窗口的尺寸
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: 获取当前窗口的尺寸
*/
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 };
src/5.tagging/ModeController.jsx
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: 模态控制
*/
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="全景漫游"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="空间总览"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
};
export { ModeController };
src/5.tagging/App.jsx
import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
/** work.json 的数据 URL */
const workURL =
"https://vrlab-public.ljcdn.com/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 };
src/5.tagging/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};
src/5.tagging/useFetchWork.ts
import { useState, useEffect } from "react";
import { Work, parseWork } from "@realsee/five";
/**
* React Hook: 通过 work.json 的地址 获取 work 对象
* @param url work.json 的数据地址
* @returns work 对象,如果获取中,返回 null
*/
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 };
src/5.tagging/useWindowDimensions.ts
import { useState, useEffect } from "react";
/**
* 获取当前窗口的尺寸
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: 获取当前窗口的尺寸
*/
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 };
src/5.tagging/ModeController.tsx
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: 模态控制
*/
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="全景漫游"
icon={<DirectionsWalkIcon />}
value={Five.Mode.Panorama}
/>
<BottomNavigationAction
label="空间总览"
icon={<ViewInArIcon />}
value={Five.Mode.Floorplan}
/>
</BottomNavigation>
</Paper>
);
};
export { ModeController };
src/5.tagging/App.tsx
import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
/** work.json 的数据 URL */
const workURL =
"https://vrlab-public.ljcdn.com/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 };
src/5.tagging/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App />, document.querySelector("#app"));
export {};
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/5.tagging/index.html"。
信息
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
开发打标签功能
添加标签样式
在 html 文件中添加标签样式。
样式并非是必须的,只是为了标签好看一些。
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>
<style>
* {
margin: 0;
padding: 0;
}
html,
body #app {
width: 100%;
height: 100%;
overflow: hidden;
}
.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>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index"></script>
</body>
</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
React state 存储标签位置和文字。 - 通过
newTag
React state 存储当前新建的标签。 - 监听 five 的
intersectionOnModelUpdate
事件,将新建中的标签放在在鼠标的位置。 - 通过 标签调用 useFiveProject2d 产生的
project2d
方法(tagElement
方法中)获取屏幕画布坐标,然后通过改变样式,渲染出来。
- JavaScript
- TypeScript
src/5.tagging/TaggingController.jsx
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: 打标签
*/
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("添加标签", "") || "未命名" });
}, []);
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}>打标签</Button>
</Paper>
{newTag && tagElement(newTag)}
{tags.map((tag, index) => tagElement(tag, index))}
</React.Fragment>
);
};
export { TaggingController };
src/5.tagging/TaggingController.tsx
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: 打标签
*/
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("添加标签", "") || "未命名" });
}, []);
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}>打标签</Button>
</Paper>
{newTag && tagElement(newTag)}
{tags.map((tag, index) => tagElement(tag, index))}
</React.Fragment>
);
};
export { TaggingController };
使用打标签组件
插入到 App 文件的 FiveProvider 中
- JavaScript
- TypeScript
src/5.tagging/App.jsx
import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
import { TaggingController } from "./TaggingController";
/** work.json 的数据 URL */
const workURL =
"https://vrlab-public.ljcdn.com/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 />
<TaggingController />
</FiveProvider>
)
);
};
export { App };
src/5.tagging/App.tsx
import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
import { TaggingController } from "./TaggingController";
/** work.json 的数据 URL */
const workURL =
"https://vrlab-public.ljcdn.com/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 />
<TaggingController />
</FiveProvider>
)
);
};
export { App };
回到你的浏览器查看,会发现你的页面左上角出现打标签按钮,点击,填写标签名称后,移动鼠标,在你需要的位置点击,然后放置标签即可。
嗯,确实是一个实用的功能 🥳 。