Skip to main content
Documentation

Adding Tags

Place interactive tags in a Realsee 3D space with Five SDK in vanilla JavaScript or TypeScript.

Recap of the previous chapter: Points in 3D

You learned about the Five SDK event system and built a small app that obtains the 3D position of a point through click events.

In this chapter you will learn

How to place tags in 3D space.

Getting ready

Create a new directory (src/5.tagging) together with the corresponding html file and a js or ts file. Carrying over the State code from the previous chapter would be too cumbersome, so we will build on top of the content from the Displaying 3D Space chapter.

<!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>
  <!-- Mode switching -->
  <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">Panorama walkthrough</button>
        <button class="btn btn-primary js-Floorplan">Space overview</button>
      </div>
    </div>
  </nav>
  <script type="module" src="./index"></script>
</body>
</html>
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);

{// === Mode switching ===
  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 {};

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 which ports are currently in use, so always rely on the value printed in the console. 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 strictly required; they just make the tags look nicer.

<!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 */
    /* Tagging */
    .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>
  <!-- Mode switching -->
  <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">Panorama walkthrough</button>
        <button class="btn btn-primary js-Floorplan">Space overview</button>
      </div>
    </div>
  </nav>
  <!-- highlight-start -->
  <!-- Tagging -->
  <div class="card position-fixed m-2 top-0 start-0">
    <button class="btn btn-primary js-add-tag">Add tag</button>
  </div>
  <!-- highlight-end -->
  <script type="module" src="./index"></script>
</body>
</html>

About project2d

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

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

  1. Pass in a 3D coordinate to obtain a 2D screen coordinate, with the origin at the top-left and units in pixels. You can use it for things like { 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), the method returns null.
  3. The second parameter, testModel, controls whether model collision is taken into account, i.e. whether coordinates occluded by the model return null.

Writing the logic

  1. newTag is the tag in the "adding" state; it needs to follow the mouse position for placement.
  2. tags holds the tags that have already been fixed in place; they follow the camera as it moves.
  3. tagToElement stores the DOM structure corresponding to each tag.
  4. When the camera moves it fires the cameraUpdate event. In the callback, call $five.project2d for each tag to obtain its screen canvas coordinate, then update the styles to render it.

Append the following after the mode switching code:

{ // === Tagging ===
  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("Add tag", "") || "Untitled" };
    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);
}

Go back to your browser. You will see an "Add tag" button appear in the top-left corner of the page. Click it, fill in a tag name, then move the mouse and click at the spot where you want it. The tag is now placed.

A genuinely handy feature indeed 🥳.