
Thatopen Agents Viewer Builder
- 5 installs
- 17 repo stars
- Updated July 8, 2026
- openaec-foundation/thatopen-claude-skill-package
Helps with ai & agent building tasks.
About
thatopen-agents-viewer-builder is a Claude Code skill in the AI & Agent Building category.
- thatopen-agents-viewer-builder
- AI & Agent Building
- AI-coding skill
Thatopen Agents Viewer Builder 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 26, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/thatopen-claude-skill-package --skill thatopen-agents-viewer-builderAdd 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 Builder Agent
Purpose
This is an agent skill that provides step-by-step instructions for scaffolding a complete ThatOpen BIM viewer application from scratch. It guides code generation, not just documentation. When a user asks to create a new BIM viewer, follow these steps exactly.
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x Prerequisites: Node.js 18+, npm
When to Use This Skill
- User asks to "create a BIM viewer"
- User asks to "scaffold a ThatOpen app"
- User asks to "set up a new IFC viewer project"
- User needs a complete working application from zero
Step-by-Step Build Instructions
Step 1: Initialize Vite Project
Create a new directory and initialize with Vite + TypeScript:
npm create vite@latest <project-name> -- --template vanilla-ts
cd <project-name>ALWAYS use the vanilla-ts template. NEVER use React/Vue templates unless the user explicitly requests a framework.
Step 2: Install Dependencies
Install ALL required packages with exact compatible versions:
npm install @thatopen/components@^3.3.3 \
@thatopen/components-front@^3.3.3 \
@thatopen/fragments@^3.3.6 \
@thatopen/ui@^3.3.3 \
@thatopen/ui-obc@^3.3.3 \
three@^0.175.0 \
web-ifc@^0.0.77
npm install -D typescript@^5.4.0 vite@^5.4.0ALWAYS install @thatopen/fragments explicitly even though it is a dependency of @thatopen/components — peer dependency resolution varies across npm versions.
ALWAYS install three and web-ifc explicitly — they are peer dependencies, not bundled.
Step 3: Create Vite Configuration
Create vite.config.ts with COOP/COEP headers and WASM handling:
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",
},
});COOP/COEP headers are REQUIRED. Without them, SharedArrayBuffer is unavailable and web-ifc WASM operations fail silently or throw.
`optimizeDeps.exclude: ["web-ifc"]` is REQUIRED. Vite's pre-bundling breaks WASM module loading.
`worker.format: "es"` is REQUIRED for fragment worker compatibility.
Step 4: Create HTML Entry Point
Replace index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BIM Viewer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
</style>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>NEVER add a static <div id="viewer"> — the bim-grid and bim-viewport custom elements handle layout. The body MUST have full width/height.
Step 5: Create TypeScript Viewer Code
Replace src/main.ts with the complete viewer setup. The code follows this exact order:
1. Initialize UI Manager — BUI.Manager.init() 2. Create Components — new OBC.Components() 3. Create World — scene, renderer, camera (in that order) 4. Enable post-processing — postproduction renderer 5. Create grid — exclude from post-processing 6. Initialize FragmentsManager — worker URL 7. Setup IfcLoader — WASM configuration 8. Setup Highlighter — click-to-select 9. Start render loop — components.init() 10. Build UI — toolbar, panel, grid layout 11. Wire events — file input, buttons
See references/examples.md for the complete copy-pasteable implementation.
Step 6: Delete Boilerplate Files
Remove Vite template files that are not needed:
rm -f src/counter.ts src/style.css src/typescript.svg public/vite.svgStep 7: Run and Verify
npm run devVerify in the browser:
- 3D viewport with grid is visible
- Orbit controls work (click + drag)
- IFC file can be loaded via file input button
- Clicking an element highlights it in yellow
- Properties panel shows element data
Setup Order Rules
ALWAYS follow this exact initialization order:
BUI.Manager.init()
|
v
new Components()
|
v
worlds.create() -> world.scene -> world.renderer -> world.camera
|
v
Grids.create(world) + exclude from postproduction
|
v
FragmentsManager.init(workerURL)
|
v
IfcLoader.setup()
|
v
Highlighter.setup({ world })
|
v
components.init()
|
v
Build UI and append to DOMNEVER call components.init() before all components are configured. NEVER call ifcLoader.load() before ifcLoader.setup() completes. NEVER use bim-* elements before BUI.Manager.init(). NEVER assign camera before scene and renderer.
Package Version Matrix
| Package | Version | Purpose |
|---|---|---|
@thatopen/components | ^3.3.3 | Core engine, Components, Worlds, IfcLoader |
@thatopen/components-front | ^3.3.3 | PostproductionRenderer, Highlighter |
@thatopen/fragments | ^3.3.6 | Fragment binary format, workers |
@thatopen/ui | ^3.3.3 | UI web components (bim-panel, bim-toolbar) |
@thatopen/ui-obc | ^3.3.3 | Pre-wired BIM UI components |
three | ^0.175.0 | 3D rendering engine |
web-ifc | ^0.0.77 | IFC WASM parser |
typescript | ^5.4.0 | TypeScript compiler |
vite | ^5.4.0 | Build tool and dev server |
Highlighter Configuration
The Highlighter provides click-to-select functionality:
import * as OBCF from "@thatopen/components-front";
const highlighter = components.get(OBCF.Highlighter);
highlighter.setup({ world });setup({ world })is REQUIRED — it binds to the world's renderer and
camera for raycasting.
- Default selection color is yellow (#BCF124).
highlighter.selectioncontains the current selection as a
{ [styleName]: ModelIdMap } object.
- Use
highlighter.clear()to deselect all. - Set
highlighter.zoomToSelection = trueto auto-frame selected elements.
File Input Pattern for IFC Loading
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".ifc";
fileInput.addEventListener("change", async () => {
const file = fileInput.files?.[0];
if (!file) return;
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
const model = await ifcLoader.load(data, true, file.name);
world.camera.fit(world.meshes);
});ALWAYS convert File to Uint8Array before passing to ifcLoader.load(). ALWAYS call world.camera.fit(world.meshes) after loading to frame the model in view.
UI Layout Pattern
Use bim-grid with named layouts for the application shell:
const grid = document.createElement("bim-grid");
grid.layouts = {
main: {
template: `
"toolbar toolbar" auto
"panel viewport" 1fr
/ 320px 1fr
`,
elements: { toolbar, panel, viewport },
},
};
grid.layout = "main";
document.body.appendChild(grid);ALWAYS define grid.layouts before setting grid.layout. ALWAYS append the grid to document.body as the root element.
Disposal Pattern
window.addEventListener("beforeunload", () => {
components.dispose();
});ALWAYS register disposal on beforeunload. For frameworks, use the component lifecycle (React useEffect cleanup, Vue onUnmounted, Angular ngOnDestroy).
Critical Rules
1. ALWAYS install all packages from the version matrix — missing peer dependencies cause silent failures. 2. ALWAYS include COOP/COEP headers in Vite config — WASM needs them. 3. ALWAYS exclude web-ifc from optimizeDeps — pre-bundling breaks WASM. 4. ALWAYS call BUI.Manager.init() before creating any bim-* elements. 5. ALWAYS follow the setup order: scene -> renderer -> camera. 6. ALWAYS call components.init() after all setup is complete. 7. ALWAYS dispose components on teardown. 8. NEVER use @thatopen/components-front in Node.js environments. 9. NEVER skip ifcLoader.setup() — WASM initialization will fail. 10. NEVER pass raw File or ArrayBuffer to ifcLoader.load() — it requires Uint8Array.
Reference Files
- references/methods.md — Setup checklist,
package versions, configuration reference
- references/examples.md — Complete working
application: HTML + TypeScript + Vite config
- references/anti-patterns.md — Common
scaffolding mistakes and how to avoid them
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 - Skills:
thatopen-impl-viewer,thatopen-syntax-ifc-loading,
thatopen-syntax-ui
Viewer Builder — Common Scaffolding Mistakes
AP-001: Missing COOP/COEP Headers
Symptom: WASM operations fail silently, SharedArrayBuffer is not defined errors in console, or IFC loading hangs indefinitely.
Cause: The Vite dev server does not set cross-origin isolation headers by default. Without them, the browser disables SharedArrayBuffer.
Fix: ALWAYS include the COOP/COEP plugin in vite.config.ts:
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();
});
},
},
],For production, configure these headers on your web server.
---
AP-002: web-ifc Not Excluded from optimizeDeps
Symptom: Cannot find module 'web-ifc' or WASM file 404 errors during development. The WASM module fails to load after Vite pre-bundles it.
Cause: Vite's dependency pre-bundling transforms the web-ifc module in a way that breaks its WASM loader.
Fix: ALWAYS exclude web-ifc:
optimizeDeps: {
exclude: ["web-ifc"],
},---
AP-003: Missing BUI.Manager.init()
Symptom: bim-toolbar, bim-panel, bim-grid and other custom elements render as empty boxes with no styling or behavior.
Cause: ThatOpen UI components are Lit-based web components that must be registered before use.
Fix: ALWAYS call BUI.Manager.init() before creating any bim-* element:
import * as BUI from "@thatopen/ui";
BUI.Manager.init(); // MUST be first---
AP-004: Wrong Setup Order (Camera Before Renderer)
Symptom: Camera controls do not work, black viewport, or TypeScript errors about undefined properties.
Cause: The camera needs the renderer's DOM element for event binding. Assigning camera before renderer leaves it unbound.
Fix: ALWAYS follow: scene -> renderer -> camera:
// CORRECT
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
// WRONG — camera before renderer
world.camera = new OBC.OrthoPerspectiveCamera(components);
world.renderer = new OBCF.PostproductionRenderer(components, container);---
AP-005: Missing scene.setup() Call
Symptom: The viewport renders but the scene is completely dark. No lights, no visible geometry even after loading a model.
Cause: SimpleScene implements Configurable. Without setup(), no default directional or ambient lights are created.
Fix: ALWAYS call setup() immediately after assigning the scene:
world.scene = new OBC.SimpleScene(components);
world.scene.setup(); // Creates default lights---
AP-006: Grid Not Excluded from PostproductionRenderer
Symptom: The grid renders with dark AO halos, thick outlines, and visual artifacts. It looks broken.
Cause: PostproductionRenderer applies ambient occlusion and edge detection to ALL objects in the scene, including the grid.
Fix: ALWAYS exclude the grid:
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);---
AP-007: Calling components.init() Too Early
Symptom: Render loop starts but nothing is visible. Console may show errors about null references in the update loop.
Cause: components.init() starts the requestAnimationFrame loop. If scene, renderer, or camera are not yet assigned, the loop tries to render an incomplete world.
Fix: ALWAYS call init() AFTER all world components are configured:
// All setup done
world.scene = ...
world.renderer = ...
world.camera = ...
// Grid, IfcLoader, Highlighter configured
components.init(); // LAST setup call---
AP-008: Passing File Object to ifcLoader.load()
Symptom: TypeError or silent failure when loading an IFC file from a file input.
Cause: ifcLoader.load() requires Uint8Array, not File, Blob, or ArrayBuffer.
Fix: ALWAYS convert to Uint8Array:
// WRONG
const model = await ifcLoader.load(file, true, "model");
// WRONG
const buffer = await file.arrayBuffer();
const model = await ifcLoader.load(buffer, true, "model");
// CORRECT
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
const model = await ifcLoader.load(data, true, "model");---
AP-009: Skipping ifcLoader.setup()
Symptom: ifcLoader.load() throws an error about WASM not being initialized, or returns undefined.
Cause: IfcLoader implements Configurable. The setup() method initializes the web-ifc WASM engine. Without it, there is no IFC parser.
Fix: ALWAYS await setup() before any load() call:
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup(); // REQUIRED — initializes WASM
// Now load() is safe to call---
AP-010: Missing Peer Dependencies
Symptom: Import errors at runtime, missing module warnings during build, or type errors in the editor.
Cause: @thatopen/components lists three, web-ifc, and @thatopen/fragments as peer dependencies. npm does not always install peers automatically.
Fix: ALWAYS install ALL packages explicitly:
npm install @thatopen/components@^3.3.3 \
@thatopen/components-front@^3.3.3 \
@thatopen/fragments@^3.3.6 \
@thatopen/ui@^3.3.3 \
@thatopen/ui-obc@^3.3.3 \
three@^0.175.0 \
web-ifc@^0.0.77NEVER rely on transitive peer dependency installation.
---
AP-011: Zero-Size Container
Symptom: The WebGL canvas exists in the DOM but is invisible. No 3D content renders. No errors in console.
Cause: The container element has zero width or height. The renderer creates a canvas matching the container dimensions.
Fix: ALWAYS ensure the container has explicit non-zero dimensions:
/* For bim-viewport in a bim-grid layout, the grid handles sizing */
/* For a raw div, set explicit dimensions: */
#viewer { width: 100vw; height: 100vh; }NEVER use display: none on the container at creation time.
---
AP-012: Missing Disposal
Symptom: Memory usage grows on page navigation. Browser tab crashes after loading several models. GPU memory is not released.
Cause: BIM models consume hundreds of MB of GPU memory. Without disposal, WebGL contexts, textures, and geometry buffers leak.
Fix: ALWAYS dispose on teardown:
window.addEventListener("beforeunload", () => {
components.dispose();
});For SPA frameworks, use lifecycle hooks:
- React:
useEffectcleanup function - Vue:
onUnmounted - Angular:
ngOnDestroy
---
AP-013: Using worker.format Other Than "es"
Symptom: Fragment workers fail to load. Console shows syntax errors or module loading failures in worker context.
Cause: Fragment workers use ES module imports. Without worker: { format: "es" }, Vite may bundle workers as IIFE or CJS.
Fix: ALWAYS set worker format in vite.config.ts:
worker: {
format: "es",
},---
AP-014: Hardcoding WASM Version in CDN Path
Symptom: IFC loading fails after updating web-ifc package. WASM file returns 404 from CDN. Or WASM version mismatch causes parse errors.
Cause: The CDN URL contains a hardcoded version that no longer matches the installed web-ifc npm package.
Fix: Either use autoSetWasm: true (default) or derive the version dynamically:
// BEST: use automatic resolution
await ifcLoader.setup(); // autoSetWasm: true
// OR: if manual path is needed, don't hardcode
import { version } from "web-ifc/package.json";
await ifcLoader.setup({
autoSetWasm: false,
wasm: { path: `https://unpkg.com/web-ifc@${version}/`, absolute: true },
});Viewer Builder — Complete Working Application
This file contains a complete, copy-pasteable ThatOpen BIM viewer application. All files below form a working Vite + TypeScript project.
---
File: package.json
{
"name": "thatopen-bim-viewer",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@thatopen/components": "^3.3.3",
"@thatopen/components-front": "^3.3.3",
"@thatopen/fragments": "^3.3.6",
"@thatopen/ui": "^3.3.3",
"@thatopen/ui-obc": "^3.3.3",
"three": "^0.175.0",
"web-ifc": "^0.0.77"
},
"devDependencies": {
"typescript": "^5.4.0",
"vite": "^5.4.0"
}
}---
File: 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",
},
});---
File: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BIM Viewer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: sans-serif;
}
</style>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>---
File: src/main.ts
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
import * as BUI from "@thatopen/ui";
// ============================================================
// 1. INITIALIZE UI
// ============================================================
BUI.Manager.init();
// ============================================================
// 2. CREATE COMPONENTS CONTAINER
// ============================================================
const components = new OBC.Components();
// ============================================================
// 3. CREATE WORLD (scene, renderer, camera)
// ============================================================
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
// Viewport element — the renderer binds to this
const viewport = document.createElement("bim-viewport");
// Scene with default lights
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
// Renderer with post-processing
world.renderer = new OBCF.PostproductionRenderer(components, viewport);
world.renderer.postproduction.enabled = true;
// Camera with orbit controls
world.camera = new OBC.OrthoPerspectiveCamera(components);
// ============================================================
// 4. GRID (excluded from post-processing)
// ============================================================
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
// ============================================================
// 5. IFC LOADER SETUP
// ============================================================
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup(); // autoSetWasm: true by default
// ============================================================
// 6. HIGHLIGHTER (click-to-select)
// ============================================================
const highlighter = components.get(OBCF.Highlighter);
highlighter.setup({ world });
// ============================================================
// 7. START RENDER LOOP
// ============================================================
components.init();
// ============================================================
// 8. BUILD TOOLBAR
// ============================================================
// --- Load IFC button ---
const loadIfcBtn = document.createElement("bim-button");
loadIfcBtn.label = "Load IFC";
loadIfcBtn.icon = "mdi:file-upload";
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".ifc";
fileInput.style.display = "none";
document.body.appendChild(fileInput);
loadIfcBtn.addEventListener("click", () => {
fileInput.click();
});
fileInput.addEventListener("change", async () => {
const file = fileInput.files?.[0];
if (!file) return;
loadIfcBtn.label = "Loading...";
loadIfcBtn.disabled = true;
try {
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
await ifcLoader.load(data, true, file.name);
world.camera.fit(world.meshes);
} catch (error) {
console.error("IFC loading failed:", error);
} finally {
loadIfcBtn.label = "Load IFC";
loadIfcBtn.disabled = false;
fileInput.value = ""; // allow re-selecting same file
}
});
// --- Fit view button ---
const fitBtn = document.createElement("bim-button");
fitBtn.label = "Fit View";
fitBtn.icon = "mdi:fit-to-screen";
fitBtn.addEventListener("click", () => {
world.camera.fit(world.meshes);
});
// --- Clear selection button ---
const clearBtn = document.createElement("bim-button");
clearBtn.label = "Clear Selection";
clearBtn.icon = "mdi:select-off";
clearBtn.addEventListener("click", () => {
highlighter.clear();
});
// --- Toolbar assembly ---
const toolbarSection = document.createElement("bim-toolbar-section");
toolbarSection.label = "Tools";
toolbarSection.appendChild(loadIfcBtn);
toolbarSection.appendChild(fitBtn);
toolbarSection.appendChild(clearBtn);
const toolbar = document.createElement("bim-toolbar");
toolbar.appendChild(toolbarSection);
// ============================================================
// 9. BUILD PANEL
// ============================================================
const panel = document.createElement("bim-panel");
panel.name = "info";
panel.label = "Model Info";
panel.icon = "mdi:information";
const infoSection = document.createElement("bim-panel-section");
infoSection.label = "Instructions";
infoSection.fixed = true;
const infoLabel = document.createElement("bim-label");
infoLabel.textContent = "Click 'Load IFC' to open an IFC file. Click elements in the viewport to select them.";
infoSection.appendChild(infoLabel);
const selectionSection = document.createElement("bim-panel-section");
selectionSection.label = "Selection";
const selectionLabel = document.createElement("bim-label");
selectionLabel.textContent = "No element selected";
selectionSection.appendChild(selectionLabel);
// Update selection info when highlight changes
highlighter.events.select.onHighlight.add((data) => {
const modelIds = Object.values(data);
let totalItems = 0;
for (const idMap of modelIds) {
for (const [, ids] of idMap) {
totalItems += ids.size;
}
}
selectionLabel.textContent = `${totalItems} element(s) selected`;
});
highlighter.events.select.onClear.add(() => {
selectionLabel.textContent = "No element selected";
});
panel.appendChild(infoSection);
panel.appendChild(selectionSection);
// ============================================================
// 10. ASSEMBLE LAYOUT
// ============================================================
const grid2 = document.createElement("bim-grid");
grid2.layouts = {
main: {
template: `
"toolbar toolbar" auto
"panel viewport" 1fr
/ 320px 1fr
`,
elements: {
toolbar,
panel,
viewport,
},
},
};
grid2.layout = "main";
document.body.appendChild(grid2);
// ============================================================
// 11. DISPOSAL
// ============================================================
window.addEventListener("beforeunload", () => {
components.dispose();
});---
File: tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
},
"include": ["src"]
}---
Quick Start Commands
# 1. Create project
npm create vite@latest my-bim-viewer -- --template vanilla-ts
cd my-bim-viewer
# 2. Install dependencies
npm install @thatopen/components@^3.3.3 @thatopen/components-front@^3.3.3 \
@thatopen/fragments@^3.3.6 @thatopen/ui@^3.3.3 @thatopen/ui-obc@^3.3.3 \
three@^0.175.0 web-ifc@^0.0.77
# 3. Replace files with the above content
# (vite.config.ts, index.html, src/main.ts, tsconfig.json)
# 4. Clean up boilerplate
rm -f src/counter.ts src/style.css src/typescript.svg public/vite.svg
# 5. Run
npm run dev---
Minimal Viewer (Without UI Components)
If the user only needs a basic viewer without the @thatopen/ui panel and toolbar, use this stripped-down version:
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
// Container — must have explicit dimensions
const container = document.getElementById("viewer") as HTMLDivElement;
if (!container) throw new Error("Container #viewer not found");
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.renderer.postproduction.enabled = true;
world.camera = new OBC.OrthoPerspectiveCamera(components);
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup();
const highlighter = components.get(OBCF.Highlighter);
highlighter.setup({ world });
components.init();
// Load an IFC file
async function loadIfc(url: string) {
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
await ifcLoader.load(data, true, "model");
world.camera.fit(world.meshes);
}
window.addEventListener("beforeunload", () => components.dispose());HTML for the minimal version:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>Viewer Builder — Setup Checklist & Configuration Reference
Complete Setup Checklist
Use this checklist when scaffolding a new ThatOpen BIM viewer. Every item is REQUIRED unless marked optional.
Project Initialization
- [ ]
npm create vite@latest <name> -- --template vanilla-ts - [ ] Install runtime dependencies (see Package Versions below)
- [ ] Install dev dependencies:
typescript@^5.4.0,vite@^5.4.0 - [ ] Create
vite.config.tswith COOP/COEP plugin, optimizeDeps, worker format - [ ] Replace
index.htmlwith full-viewport template - [ ] Replace
src/main.tswith viewer code - [ ] Delete boilerplate:
counter.ts,style.css, SVG files
Code Initialization Order
- [ ]
BUI.Manager.init()— register UI custom elements - [ ]
new OBC.Components()— create component container - [ ]
components.get(OBC.Worlds).create()— create world - [ ]
world.scene = new OBC.SimpleScene(components)— assign scene - [ ]
world.scene.setup()— initialize default lights - [ ]
world.renderer = new OBCF.PostproductionRenderer(components, container)— assign renderer - [ ]
world.renderer.postproduction.enabled = true— enable post-processing - [ ]
world.camera = new OBC.OrthoPerspectiveCamera(components)— assign camera - [ ]
components.get(OBC.Grids).create(world)— create grid - [ ] Exclude grid from postproduction —
world.renderer.postproduction.exclude.add(grid.three) - [ ]
components.get(OBC.FragmentsManager).init(workerURL)— (optional, for worker-based loading) - [ ]
components.get(OBC.IfcLoader).setup()— configure WASM - [ ]
components.get(OBCF.Highlighter).setup({ world })— enable selection - [ ]
components.init()— start render loop - [ ] Build UI (toolbar, panel, grid layout)
- [ ] Append
bim-gridtodocument.body - [ ] Register disposal on
beforeunload
Verification
- [ ]
npm run dev— starts without errors - [ ] 3D viewport with grid visible in browser
- [ ] Orbit controls respond to mouse interaction
- [ ] IFC file loads via file input
- [ ] Clicking an element highlights it
- [ ] No console errors related to WASM or SharedArrayBuffer
---
Package Versions
| Package | Install Version | Peer Requirements |
|---|---|---|
@thatopen/components | ^3.3.3 | fragments ~3.3.0, three >=0.175, web-ifc >=0.0.74 |
@thatopen/components-front | ^3.3.3 | fragments ~3.3.0, three >=0.175, web-ifc >=0.0.74 |
@thatopen/fragments | ^3.3.6 | three >=0.175, web-ifc >=0.0.74 |
@thatopen/ui | ^3.3.3 | standalone |
@thatopen/ui-obc | ^3.3.3 | components ~3.3.0, components-front ~3.3.0, fragments ~3.3.0, three >=0.175 |
three | ^0.175.0 | standalone |
web-ifc | ^0.0.77 | standalone (WASM) |
typescript | ^5.4.0 | dev only |
vite | ^5.4.0 | dev only |
---
Vite Configuration Reference
COOP/COEP Headers Plugin
{
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();
});
},
}Purpose: Enable SharedArrayBuffer for web-ifc WASM multithreading. Without these headers, the browser disables SharedArrayBuffer and WASM operations fail.
For production builds, configure these headers on your web server (nginx, Apache, Cloudflare, etc.) — the Vite plugin only works in dev mode.
optimizeDeps Configuration
optimizeDeps: {
exclude: ["web-ifc"],
}Purpose: Prevent Vite from pre-bundling web-ifc. The WASM module loader breaks when Vite transforms it.
Worker Configuration
worker: {
format: "es",
}Purpose: Fragment processing workers use ES module format. Without this, worker imports fail in development.
---
Component Configuration Reference
PostproductionRenderer
| Property | Type | Default | Description |
|---|---|---|---|
postproduction.enabled | boolean | false | Master toggle for post-processing |
postproduction.ao | boolean | true | Ambient occlusion |
postproduction.customEdges | boolean | true | Edge detection outlines |
postproduction.exclude | Set<THREE.Object3D> | empty | Objects excluded from post-processing |
Highlighter
| Property | Type | Default | Description |
|---|---|---|---|
multiple | `"none" \ | "shiftKey" \ | "ctrlKey"` |
zoomToSelection | boolean | false | Auto-frame selected elements |
selection | { [name]: ModelIdMap } | {} | Current selection state |
styles | DataMap<string, MaterialDefinition> | — | Style definitions |
Setup config:
| Config Key | Type | Default | Description |
|---|---|---|---|
world | World | REQUIRED | World to bind to |
selectName | string | "select" | Name for selection style |
selectionColor | THREE.Color | #BCF124 | Selection highlight color |
autoHighlightOnClick | boolean | true | Auto-highlight on click |
selectEnabled | boolean | true | Enable selection |
IfcLoader
| Config Key | Type | Default | Description |
|---|---|---|---|
autoSetWasm | boolean | true | Auto-resolve WASM path |
wasm.path | string | — | Directory with WASM files |
wasm.absolute | boolean | — | Is path absolute URL? |
OrthoPerspectiveCamera
| Method | Parameters | Description |
|---|---|---|
set(mode) | `"Orbit" \ | "FirstPerson" \ |
fit(meshes, offset?) | meshes: Iterable<THREE.Mesh>, offset: number | Frame objects in view |
---
TypeScript Configuration
The Vite vanilla-ts template provides a working tsconfig.json. Ensure these compiler options are set:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}skipLibCheck: true is recommended because some @thatopen type definitions reference internal Three.js types that may not resolve cleanly.
---
Production Build Notes
For vite build:
1. WASM files from web-ifc MUST be accessible at runtime. Either:
- Copy them to
public/directory, or - Use CDN path via
ifcLoader.setup({ autoSetWasm: false, wasm: { ... } })
2. COOP/COEP headers MUST be configured on the production web server. The Vite dev plugin does NOT affect production builds.
3. Fragment workers are bundled by Vite automatically with worker: { format: "es" }.