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.
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.
"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.
"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.