Docs

Let people point, move and look

Read where the pointer is on Earth, let a person drag an object, and move the camera from your own buttons.

A view of a twin is something people work in. The renderer reports what they do with the pointer and lets your code move the camera; it changes nothing in the world by itself.

view-controls.ts
import { createScene } from "@klorad/api/world";
import { createThreeRenderer } from "@klorad/engine-three";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "kiosk", name: "Kiosk", position: { east: 0, north: 0 }, geometry: { kind: "box", width: 6, depth: 4, height: 3 } });

const readout = document.getElementById("readout")!;
const renderer = createThreeRenderer({
  container: document.getElementById("view")!,
  scene,
  // A click selects. A click on empty ground also says where it landed, here to put a pin there.
  onPick: (id, hit) => {
    renderer.select(id);
    if (!id && hit.geo) scene.add({ name: "Pin", position: hit.geo, geometry: { kind: "marker" } });
  },
  // Where the pointer is, on Earth.
  onPointer: (hit) => (readout.textContent = hit?.geo ? `${hit.geo.lat.toFixed(5)}, ${hit.geo.lon.toFixed(5)}` : ""),
  // The renderer moves nothing. A drag becomes a move because this line updates the scene.
  onDrag: ({ objectId, position }) => scene.update(objectId, { position }),
});

// The camera, in the words of a map.
document.getElementById("north")!.onclick = () => renderer.setView({ heading: 0 });
document.getElementById("above")!.onclick = () => renderer.setView({ tilt: 89.5, heading: 0 });
document.getElementById("all")!.onclick = () => renderer.frame();
document.getElementById("kiosk")!.onclick = () => renderer.frame(["kiosk"]);

Where the pointer is

onPointer fires on every move with a PointerHit: the object under the pointer, and where the pointer meets the ground, both in the scene's frame (local) and on Earth (geo). It fires with null when the pointer leaves the view. onPick receives the same hit with a click, so a click on empty ground can place something there.

Dragging

onDrag reports a drag of the selected object: select one with select(id), and a press on it starts a drag instead of moving the camera. Each report carries where the object would stand. The renderer does not move it: call scene.update and the move becomes one event of the scene, seen by every other view and every hook.

To restrict what may be dragged, decide in onDrag: ignore the report and nothing moves.

The camera

setView takes any part of a CameraView: target, heading, tilt, distance. The camera eases there, turning the short way round. frame() fits everything, frame([id]) one object. onViewChange reports every movement, which is enough to draw a compass, and metresPerPixel() gives the scale at the target for a scale bar.

In React

ThreeView takes the same callbacks as props and gives the camera through a ref.

site-view.tsx
"use client";

import { useRef, useState } from "react";
import type { CameraView } from "@klorad/engine-three";
import { ThreeView, type ThreeViewHandle } from "@klorad/engine-three/react";
import { useScene } from "@klorad/react";

/** A view with a north button, a bearing readout and objects a person can drag. */
export function SiteView() {
  const scene = useScene();
  const view = useRef<ThreeViewHandle>(null);
  const [selected, setSelected] = useState<string | null>(null);
  const [camera, setCamera] = useState<CameraView | null>(null);
  return (
    <>
      <ThreeView
        ref={view}
        selected={selected}
        onPick={setSelected}
        onDrag={({ objectId, position }) => scene.update(objectId, { position })}
        onViewChange={setCamera}
      />
      <button onClick={() => view.current?.setView({ heading: 0 })}>North up</button>
      <output>{camera ? `${Math.round(camera.heading)}°` : ""}</output>
    </>
  );
}

Inside a page that scrolls

A view that fills its page may take every gesture. A view between the paragraphs of a page may not: a reader scrolling past would be caught zooming the scene instead. cooperativeGestures shares the gestures the way web maps do. The wheel scrolls the page unless Ctrl or ⌘ is held, and on a touch screen a vertical swipe scrolls the page. Dragging with a mouse, a sideways swipe and a pinch still move the view. Say so beside the view, since the renderer draws no hint of its own.

inline-view.tsx
"use client";

import { ThreeView } from "@klorad/engine-three/react";

/** A view between the paragraphs of a page: the page keeps the wheel and the vertical swipe. */
export function InlineView() {
  return (
    <figure>
      <div style={{ height: 360 }}>
        <ThreeView cooperativeGestures />
      </div>
      <figcaption>Drag to look around. Hold Ctrl or ⌘ and scroll to zoom.</figcaption>
    </figure>
  );
}

The twin and the tour on the home of these docs are set this way.

Run it liveSandbox: Scene ObjectsEach object says whether it is only modelled, mirrors a sensor, or also commands a device.

Something wrong or unclear? Tell us.