跳到主要内容

打标签

上一章回顾: 三维中的点
你了解 Five SDK 的事件系统,并且尝试开发了小应用,通过点击事件获取到点的三维位置。

本章你可以学习到

在三维空间中设置标签。

准备工作

我们新建一个目录(src/5.tagging)以及对应的 html 文件 以及 jsxtsx 文件。 携带着上一章的 State 代码,太过于繁琐,那我们从 展示三维空间 章的内容的基础开发。

<!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>
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://vr-public.realsee-cdn.cn/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 {};

启动服务 npm run dev。 并跳转到当前页面 "http://localhost:3000/src/5.tagging/index.html"。

信息

请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。

开发打标签功能

添加标签样式

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;
      }
      /* 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>
    <script type="module" src="./index"></script>
  </body>
</html>

useFiveProject2d 说明

本章会使用到 useFiveProject2d 方法。他可以将三维的坐标对应到二维屏幕。

useFiveProject2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null

  1. 将一个三维坐标传入可以得到一个屏幕二维坐标, 原点在左上, 单位为像素。可以作为 { left: returnValue.x + "px", top: returnValue.y + "px" } 等方式使用。
  2. 如果三维坐标无法计算到屏幕中(比如在背后或者被遮挡),则返回 null
  3. 第二个参数 testModel 为是否计算模型碰撞,即被模型遮挡的坐标是否为 null

编写 TaggingController

  1. 添加一个 TaggingController 文件,用于编写组件。
  2. 通过 tags React state 存储标签位置和文字。
  3. 通过 newTag React state 存储当前新建的标签。
  4. 监听 five 的 intersectionOnModelUpdate 事件,将新建中的标签放在在鼠标的位置。
  5. 通过 标签调用 useFiveProject2d 产生的 project2d 方法(tagElement 方法中)获取屏幕画布坐标,然后通过改变样式,渲染出来。
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 };

使用打标签组件

插入到 App 文件的 FiveProvider

import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { TaggingController } from "./TaggingController";
// highlight-end

/** work.json 的数据 URL */
const workURL =
  "https://vr-public.realsee-cdn.cn/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 />
        // highlight-start
        <TaggingController />
        // highlight-end
      </FiveProvider>
    )
  );
};

export { App };

回到你的浏览器查看,会发现你的页面左上角出现打标签按钮,点击,填写标签名称后,移动鼠标,在你需要的位置点击,然后放置标签即可。

嗯,确实是一个实用的功能 🥳 。