| name | crosstech-impl-bim-web-viewer |
| description | Use when building an end-to-end BIM web viewer: from IFC file to interactive browser application with navigation, selection, property inspection, and clipping. Prevents the common mistake of building a custom viewer when @thatopen/components provides production-ready functionality out of the box. Covers the full pipeline: IFC parsing, Three.js rendering, UI interaction, section planes, annotations, and performance optimization for large models. Keywords: BIM viewer, web viewer, IFC viewer, @thatopen/components, Three.js, section plane, property panel, model navigation, web-ifc, BIM web application, view IFC in browser, 3D building viewer, online BIM viewer.
|
| license | MIT |
| compatibility | Designed for Claude Code. Covers @thatopen/components 3.x, Three.js r160+, web-ifc 0.0.77. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
crosstech-impl-bim-web-viewer
Technology Boundary
| Aspect | Side A: BIM/IFC Data | Side B: Web Browser |
|---|
| Format | IFC STEP file (.ifc) — IFC4 / IFC4X3 | WebGL rendering via Three.js |
| Coordinates | Z-up (IFC standard) | Y-up (Three.js / WebGL) |
| Data model | EXPRESS schema, entity graph | JavaScript objects, typed arrays |
| Units | Millimeters or meters (per IfcProject) | Three.js scene units (unitless) |
| Size limit | No limit (files reach 500MB–1GB) | ~2GB WASM memory, ~4GB tab total |
The Bridge: @thatopen/components (v3.x) — converts IFC to Fragments format via web-ifc WASM, then renders through Three.js. Handles coordinate transformation, geometry tessellation, and entity-to-mesh mapping automatically.
Directionality: IFC → Web browser (one-way rendering + interactive inspection). Writing IFC from the browser is NOT covered by this skill.
Decision Tree: Which Approach?
Need a BIM web viewer?
├── Using standard BIM features (navigate, select, clip, inspect)?
│ └── YES → Use @thatopen/components (Option A)
├── Integrating into existing Three.js app with custom rendering?
│ └── YES → Use custom web-ifc + Three.js pipeline (Option B)
├── Need server-side IFC processing (validation, authoring)?
│ └── YES → Use IfcOpenShell on backend + web-ifc on frontend (Hybrid)
└── File size > 200MB?
└── YES → ALWAYS pre-convert to Fragments on server (see Large Model Handling)
Option A: @thatopen/components Viewer (Recommended)
Installation
npm install @thatopen/components @thatopen/components-front @thatopen/fragments three web-ifc
CRITICAL: The Three.js version MUST match the version pinned by @thatopen/components. Run npm ls three and verify only ONE version is installed. Mismatched versions cause silent rendering failures.
Complete Minimal Viewer
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
import * as THREE from "three";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<OBC.SimpleScene, OBC.SimpleCamera, OBC.SimpleRenderer>();
const container = document.getElementById("viewer")!;
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.SimpleCamera(components);
world.scene = new OBC.SimpleScene(components);
ifcLoader = components.(.);
ifcLoader.({
: ,
: { : , : },
});
fragments = components.(.);
fragments...( {
model.(world..);
world...(model.);
fragments..();
});
highlighter = components.(.);
highlighter. = ;
highlighter. = ;
hider = components.(.);
response = ();
data = ( response.());
ifcLoader.(data, , );
Key Components Reference
| Component | Package | Purpose |
|---|
OBC.IfcLoader | components | IFC → Fragments conversion and loading |
OBC.FragmentsManager | components | Model lifecycle, Fragment caching, disposal |
OBC.Hider | components | Hide, show, isolate elements by ID or category |
OBCF.Highlighter | components-front | Selection highlighting via raycasting |
OBCF.Clipper | components-front | Section planes through models |
Selection and Property Inspection
const highlighter = components.get(OBCF.Highlighter);
await highlighter.highlight("select");
const idMap: OBC.ModelIdMap = {};
idMap[model.modelId] = new Set([101, 102, 103]);
await highlighter.highlightByID("select", idMap);
await highlighter.clear("select");
Hide / Isolate Elements
const hider = components.get(OBC.Hider);
const wallIds = model.getItemsOfCategories(["IFCWALL"]);
const wallMap: OBC.ModelIdMap = {};
wallMap[model.modelId] = wallIds;
await hider.isolate(wallMap);
await hider.set(true);
Z-Fighting Prevention
ALWAYS apply polygon offset to Fragment materials to prevent z-fighting on coplanar BIM faces:
fragments.core.models.materials.list.onItemSet.add(({ value: material }) => {
material.polygonOffset = true;
material.polygonOffsetUnits = 1;
material.polygonOffsetFactor = Math.random();
});
Option B: Custom web-ifc + Three.js Pipeline
Use this ONLY when integrating BIM data into an existing Three.js application where @thatopen/components cannot be adopted.
Core Pipeline
import * as THREE from "three";
import { IfcAPI } from "web-ifc";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const ifcApi = new IfcAPI();
ifcApi.SetWasmPath("/static/wasm/");
await ifcApi.Init();
const ifcData = new Uint8Array(await fetch("model.ifc").then(r => r.arrayBuffer()));
const modelID = ifcApi.OpenModel(ifcData, { COORDINATE_TO_ORIGIN: true });
const expressIdToMesh = new Map<number, THREE.Mesh>();
ifcApi.StreamAllMeshes(modelID, (flatMesh) => {
const geometries = flatMesh.geometries;
( i = ; i < geometries.(); i++) {
pg = geometries.(i);
geom = ifcApi.(modelID, pg.);
verts = ifcApi.(geom.(), geom.());
idx = ifcApi.(geom.(), geom.());
positions = (verts. / );
normals = (verts. / );
( j = ; j < verts.; j += ) {
k = j / ;
positions[k] = verts[j]; positions[k+] = verts[j+]; positions[k+] = verts[j+];
normals[k] = verts[j+]; normals[k+] = verts[j+]; normals[k+] = verts[j+];
}
bufGeom = .();
bufGeom.(, .(positions, ));
bufGeom.(, .(normals, ));
bufGeom.( .(idx, ));
bufGeom.( .().(pg.));
mesh = .(bufGeom, .({
: .(pg.., pg.., pg..),
: pg..,
: pg.. < ,
: .,
}));
mesh.. = flatMesh.;
scene.(mesh);
expressIdToMesh.(flatMesh., mesh);
}
});
Selection via Raycasting
const raycaster = new THREE.Raycaster();
renderer.domElement.addEventListener("click", (event) => {
const mouse = new THREE.Vector2(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1
);
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(scene.children);
if (hits.length > 0) {
const expressID = hits[0].object.userData.expressID;
const entity = ifcApi.GetLine(modelID, expressID, true);
displayProperties(entity);
}
});
Section Planes (Custom)
const clipPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 5);
renderer.clippingPlanes = [clipPlane];
renderer.localClippingEnabled = true;
function setClipHeight(y: number) {
clipPlane.constant = y;
}
Coordinate System Rules
| Source | Coordinate System | Action Required |
|---|
| web-ifc geometry output | Y-up (transformed internally) | NONE — render directly |
| IfcOpenShell geometry | Z-up (IFC native) | ALWAYS rotate: matrix.makeRotationX(-Math.PI / 2) |
COORDINATE_TO_ORIGIN: true | Centered at origin | Real-world coordinates are LOST — store separately if needed |
COORDINATE_TO_ORIGIN: false | Real-world offset | Model may appear far from origin — camera setup required |
Large Model Handling (>100MB IFC)
Memory Constraints
| Constraint | Limit |
|---|
| WASM linear memory | ~2GB practical maximum |
| Browser tab total | 2–4GB before crash |
| 8GB RAM laptop | ~1.7GB practical tab limit |
Performance Strategy
- ALWAYS pre-convert to Fragments on server for files >200MB. First-load IFC parsing is the bottleneck; cached Fragments load near-instantly.
- ALWAYS use
StreamAllMeshes instead of LoadAllGeometry. Streaming processes geometry incrementally without peak memory spike.
- ALWAYS call
ifcApi.CloseModel(modelID) when done to free WASM memory.
- Configure conservative memory limits:
const modelID = ifcApi.OpenModel(data, {
MEMORY_LIMIT: 1073741824,
TAPE_SIZE: 33554432,
});
- Use geometry instancing for repeated elements (identical windows, doors). web-ifc provides shared
geometryExpressID values that indicate reusable geometry.
- Export Fragments for caching:
const fragsBuffer = await model.getBuffer(false);
The Fragments Format
IFC files are NEVER rendered directly. The pipeline is:
IFC file → web-ifc WASM parsing → Fragment conversion → Three.js rendering
- First load: slow (parsing + tessellation + conversion)
- Subsequent loads from cached
.frag files: near-instant
- Fragments use Google Flatbuffers — 5-10x smaller than source IFC
- ALWAYS cache Fragments server-side for production applications
Property Access Patterns
| Operation | @thatopen/components | Raw web-ifc |
|---|
| Get by type | model.getItemsOfCategories(["IFCWALL"]) | ifcApi.GetLineIDsWithType(modelID, IFCWALL) |
| Get entity | Via FragmentsManager | ifcApi.GetLine(modelID, expressID, true) |
| Property sets | ifcApi.properties.getPropertySets(modelID, id, true) | Same |
| Spatial structure | ifcApi.properties.getSpatialStructure(modelID) | Same |
| Access name | entity.Name.value | entity.Name.value |
CRITICAL: web-ifc wraps string/numeric values in { value: ... } containers. ALWAYS access .value for the actual data. References are Handle objects unless flatten=true is passed to GetLine.
Critical Warnings
NEVER set the WASM path after calling ifcApi.Init() — the WASM module loads during Init() and path changes after that point have no effect.
NEVER use LoadAllGeometry for models with more than 10,000 elements — use StreamAllMeshes to avoid memory spikes.
NEVER mix Three.js versions — @thatopen/components pins a specific Three.js version. A second version in node_modules causes silent rendering failures.
NEVER manually rotate web-ifc output by -90 degrees — web-ifc ALREADY transforms IFC Z-up coordinates to Three.js Y-up internally.
ALWAYS call CloseModel(modelID) when disposing a model to free WASM memory.
ALWAYS verify the WASM path resolves correctly in your bundler (Webpack/Vite) — .wasm files MUST be copied to the output directory.
Reference Links
Official Sources