MultiDrawingViewports
Copying and pasting? We've got you covered! You can find the full source code of this tutorial here.
๐๏ธ Multiple Drawing Viewportsโ
A single viewport showing the full floor plan is rarely enough โ construction drawings typically combine a general view with one or more detail areas at different scales on the same sheet. Setting this up manually means managing multiple cameras, canvases, and coordinate systems. The viewport system handles all of that: each viewport is an independent window into the same drawing, with its own bounds and scale, rendered together into a paper-space canvas that only updates when annotation data changes. This tutorial covers creating two viewports on a single drawing framing different areas of the same floor plan, rendering them into a floating paper-space canvas, making viewport boundaries resizable and movable directly in the 3D view, and placing linear dimensions from the paper-space canvas without touching the 3D viewport. By the end, you'll have a multi-viewport sheet layout with interactive bounds and direct paper-space annotation.
๐ Importing our Librariesโ
First, let's install all necessary dependencies to make this example work:
import * as THREE from "three";
// @ts-ignore
import { TTFLoader } from "three/examples/jsm/loaders/TTFLoader.js";
import { Font } from "three/examples/jsm/loaders/FontLoader.js";
import Stats from "stats.js";
import * as BUI from "@thatopen/ui";
// You have to import * as OBC from "@thatopen/components"
import * as OBC from "../../../../index";
๐ Setting up the sceneโ
Nothing special here โ just a regular 3D scene setup. We'll keep the 3D canvas filling the entire viewport and float the paper-space panel over it in the bottom-left corner. That overlay approach keeps both views accessible at the same time without splitting the page.
document.body.style.cssText = "margin:0; width:100vw; height:100vh; overflow:hidden; position:relative;";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.SimpleCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.scene.three.background = null;
const container = document.getElementById("container")!;
container.style.cssText = "position:absolute; inset:0;";
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.SimpleCamera(components);
await world.camera.controls.setLookAt(48.213, 33.495, -5.062, 13.117, -1.205, 22.223);
components.init();
๐๏ธ Loading a BIM modelโ
A technical drawing is most useful when it has real geometry to annotate. Here we load the architectural model in Fragment format โ a worker-based geometry system that keeps the main thread free while processing large models. The model gives the drawing spatial context: the projection lines we'll add later are wall outlines extracted directly from it.
// `FragmentsManager.getWorker()` fetches the matching worker for this library version from unpkg and returns a blob URL.
// You can also pass your own URL to `fragments.init(...)` if you'd rather host the worker yourself.
const workerUrl = await OBC.FragmentsManager.getWorker();
const fragments = components.get(OBC.FragmentsManager);
fragments.init(workerUrl);
world.camera.controls.addEventListener("update", () => fragments.core.update());
fragments.list.onItemSet.add(({ value: model }) => {
model.useCamera(world.camera.three);
world.scene.three.add(model.object);
fragments.core.update(true);
});
fragments.core.models.materials.list.onItemSet.add(({ value: material }) => {
if (!("isLodMaterial" in material && material.isLodMaterial)) {
material.polygonOffset = true;
material.polygonOffsetUnits = 1;
material.polygonOffsetFactor = Math.random();
}
});
const arqFile = await fetch("https://thatopen.github.io/engine_components/resources/frags/school_arq.frag");
const arqBuffer = await arqFile.arrayBuffer();
await fragments.core.load(arqBuffer, { modelId: "school_arq" });
๐ Creating the drawing and loading projection linesโ
Here we create the drawing and load a set of pre-computed projection lines โ wall outlines already flattened to drawing space. We register them for picking, which enables efficient raycasting against individual segments. We'll use that later to let the user click projection lines and place dimensions on them. The lines live on layer 1 so the viewport cameras pick them up in the paper-space render automatically.
const techDrawings = components.get(OBC.TechnicalDrawings);
const drawing = techDrawings.create(world);
drawing.three.position.y = 11.427046;
const projData = await fetch("https://thatopen.github.io/engine_components/resources/projections/projection.json").then((r) =>
r.json(),
) as { positions: number[] };
const projGeo = new THREE.BufferGeometry();
projGeo.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array(projData.positions), 3),
);
drawing.layers.create("projection", { material: new THREE.LineBasicMaterial({ color: 0xff0000 }) });
const projLines = new THREE.LineSegments(projGeo);
drawing.addProjectionLines(projLines, "projection");