Geometry Processing¶
Guide to geometry extraction and processing in IFClite.
Overview¶
IFClite processes IFC geometry through a streaming pipeline:
Tessellation Quality¶
Curved geometry (swept pipes, cylinders, fillets, NURBS patches) is
approximated with straight segments. The detail level is selectable per
GeometryProcessor — no WASM rebuild needed:
| Level | Curved-surface segment density | Profile circles (opening cutters / caps) | Use case |
|---|---|---|---|
'lowest' |
×0.25 | max 8 segments | Maximum throughput, previews |
'low' |
×0.5 | max 16 segments | Mobile, large federated models |
'medium' (default) |
×1 — historical densities | 36 segments | General use |
'high' |
×2 | 36 (never finer) | Smooth pipes / cylinders |
'highest' |
×4 | 36 (never finer) | Close-up curved detail |
import { GeometryProcessor } from '@ifc-lite/geometry';
// At construction…
const geometry = new GeometryProcessor({ tessellationQuality: 'high' });
await geometry.init();
// …or at runtime, BEFORE processing (already-emitted meshes are not
// regenerated — reload the model to apply a new level):
geometry.setTessellationQuality('low');
const result = await geometry.process(new Uint8Array(buffer));
The same knob exists on the raw WASM API for consumers driving
processGeometryBatch directly:
import { IfcAPI } from '@ifc-lite/wasm';
const api = new IfcAPI();
api.setTessellationQuality('highest'); // applies to subsequent batches
Performance trade-off. Triangle count and processing time on
curved-heavy models scale roughly with the density multiplier: 'highest'
can quadruple the triangles of a pipe-rack model, 'lowest' quarters them.
Boxy architectural models (extrusions, breps) are barely affected — only
curved tessellation scales.
Guarantees:
- Leaving the level unset (or passing
'medium'/null) produces output byte-for-byte identical to previous releases — upgrading is safe. - Segment counts rise monotonically with the level (never fewer triangles at a higher level).
- Profile-plane outlines (extruded caps and opening cutters) never get
finer than
'medium'— denser opening circles only multiply earcut cap-bridge slivers on plates with bolt holes. They do coarsen below'medium'for preview levels. - WASM paths only (main-thread, streaming and worker pool); the native Tauri pipeline does not consume the level yet.
Mesh Data Structure¶
Accessing Mesh Data¶
import { GeometryProcessor } from '@ifc-lite/geometry';
const geometry = new GeometryProcessor();
await geometry.init();
const result = await geometry.process(new Uint8Array(buffer));
// Get all meshes
for (const mesh of result.meshes) {
console.log(`Entity #${mesh.expressId}:`);
console.log(` Vertices: ${mesh.positions.length / 3}`);
console.log(` Triangles: ${mesh.indices.length / 3}`);
console.log(` Color: rgba(${mesh.color.join(', ')})`);
}
// Find mesh by entity ID
const wallMesh = result.meshes.find(m => m.expressId === wallId);
// Precomputed model bounds (from coordinate info on the result)
const bounds = result.coordinateInfo.shiftedBounds;
console.log(`Model bounds:`, bounds);
Streaming Geometry¶
Process geometry incrementally for large files:
Streaming Example¶
import { GeometryProcessor } from '@ifc-lite/geometry';
import { Renderer } from '@ifc-lite/renderer';
const geometry = new GeometryProcessor();
await geometry.init();
const renderer = new Renderer(canvas);
await renderer.init();
// Stream geometry progressively
for await (const event of geometry.processStreaming(new Uint8Array(buffer))) {
switch (event.type) {
case 'start':
console.log('Starting geometry extraction');
break;
case 'batch':
// Upload meshes to GPU as they arrive
renderer.addMeshes(event.meshes, true); // isStreaming = true
// Render current state
renderer.render();
console.log(`Meshes so far: ${event.totalSoFar}`);
break;
case 'complete':
// Finalize rendering
renderer.fitToView();
console.log(`Complete: ${event.totalMeshes} meshes`);
break;
}
}
Coordinate Handling¶
IFC files often use large georeferenced coordinates that cause precision issues:
Auto Origin Shift¶
The geometry processor automatically handles large coordinates:
import { GeometryProcessor } from '@ifc-lite/geometry';
const geometry = new GeometryProcessor();
await geometry.init();
const result = await geometry.process(new Uint8Array(buffer));
// Access the computed shift from coordinate info (returned on the result)
const coordInfo = result.coordinateInfo;
if (coordInfo?.originShift) {
console.log(`Origin shifted by:`, coordInfo.originShift);
// { x: 487234.5, y: 5234891.2, z: 0 }
}
// Convert local coordinates back to world
function toWorldCoords(localPos: Vector3, shift: Vector3): Vector3 {
return {
x: localPos.x + shift.x,
y: localPos.y + shift.y,
z: localPos.z + shift.z
};
}
Geometry Processors¶
Extrusion Processor¶
Handles IfcExtrudedAreaSolid entities:
Brep Processor¶
Handles IfcFacetedBrep entities:
// Brep processing is straightforward - faces are already triangulated
// in most cases, or need simple fan triangulation
const brepMesh = processBrep({
faces: brepEntity.faces,
vertices: brepEntity.vertices
});
Boolean Operations¶
Handles IfcBooleanClippingResult:
Batching¶
The renderer automatically groups geometry by colour into a small number of
batched draw calls (one BatchedMesh per colour group), so a model with many
repeated elements still renders in a handful of draws — no manual step:
import { GeometryProcessor } from '@ifc-lite/geometry';
const geometry = new GeometryProcessor();
await geometry.init();
const result = await geometry.process(new Uint8Array(buffer));
// The renderer batches by colour when you load the meshes.
renderer.loadGeometry(result);
Performance Optimization¶
Memory-Efficient Processing¶
Use streaming for large files:
import { GeometryProcessor } from '@ifc-lite/geometry';
const geometry = new GeometryProcessor();
await geometry.init();
// Stream geometry in batches
for await (const event of geometry.processStreaming(new Uint8Array(buffer), undefined, 50)) {
if (event.type === 'batch') {
renderer.addMeshes(event.meshes, true);
console.log(`Meshes so far: ${event.totalSoFar}`);
}
}
Filtering Geometry¶
To only render specific entity types, filter the meshes after processing:
import { IfcParser } from '@ifc-lite/parser';
import { GeometryProcessor } from '@ifc-lite/geometry';
const parser = new IfcParser();
const store = await parser.parseColumnar(buffer);
// Get expressIds for types you want
const wantedIds = new Set([
...(store.entityIndex.byType.get('IFCWALL') ?? []),
...(store.entityIndex.byType.get('IFCDOOR') ?? []),
...(store.entityIndex.byType.get('IFCWINDOW') ?? [])
]);
// Process all geometry
const geometry = new GeometryProcessor();
await geometry.init();
const result = await geometry.process(new Uint8Array(buffer));
// Filter meshes
const filteredMeshes = result.meshes.filter(m => wantedIds.has(m.expressId));
renderer.loadGeometry(filteredMeshes);
Geometry Statistics¶
import { GeometryProcessor } from '@ifc-lite/geometry';
const geometry = new GeometryProcessor();
await geometry.init();
const result = await geometry.process(new Uint8Array(buffer));
// Calculate statistics from meshes
let totalTriangles = 0;
let totalVertices = 0;
for (const mesh of result.meshes) {
totalTriangles += mesh.indices.length / 3;
totalVertices += mesh.positions.length / 3;
}
console.log('Geometry Statistics:');
console.log(` Total meshes: ${result.meshes.length}`);
console.log(` Total triangles: ${totalTriangles}`);
console.log(` Total vertices: ${totalVertices}`);
Next Steps¶
- Rendering Guide - Display geometry with WebGPU
- Parsing Guide - Parse options and streaming
- API Reference - Complete API docs