Skip to content

Geometry Processing

Guide to geometry extraction and processing in IFClite.

Overview

IFClite processes IFC geometry through a streaming pipeline:

flowchart TB subgraph Input["IFC Geometry Types"] Extrusion["ExtrudedAreaSolid"] Brep["FacetedBrep"] Clipping["BooleanClipping"] Mapped["MappedItem"] end subgraph Router["Geometry Router"] Detect["Type Detection"] Select["Processor Selection"] end subgraph Processors["Specialized Processors"] ExtProc["Extrusion Processor"] BrepProc["Brep Processor"] CSGProc["CSG Processor"] MapProc["Instance Processor"] end subgraph Output["GPU-Ready Output"] Mesh["Triangle Mesh"] Buffers["Vertex Buffers"] end Input --> Router Router --> Processors Extrusion --> ExtProc Brep --> BrepProc Clipping --> CSGProc Mapped --> MapProc Processors --> Output style Input fill:#6366f1,stroke:#312e81,color:#fff style Router fill:#2563eb,stroke:#1e3a8a,color:#fff style Processors fill:#10b981,stroke:#064e3b,color:#fff style Output fill:#a855f7,stroke:#581c87,color:#fff

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

classDiagram class Mesh { +number expressId +Float32Array positions +Float32Array normals +Uint32Array indices +Float32Array? uvs +number[] color } class GeometryResult { +Mesh[] meshes +BoundingBox bounds +number triangleCount +number vertexCount } class BoundingBox { +Vector3 min +Vector3 max +Vector3 center +Vector3 size } GeometryResult "1" --> "*" Mesh GeometryResult "1" --> "1" BoundingBox

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:

sequenceDiagram participant Parser participant Processor as Geometry Processor participant Collector as Mesh Collector participant GPU as WebGPU loop Batch Processing Parser->>Processor: Entity batch Processor->>Processor: Triangulate Processor->>Collector: Mesh batch Collector->>GPU: Upload buffers Note over GPU: Render visible meshes end

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:

flowchart LR subgraph Problem["Problem"] Large["Large Coordinates<br/>(6-7 digit values)"] Precision["Float32 Precision Loss"] Jitter["Visual Jitter"] Large --> Precision --> Jitter end subgraph Solution["Solution"] Detect["Detect Large Coords"] Shift["Auto-Shift to Origin"] Store["Store Offset"] Detect --> Shift --> Store end Problem --> Solution style Problem fill:#dc2626,stroke:#7f1d1d,color:#fff style Solution fill:#16a34a,stroke:#14532d,color:#fff

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:

flowchart LR subgraph Input Profile["2D Profile"] Direction["Extrusion Direction"] Depth["Extrusion Depth"] end subgraph Process Triangulate["Triangulate Profile<br/>(earcutr)"] Extrude["Generate Side Faces"] Cap["Create End Caps"] end subgraph Output Mesh["3D Mesh"] end Profile --> Triangulate Triangulate --> Extrude Direction --> Extrude Depth --> Extrude Extrude --> Cap Cap --> Mesh style Input fill:#6366f1,stroke:#312e81,color:#fff style Process fill:#2563eb,stroke:#1e3a8a,color:#fff style Output fill:#a855f7,stroke:#581c87,color:#fff

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:

flowchart LR First["First Operand"] Second["Second Operand"] Op["Boolean Operation<br/>(Difference/Union/Intersection)"] Result["Result Mesh"] First --> Op Second --> Op Op --> Result style First fill:#6366f1,stroke:#312e81,color:#fff style Second fill:#6366f1,stroke:#312e81,color:#fff style Op fill:#2563eb,stroke:#1e3a8a,color:#fff style Result fill:#a855f7,stroke:#581c87,color:#fff

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