Skip to main content
Documentation

Tagging

Place interactive tags in a Realsee 3D space with Five SDK in React class components and HOCs.

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 from a click event.

In this chapter you will learn

How to place tags in a 3D space.

Getting ready

Let's create a new directory (src/5.tagging) along with its html file and a jsx or tsx 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 instead.

<!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>
  <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 React, { Component } from "react";
import { parseWork } from "@realsee/five";

/**
 * React HOC that fetches the work
 * @param url the URL of work.json
 */
function withFetchWork(url) {
  return function(Compnent) {
    return class extends Component {
      state = { work: null };
      componentDidMount() {
        fetch(url).then(res => res.json()).then(json => {
          this.setState({ work: parseWork(json) });
        });
      }
      render() {
        if (this.state.work === null) return null;
        return <Compnent work={this.state.work} {...this.props}/>;
      }
    }
  }
}

export { withFetchWork };
import React, { Component } from "react";

/**
 * React HOC: get the current window dimensions
 */
function withWindowDimensions() {
  return function(Compnent) {
    return class extends Component {
      state = this.getWindowDimensions();
      resizeListener = () => {
        this.setState(this.getWindowDimensions());
      };
      getWindowDimensions() {
        return { width: window.innerWidth, height: window.innerHeight };
      }
      componentDidMount() {
        window.addEventListener("resize", this.resizeListener, false);
      }
      componentWillUnmount() {
        window.removeEventListener("resize", this.resizeListener, false);
      }
      render() {
        const dimensions = { width: this.state.width, height: this.state.height };
        return <Compnent windowDimensions={dimensions} {...this.props}/>;
      }
    }
  }
}

export { withWindowDimensions };
import React, { Component } from "react";
import { Five } from "@realsee/five";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
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";

const FEATURES = createFiveFeature("currentState", "setState");

/**
 * React Component: mode control
 */
const ModeController = compose(
  withFive(FEATURES)
)(class extends Component {
  render() {
    return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
      <BottomNavigation
        showLabels
        value={this.props.$five.currentState.mode}
        onChange={(_, newValue) => {
          this.props.$five.setState({ mode: newValue });
        }}
      >
        <BottomNavigationAction label="Panorama" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
        <BottomNavigationAction label="Floorplan" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
      </BottomNavigation>
    </Paper>;
  }
})

export { ModeController };
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";

/** Data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>;
    </FiveProvider>;
  }
});

export { App };
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App/>, document.querySelector("#app"));

export {};

Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/5.tagging/index.html".

Info

Please check your console: the port number may change depending on your configuration and on which ports are currently in use, so always rely on the console output. If you use a different build tool, start the server according to that tool's requirements.

Building the tagging feature

Adding tag styles

Add the tag styles to the html file.

The styles are not required; they just make the tags look a little nicer.

<!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>
  <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>

About useFiveProject2d

This chapter uses the `useFiveProject2d` method. It maps a 3D coordinate to a position on the 2D screen.

$five.project2d(vector: THREE.Vector3, testModel: boolean): THREE.Vector2 | null

  1. Pass in a 3D coordinate to get a 2D screen coordinate. The origin is at the top-left, and the unit is pixels. You can use it directly as, for example, { left: returnValue.x + "px", top: returnValue.y + "px" }.
  2. If the 3D coordinate cannot be projected onto the screen (for example, it is behind the camera or occluded), it returns null.
  3. The second parameter, testModel, controls whether model collision is taken into account, i.e. whether a coordinate occluded by the model returns null.

Writing the TaggingController

  1. Add a TaggingController file to hold the component.
  2. Store tag positions and text in the tags React state.
  3. Store the tag currently being created in the newTag React state.
  4. Listen to Five SDK's intersectionOnModelUpdate event to keep the in-progress tag positioned under the mouse.
  5. For each tag, call the $five.project2d method (inside the tagElement method) to get the screen canvas coordinate, then render it by updating the styles.
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { withFive, createFiveFeature } from "@realsee/five/react";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";

const FEATURES = createFiveFeature("project2d", "currentState", "on", "off");

/**
 * React Component: tagging
 */
const TaggingController = compose(
  withFive(FEATURES)
)(class extends Component {

  state = { tags: [], newTag: null };

  addTag = () => {
    this.setState({ newTag: { label: window.prompt("Add tag", "") || "Untitled" } });
  }

  tagElement(tag, key) {
    const position = tag.position && this.props.$five.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>
  }

  onIntersectionUpdate = intersect => {
    if (this.state.newTag) this.setState({ newTag: { position: intersect.point, label: this.state.newTag.label } });
  };

  onTapGesture = () => {
    if (this.state.newTag && this.state.newTag.position) {
      this.setState({
        tags: this.state.tags.concat(this.state.newTag),
        newTag: null
      });
      return false;
    }
  };

  componentDidMount() {
    this.props.$five.on("intersectionOnModelUpdate", this.onIntersectionUpdate);
    this.props.$five.on("wantsTapGesture", this.onTapGesture);
  }

  componentWillUnmount() {
    this.props.$five.off("intersectionOnModelUpdate", this.onIntersectionUpdate);
    this.props.$five.off("wantsTapGesture", this.onTapGesture);
  }

  render() {
    return <React.Fragment>
      <Paper sx={{ position: "fixed", top: 10, left: 10 }}>
        <Button onClick={this.addTag}>Add tag</Button>
      </Paper>
        {this.state.newTag && this.tagElement(this.state.newTag)}
        {this.state.tags.map((tag, index) => this.tagElement(tag, index))}
    </React.Fragment>;
  }
});

export { TaggingController };

Using the tagging component

Insert it into the FiveProvider in the App file.

import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { TaggingController } from "./TaggingController";
// highlight-end

/** Data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>;
      // highlight-start
      <TaggingController/>;
      // highlight-end
    </FiveProvider>;
  }
});

export { App };

Go back to your browser and take a look. 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 and click at the spot you want. The tag is placed there.

Yes, this really is a handy feature 🥳.