
Thatopen Impl Viewer
- 5 installs
- 17 repo stars
- Updated July 8, 2026
- openaec-foundation/thatopen-claude-skill-package
Helps with ai & agent building tasks.
About
thatopen-impl-viewer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- thatopen-impl-viewer
- AI & Agent Building
- AI-coding skill
Thatopen Impl Viewer by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 21, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/thatopen-claude-skill-package --skill thatopen-impl-viewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 17 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/thatopen-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
ThatOpen Viewer Setup
Overview
This skill covers the complete setup of a ThatOpen BIM viewer: creating a World (scene + camera + renderer), configuring the container element, starting the render loop, and proper cleanup. It includes both the basic SimpleRenderer path and the advanced PostproductionRenderer path with ambient occlusion and edge detection.
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x Prerequisite: thatopen-core-architecture (component system, lifecycle)
Setup Order
ALWAYS follow this exact order when creating a viewer:
1. new Components()
2. components.get(Worlds)
3. worlds.create<Scene, Camera, Renderer>()
4. world.scene = new SimpleScene(components)
5. world.scene.setup()
6. world.renderer = new SimpleRenderer(components, container)
— OR new PostproductionRenderer(components, container)
7. world.camera = new OrthoPerspectiveCamera(components)
8. components.get(Grids).create(world) [optional]
9. components.init() [REQUIRED]NEVER reorder these steps. The scene MUST be assigned before the renderer. The camera MUST be assigned last. components.init() MUST be the final call after all world setup is complete.
Container Element
The HTML container element is the DOM element where the WebGL canvas is rendered. It MUST satisfy these requirements:
- MUST be a block-level element (typically
<div>). - MUST have explicit width and height via CSS. Zero-size containers
produce a black or invisible viewport.
- MUST exist in the DOM before passing it to the renderer constructor.
- MUST NOT be
display: noneat construction time.
<div id="viewer" style="width: 100%; height: 100vh;"></div>const container = document.getElementById("viewer") as HTMLDivElement;
// ALWAYS verify the container exists before creating the renderer
if (!container) throw new Error("Viewer container not found");The renderer creates a <canvas> element inside the container and manages its lifecycle. NEVER manually create or manipulate this canvas.
World Creation
A World connects a Scene, Camera, and Renderer into a single viewport. Use the Worlds component to create worlds.
import * as OBC from "@thatopen/components";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
// Type parameters specify the concrete implementations
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();- ALWAYS use
components.get(OBC.Worlds)to access the Worlds manager.
NEVER instantiate Worlds directly.
- The type parameters are for TypeScript type narrowing only. You MUST
still assign the concrete instances manually (see next sections).
- Multiple worlds are supported. Each world gets its own scene, camera,
and renderer. All worlds update in the same components.init() loop.
Scene Setup
SimpleScene wraps a THREE.Scene and provides default lighting and background setup via the Configurable interface.
world.scene = new OBC.SimpleScene(components);
world.scene.setup(); // Creates default directional + ambient lightsSimpleSceneimplementsConfigurable. ALWAYS callsetup()after
assigning it to get default lights and background.
- Access the underlying Three.js scene:
world.scene.three. - To customize lighting, modify the Three.js scene after
setup():
world.scene.setup();
const scene = world.scene.three;
scene.background = new THREE.Color(0x202932);- To add custom Three.js objects:
const mesh = new THREE.Mesh(geometry, material);
world.scene.three.add(mesh);
world.meshes.add(mesh); // Track for raycastingRenderer: SimpleRenderer
SimpleRenderer wraps THREE.WebGLRenderer. It handles canvas creation, resizing, and per-frame rendering.
const container = document.getElementById("viewer") as HTMLDivElement;
world.renderer = new OBC.SimpleRenderer(components, container);- The renderer creates a WebGL canvas inside the container element.
- It implements
Resizeableand auto-resizes viaResizeObserver. - Access the underlying Three.js renderer:
world.renderer.three. - To customize WebGL parameters:
world.renderer = new OBC.SimpleRenderer(components, container, {
antialias: true,
alpha: true,
});Renderer: PostproductionRenderer
PostproductionRenderer extends the renderer with post-processing effects: ambient occlusion (AO), edge detection, outline rendering, and SMAA anti-aliasing. It is in @thatopen/components-front (browser-only).
import * as OBCF from "@thatopen/components-front";
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
world.renderer = new OBCF.PostproductionRenderer(components, container);Enabling Post-Processing
Post-processing is disabled by default. ALWAYS enable it explicitly:
world.renderer.postproduction.enabled = true;Post-Processing Features
Access features via world.renderer.postproduction:
| Feature | Property | Default | Effect |
|---|---|---|---|
| Ambient Occlusion | ao | enabled | Darkens crevices and edges |
| Edge Detection | customEdges | enabled | Draws outlines on geometry |
| SMAA | Part of pipeline | enabled | Anti-aliasing |
| Gloss Pass | glossEnabled | false | Reflective surface pass |
const pp = world.renderer.postproduction;
pp.enabled = true;
pp.ao = true; // ambient occlusion
pp.customEdges = true; // edge detection outlinesPostproductionRenderer + Grid Interaction
When using PostproductionRenderer, the grid MUST be excluded from post-processing to avoid visual artifacts:
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);ALWAYS exclude the grid mesh from post-processing. Failing to do so causes the grid to render with incorrect edges and AO artifacts.
Camera: OrthoPerspectiveCamera
OrthoPerspectiveCamera provides dual perspective/orthographic cameras with orbit, first-person, and plan navigation modes.
world.camera = new OBC.OrthoPerspectiveCamera(components);- ALWAYS assign the camera after the scene and renderer.
- It wraps
camera-controlsfor orbit/pan/zoom interaction. - Access the underlying cameras:
world.camera.threePersp—THREE.PerspectiveCameraworld.camera.threeOrtho—THREE.OrthographicCameraworld.camera.three— whichever is currently active
Navigation Modes
const camera = world.camera as OBC.OrthoPerspectiveCamera;
camera.set("Orbit"); // Default: orbit around target
camera.set("FirstPerson"); // WASD-style movement
camera.set("Plan"); // Orthographic top-down viewFraming Objects
// Frame all meshes in the world
world.camera.fit(world.meshes);
// Frame with offset (1.5 = 50% padding)
world.camera.fit(world.meshes, 1.5);Grid
Grids creates an infinite ground grid for spatial reference.
const grids = components.get(OBC.Grids);
const grid = grids.create(world);- ALWAYS create the grid BEFORE calling
components.init(). - The grid auto-fades based on camera distance.
- Access the underlying Three.js mesh:
grid.three.
Starting the Render Loop
components.init();- ALWAYS call
components.init()AFTER all world setup is complete. - This starts the
requestAnimationFrameloop. - All
Updateablecomponents (renderer, camera, etc.) receive
update(delta) calls each frame.
- NEVER call
init()before assigning scene, renderer, and camera.
The update loop will attempt to render an incomplete world.
- Calling
init()multiple times is safe but unnecessary.
Disposal
components.dispose();- ALWAYS call
components.dispose()when the viewer is no longer needed. - This disposes ALL components, worlds, renderers, scenes, and cameras.
FragmentsManageris disposed last to prevent dangling references.- After calling
dispose(), NEVER use any component or world references. - In frameworks (React, Vue, Angular), call dispose in the unmount/destroy
lifecycle hook.
Framework Integration Example (React)
useEffect(() => {
const components = new OBC.Components();
// ... setup world ...
components.init();
return () => {
components.dispose();
};
}, []);Bundler Configuration (Vite)
ThatOpen requires specific Vite configuration for WASM files, web workers, and cross-origin isolation headers (needed for SharedArrayBuffer).
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
{
name: "coop-coep",
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
},
},
],
optimizeDeps: {
exclude: ["web-ifc"],
},
worker: {
format: "es",
},
});COOP/COEP Headers
Cross-Origin-Opener-Policy: same-originand
Cross-Origin-Embedder-Policy: require-corp are REQUIRED for SharedArrayBuffer, which web-ifc uses internally.
- Without these headers, WASM operations may fail silently or throw.
- For production, configure these headers on your web server or CDN.
WASM Files
- web-ifc WASM files (
web-ifc.wasm,web-ifc-mt.wasm) MUST be
accessible at runtime.
IfcLoader.setup()with default settings auto-resolves WASM paths.- For custom paths, copy WASM files to your
public/directory and
configure IfcLoader accordingly.
Worker Files
- Fragment processing uses web workers for off-main-thread performance.
- ALWAYS initialize
FragmentsManagerwith a worker URL:
fragments.init(workerURL).
- Vite handles worker bundling via
worker: { format: "es" }.
Minimal Viewer (Copy-Paste Ready)
import * as OBC from "@thatopen/components";
// Container: must have width and height
const container = document.getElementById("viewer") as HTMLDivElement;
// 1. Components container
const components = new OBC.Components();
// 2. Create world
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
// 3. Scene with default lights
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
// 4. Renderer bound to container
world.renderer = new OBC.SimpleRenderer(components, container);
// 5. Camera with orbit controls
world.camera = new OBC.OrthoPerspectiveCamera(components);
// 6. Grid (optional)
components.get(OBC.Grids).create(world);
// 7. Start render loop
components.init();
// 8. Cleanup when done
window.addEventListener("beforeunload", () => {
components.dispose();
});PostproductionRenderer Viewer (Copy-Paste Ready)
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
const container = document.getElementById("viewer") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
// Enable post-processing
world.renderer.postproduction.enabled = true;
// Grid — exclude from post-processing
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
components.init();
window.addEventListener("beforeunload", () => {
components.dispose();
});Critical Rules
1. ALWAYS call components.init() after world setup. Without it, nothing renders. 2. ALWAYS call components.dispose() on cleanup. BIM models consume hundreds of MB of GPU memory. 3. ALWAYS call world.scene.setup() after assigning the scene. Without it, there are no lights. 4. ALWAYS exclude the grid from PostproductionRenderer. 5. ALWAYS ensure the container has non-zero dimensions before creating the renderer. 6. NEVER reorder the setup sequence: scene first, then renderer, then camera. 7. NEVER use component references after calling components.dispose(). 8. NEVER import @thatopen/components-front in Node.js environments. 9. NEVER create the renderer with a container that has display: none. 10. NEVER call components.init() before assigning all three world components (scene, renderer, camera).
Reference Files
- references/methods.md — World setup methods,
renderer options, camera configuration
- references/examples.md — Minimal viewer,
PostproductionRenderer, full application setup
- references/anti-patterns.md — Missing
init, wrong dispose order, WebGL context issues
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_componentsmain branch - npm:
@thatopen/components@3.3.3,@thatopen/components-front@3.3.3 - Research:
docs/research/vooronderzoek-thatopen.md - Skill:
thatopen-core-architecture(SKILL.md, references/)
Anti-Patterns — Viewer Setup
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x
Common mistakes when setting up a ThatOpen viewer and how to fix them.
---
AP-001: Missing components.init()
WRONG:
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
// Forgot components.init() — nothing rendersCORRECT:
// ... same setup ...
components.init(); // ALWAYS call this to start the render loopWhy: Without init(), the requestAnimationFrame loop never starts. The renderer never draws frames. The camera controls do not update. The viewport appears blank or frozen.
---
AP-002: Wrong Setup Order
WRONG:
// Camera before renderer — camera may not bind to the correct canvas
world.camera = new OBC.OrthoPerspectiveCamera(components);
world.renderer = new OBC.SimpleRenderer(components, container);
world.scene = new OBC.SimpleScene(components);CORRECT:
// ALWAYS: scene first, then renderer, then camera
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);Why: The camera needs the renderer's canvas for mouse/touch event binding. The renderer needs the scene to exist. ALWAYS follow the order: scene, renderer, camera.
---
AP-003: Zero-Size Container
WRONG:
<!-- No dimensions — renders as 0x0 -->
<div id="viewer"></div>const container = document.getElementById("viewer")!;
world.renderer = new OBC.SimpleRenderer(components, container);
// Canvas is 0x0 — viewport is invisibleCORRECT:
<div id="viewer" style="width: 100%; height: 100vh;"></div>Why: The WebGL canvas inherits its size from the container. A container with zero width or height produces an invisible or degenerate viewport. The renderer creates the canvas but cannot render to a zero-size buffer. ALWAYS ensure the container has explicit dimensions via CSS.
---
AP-004: Container Not in DOM
WRONG:
const container = document.createElement("div");
// Container not appended to DOM yet
world.renderer = new OBC.SimpleRenderer(components, container);
// ResizeObserver may not work, canvas not visibleCORRECT:
const container = document.createElement("div");
container.style.width = "100%";
container.style.height = "100vh";
document.body.appendChild(container); // FIRST add to DOM
world.renderer = new OBC.SimpleRenderer(components, container); // THEN create rendererWhy: The renderer uses ResizeObserver to track container dimensions. If the container is not in the DOM, the observer may not fire, and the canvas size is indeterminate.
---
AP-005: Missing scene.setup()
WRONG:
world.scene = new OBC.SimpleScene(components);
// Forgot setup() — no lights, no background
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.init();
// Scene renders completely black — no lights existCORRECT:
world.scene = new OBC.SimpleScene(components);
world.scene.setup(); // ALWAYS call setup() for default lights and backgroundWhy: SimpleScene implements Configurable. Without setup(), the scene has no lights and no background. All geometry renders as black silhouettes against a black background.
---
AP-006: Not Excluding Grid from PostproductionRenderer
WRONG:
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.renderer.postproduction.enabled = true;
const grid = components.get(OBC.Grids).create(world);
// Grid is included in post-processing — visual artifacts appearCORRECT:
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.renderer.postproduction.enabled = true;
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);Why: The post-processing pipeline applies ambient occlusion and edge detection to everything in the scene. The infinite grid produces incorrect depth values that create AO halos and false edges. ALWAYS exclude it.
---
AP-007: Missing Disposal on Component Unmount
WRONG (React):
function BIMViewer() {
useEffect(() => {
const components = new OBC.Components();
// ... setup ...
components.init();
// No cleanup function — memory leaks on every remount
}, []);
}CORRECT (React):
function BIMViewer() {
useEffect(() => {
const components = new OBC.Components();
// ... setup ...
components.init();
return () => {
components.dispose(); // ALWAYS dispose on unmount
};
}, []);
}Why: Each viewer creates WebGL contexts, GPU buffers, WASM memory, and event listeners. Without disposal, these accumulate on every React remount. After a few hot reloads, the browser runs out of WebGL contexts (limit is typically 8-16 per page) and crashes.
---
AP-008: Using Components After Disposal
WRONG:
components.dispose();
// NEVER use references after dispose
const worlds = components.get(OBC.Worlds); // Undefined behavior
world.camera.fit(world.meshes); // Dangling referencesCORRECT:
components.dispose();
// Null all references — do NOT use them after dispose
components = null;
world = null;Why: After dispose(), all internal state is cleaned up. GPU buffers are freed, event handlers are removed, and WASM memory is released. Accessing disposed objects leads to null reference errors and potential use-after-free bugs.
---
AP-009: Multiple WebGL Contexts
WRONG:
// Creating and destroying viewers in a loop without proper disposal
for (let i = 0; i < 20; i++) {
const components = new OBC.Components();
// ... setup world with renderer ...
components.init();
// No dispose — each iteration creates a new WebGL context
}
// Browser limit exceeded — "Too many active WebGL contexts"CORRECT:
let currentComponents: OBC.Components | null = null;
function createViewer(container: HTMLDivElement) {
// ALWAYS dispose previous viewer first
if (currentComponents) {
currentComponents.dispose();
}
currentComponents = new OBC.Components();
// ... setup ...
currentComponents.init();
}Why: Browsers limit the number of active WebGL contexts (typically 8-16). Each renderer creates one context. Without disposing old contexts, the limit is quickly reached and new renderers silently fail or force-lose older contexts.
---
AP-010: Importing PostproductionRenderer in Node.js
WRONG:
// In a Node.js script or SSR context
import * as OBCF from "@thatopen/components-front";
// Crashes: requires DOM, WebGL, PointerEvent, ResizeObserverCORRECT:
// Node.js: use core only
import * as OBC from "@thatopen/components";
// Browser only: use components-front
// Guard with environment check if in SSR/isomorphic code
if (typeof window !== "undefined") {
const OBCF = await import("@thatopen/components-front");
}Why: @thatopen/components-front depends on browser APIs (HTMLCanvasElement, WebGLRenderingContext, PointerEvent, ResizeObserver). Importing it in Node.js causes immediate crashes.
---
AP-011: Calling init() Before World Is Complete
WRONG:
const components = new OBC.Components();
components.init(); // Too early — no world exists yet
const worlds = components.get(OBC.Worlds);
const world = worlds.create();
world.scene = new OBC.SimpleScene(components);
// The update loop is already running on an incomplete worldCORRECT:
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.init(); // ALWAYS last, after all setup is doneWhy: init() starts the render loop immediately. If called before the world is fully set up, the loop tries to update/render incomplete worlds, potentially throwing errors or producing visual glitches.
---
AP-012: Hardcoding Container Element ID
WRONG:
// Fragile — breaks if HTML changes
const container = document.getElementById("viewer")!;
// The ! non-null assertion hides the error if element is missingCORRECT:
const container = document.getElementById("viewer");
if (!container) {
throw new Error("Viewer container element #viewer not found in DOM");
}
world.renderer = new OBC.SimpleRenderer(components, container);Why: Using the non-null assertion operator (!) on getElementById hides the case where the element does not exist. The renderer constructor then receives null, causing a cryptic error deep in Three.js. ALWAYS validate the container exists before use.
Examples — Viewer Setup
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x
---
1. Minimal Viewer (SimpleRenderer)
The absolute minimum code to get a working 3D viewport:
import * as OBC from "@thatopen/components";
const container = document.getElementById("viewer") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.get(OBC.Grids).create(world);
components.init();HTML requirement:
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; }
#viewer { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="viewer"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>---
2. PostproductionRenderer Viewer
Enhanced viewer with ambient occlusion and edge detection:
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
const container = document.getElementById("viewer") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
// Enable post-processing
world.renderer.postproduction.enabled = true;
// Grid — ALWAYS exclude from post-processing
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
components.init();---
3. Full Application Setup with IFC Loading
Complete viewer with model loading, highlighting, and cleanup:
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
const container = document.getElementById("viewer") as HTMLDivElement;
// 1. Components container
const components = new OBC.Components();
// 2. World with post-processing
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
// 3. Scene
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
// 4. Renderer
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.renderer.postproduction.enabled = true;
// 5. Camera
world.camera = new OBC.OrthoPerspectiveCamera(components);
// 6. Grid (excluded from post-processing)
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
// 7. Fragments manager with worker
const fragments = components.get(OBC.FragmentsManager);
fragments.init("/workers/fragments-worker.js");
// 8. IFC loader
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup();
// 9. Highlighter for selection
const highlighter = components.get(OBCF.Highlighter);
highlighter.setup({ world });
// 10. Start render loop
components.init();
// 11. Load a model
async function loadModel(url: string, name: string) {
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
const model = await ifcLoader.load(data, true, name);
// Frame model in view
world.camera.fit(world.meshes);
return model;
}
// 12. Cleanup
function dispose() {
components.dispose();
}
window.addEventListener("beforeunload", dispose);---
4. Custom Scene Background and Lighting
Override the default scene setup with custom values:
import * as OBC from "@thatopen/components";
import * as THREE from "three";
// ... world creation ...
world.scene = new OBC.SimpleScene(components);
world.scene.setup(); // Get default lights first
// Override background
world.scene.three.background = new THREE.Color(0x202932);
// Add custom lighting
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8);
hemiLight.position.set(0, 20, 0);
world.scene.three.add(hemiLight);
// Stronger directional light with shadows
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
world.scene.three.add(dirLight);---
5. Multiple Worlds (3D + Plan View)
Two viewports sharing the same Components instance:
import * as OBC from "@thatopen/components";
const container3D = document.getElementById("view-3d") as HTMLDivElement;
const containerPlan = document.getElementById("view-plan") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
// Main 3D viewport
const world3D = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world3D.scene = new OBC.SimpleScene(components);
world3D.scene.setup();
world3D.renderer = new OBC.SimpleRenderer(components, container3D);
world3D.camera = new OBC.OrthoPerspectiveCamera(components);
// Plan viewport (orthographic top-down)
const worldPlan = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
worldPlan.scene = new OBC.SimpleScene(components);
worldPlan.scene.setup();
worldPlan.renderer = new OBC.SimpleRenderer(components, containerPlan);
worldPlan.camera = new OBC.OrthoPerspectiveCamera(components);
// Switch plan camera to top-down orthographic
const planCamera = worldPlan.camera as OBC.OrthoPerspectiveCamera;
planCamera.set("Plan");
// Add grids to both
const grids = components.get(OBC.Grids);
grids.create(world3D);
grids.create(worldPlan);
// Single init() starts both worlds
components.init();---
6. React Integration
import { useEffect, useRef } from "react";
import * as OBC from "@thatopen/components";
function BIMViewer() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.get(OBC.Grids).create(world);
components.init();
// Cleanup on unmount — ALWAYS dispose
return () => {
components.dispose();
};
}, []);
return <div ref={containerRef} style={{ width: "100%", height: "100vh" }} />;
}---
7. Vite Project Setup
package.json
{
"name": "thatopen-viewer",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@thatopen/components": "^3.3.3",
"@thatopen/components-front": "^3.3.3",
"@thatopen/fragments": "^3.3.6",
"three": ">=0.175.0",
"web-ifc": ">=0.0.74"
},
"devDependencies": {
"typescript": "^5.4.0",
"vite": "^5.4.0"
}
}vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
{
name: "coop-coep-headers",
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
},
},
],
optimizeDeps: {
exclude: ["web-ifc"],
},
worker: {
format: "es",
},
});---
8. Accessing the WebGL Context
For advanced rendering needs (custom shaders, reading pixels):
const gl = world.renderer.three.getContext();
// Enable screenshot capture
world.renderer = new OBC.SimpleRenderer(components, container, {
preserveDrawingBuffer: true,
});
// Take screenshot
function takeScreenshot(): string {
world.renderer.three.render(world.scene.three, world.camera.three);
return world.renderer.three.domElement.toDataURL("image/png");
}---
9. Renderer Event Hooks
Hook into the render loop for custom rendering:
// Run code before each frame render
world.renderer.onBeforeUpdate.add(() => {
// Update custom animations, helpers, etc.
});
// Run code after each frame render
world.renderer.onAfterUpdate.add(() => {
// Post-render overlay, HUD, etc.
});
// React to viewport resize
world.renderer.onResize.add((newSize: THREE.Vector2) => {
console.log(`Viewport resized to: ${newSize.x}x${newSize.y}`);
});API Methods — Viewer Setup
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x
---
Worlds Manager
class Worlds extends Component implements Updateable, Disposable {
static readonly uuid: string;
enabled: boolean;
list: DataMap<string, World>;
/**
* Creates a new World. Type parameters are for TypeScript narrowing only.
* You MUST still assign scene, camera, renderer manually.
*/
create<TScene, TCamera, TRenderer>(): SimpleWorld;
/** Disposes and removes a world from the list. */
delete(world: World): void;
/** Called by Components animation loop. Updates all worlds. */
update(delta?: number): void;
dispose(): void;
onDisposed: Event<void>;
}SimpleWorld
class SimpleWorld implements World, Disposable {
uuid: string;
scene: BaseScene;
camera: BaseCamera;
renderer: BaseRenderer;
/** Set of meshes tracked for raycasting. */
meshes: Set<THREE.Mesh>;
onDisposed: Event<void>;
dispose(): void;
}SimpleScene
class SimpleScene extends BaseScene implements Configurable {
/** The underlying Three.js scene. */
three: THREE.Scene;
/**
* Creates default lighting and background.
* Adds a DirectionalLight and an AmbientLight to the scene.
* Sets a default background color.
*/
setup(config?: Partial<SimpleSceneConfig>): void;
config: SimpleSceneConfig;
isSetup: boolean;
onSetup: Event<SimpleScene>;
dispose(): void;
onDisposed: Event<void>;
}SimpleSceneConfig
interface SimpleSceneConfig {
directionalLight: {
color: THREE.Color;
intensity: number;
position: THREE.Vector3;
};
ambientLight: {
color: THREE.Color;
intensity: number;
};
}SimpleRenderer
class SimpleRenderer extends BaseRenderer
implements Disposable, Resizeable, Updateable {
/** The underlying Three.js WebGLRenderer. */
three: THREE.WebGLRenderer;
/**
* @param components - Components container
* @param container - DOM element to render into
* @param parameters - Optional THREE.WebGLRendererParameters
*/
constructor(
components: Components,
container: HTMLElement,
parameters?: Partial<THREE.WebGLRendererParameters>
);
/** Resize to given size or auto-detect from container. */
resize(size?: THREE.Vector2): void;
/** Get current renderer size. */
getSize(): THREE.Vector2;
/** Render one frame. Called by animation loop. */
update(): void;
dispose(): void;
onResize: Event<THREE.Vector2>;
onDisposed: Event<void>;
onBeforeUpdate: Event<any>;
onAfterUpdate: Event<any>;
}WebGLRendererParameters (subset)
interface WebGLRendererParameters {
antialias?: boolean; // Default: false. Enable MSAA.
alpha?: boolean; // Default: false. Transparent background.
powerPreference?: "high-performance" | "low-power" | "default";
preserveDrawingBuffer?: boolean; // Needed for screenshots.
}PostproductionRenderer
Package: @thatopen/components-front (browser-only)
class PostproductionRenderer extends RendererWith2D
implements Disposable, Resizeable, Updateable {
/** The underlying Three.js WebGLRenderer. */
three: THREE.WebGLRenderer;
/**
* @param components - Components container
* @param container - DOM element to render into
* @param parameters - Optional WebGL parameters
*/
constructor(
components: Components,
container: HTMLElement,
parameters?: Partial<THREE.WebGLRendererParameters>
);
/** Post-processing controller. */
postproduction: Postproduction;
resize(size?: THREE.Vector2): void;
getSize(): THREE.Vector2;
update(): void;
dispose(): void;
onResize: Event<THREE.Vector2>;
onDisposed: Event<void>;
onBeforeUpdate: Event<any>;
onAfterUpdate: Event<any>;
}Postproduction
class Postproduction {
/** Enable/disable all post-processing. Default: false. */
enabled: boolean;
/** Enable/disable ambient occlusion. */
ao: boolean;
/** Enable/disable custom edge outlines. */
customEdges: boolean;
/** Enable/disable gloss pass. */
glossEnabled: boolean;
/** Set of Three.js objects to exclude from post-processing. */
exclude: Set<THREE.Object3D>;
}OrthoPerspectiveCamera
class OrthoPerspectiveCamera extends BaseCamera
implements CameraControllable, Updateable, Disposable, Configurable {
/** Currently active Three.js camera (perspective or orthographic). */
three: THREE.PerspectiveCamera | THREE.OrthographicCamera;
/** The perspective camera instance. */
threePersp: THREE.PerspectiveCamera;
/** The orthographic camera instance. */
threeOrtho: THREE.OrthographicCamera;
/** camera-controls instance for orbit/pan/zoom. */
controls: CameraControls;
/** Projection manager for switching perspective/ortho. */
projection: ProjectionManager;
/**
* Switch navigation mode.
* @param mode - "Orbit" | "FirstPerson" | "Plan"
*/
set(mode: NavigationMode): void;
/**
* Frame objects in view.
* @param meshes - Meshes to frame (defaults to all world meshes)
* @param offset - Zoom offset factor (1.0 = tight fit)
*/
fit(meshes?: Iterable<THREE.Mesh>, offset?: number): void;
/** Register a custom navigation mode. */
addCustomNavigationMode(mode: NavigationMode): void;
setup(config?: any): void;
update(delta?: number): void;
dispose(): void;
onDisposed: Event<void>;
}NavigationMode
type NavigationMode = "Orbit" | "FirstPerson" | "Plan" | string;- Orbit: Default. Rotate around a target point with mouse/touch.
- FirstPerson: WASD-style movement with mouse look.
- Plan: Orthographic top-down view. Switches to ortho projection.
Grids
class Grids extends Component implements Disposable {
static readonly uuid: string;
enabled: boolean;
/** Create a grid for the given world. */
create(world: World): SimpleGrid;
dispose(): void;
onDisposed: Event<void>;
}SimpleGrid
class SimpleGrid implements Disposable, Hideable {
/** The underlying Three.js grid mesh. */
three: THREE.Mesh;
visible: boolean;
dispose(): void;
onDisposed: Event<void>;
}Components Lifecycle Methods (Relevant)
class Components {
/**
* Start the requestAnimationFrame loop.
* Calls update(delta) on all enabled Updateable components each frame.
*/
init(): void;
/**
* Stop the loop and dispose ALL registered components.
* FragmentsManager is disposed last.
*/
dispose(): void;
onInit: Event<undefined>;
onDisposed: Event<void>;
}