Skip to content

Geometry Pipeline

Detailed architecture of geometry processing in IFClite.

Overview

The geometry pipeline transforms IFC shape representations into GPU-ready triangle meshes:

flowchart TB subgraph Input["IFC Geometry"] Extrusion["IfcExtrudedAreaSolid"] Brep["IfcFacetedBrep"] Boolean["IfcBooleanResult"] Mapped["IfcMappedItem"] Surface["IfcSurfaceModel"] end subgraph Router["Geometry Router"] Detect["Type Detection"] Select["Processor Selection"] end subgraph Processors["Specialized Processors"] ExtProc["ExtrusionProcessor"] BrepProc["BrepProcessor"] BoolProc["BooleanProcessor"] MapProc["MappedItemProcessor"] SurfProc["SurfaceProcessor"] end subgraph Output["Output"] Mesh["Triangle Mesh"] end Input --> Router --> Processors --> Output

Geometry Representation Types

IFC Geometry Hierarchy

classDiagram class IfcRepresentationItem { <<abstract>> } class IfcSolidModel { <<abstract>> } class IfcSweptAreaSolid { +IfcProfileDef SweptArea +IfcAxis2Placement3D Position } class IfcExtrudedAreaSolid { +IfcDirection ExtrudedDirection +IfcPositiveLengthMeasure Depth } class IfcFacetedBrep { +IfcClosedShell Outer } class IfcBooleanResult { +IfcBooleanOperand FirstOperand +IfcBooleanOperand SecondOperand +IfcBooleanOperator Operator } IfcRepresentationItem <|-- IfcSolidModel IfcSolidModel <|-- IfcSweptAreaSolid IfcSweptAreaSolid <|-- IfcExtrudedAreaSolid IfcSolidModel <|-- IfcFacetedBrep IfcSolidModel <|-- IfcBooleanResult

Coverage by Type

Geometry Type Coverage Notes
IfcExtrudedAreaSolid Full Most common
IfcFacetedBrep Full Pre-triangulated
IfcBooleanClippingResult Partial CSG operations
IfcMappedItem Full Instancing
IfcSurfaceModel Partial Surface meshes
IfcTriangulatedFaceSet Full IFC4 triangles

Extrusion Processing

Pipeline

flowchart TB subgraph Input["Input"] Profile["2D Profile"] Direction["Extrusion Direction"] Depth["Depth"] Position["Placement"] end subgraph Profile["Profile Processing"] Extract["Extract Outer Boundary"] Holes["Extract Inner Boundaries"] Flatten["Flatten to 2D"] end subgraph Triangulate["Triangulation"] Earcut["earcutr Algorithm"] Bottom["Bottom Face"] Top["Top Face"] end subgraph Extrude["Extrusion"] Walls["Generate Side Walls"] Join["Join Vertices"] Normals["Compute Normals"] end subgraph Output["Output"] Mesh["Triangle Mesh"] end Input --> Profile --> Triangulate --> Extrude --> Output

Profile Types

classDiagram class IfcProfileDef { <<abstract>> +IfcProfileTypeEnum ProfileType +IfcLabel ProfileName } class IfcRectangleProfileDef { +IfcPositiveLengthMeasure XDim +IfcPositiveLengthMeasure YDim } class IfcCircleProfileDef { +IfcPositiveLengthMeasure Radius } class IfcArbitraryClosedProfileDef { +IfcCurve OuterCurve } class IfcArbitraryProfileDefWithVoids { +SET~IfcCurve~ InnerCurves } IfcProfileDef <|-- IfcRectangleProfileDef IfcProfileDef <|-- IfcCircleProfileDef IfcProfileDef <|-- IfcArbitraryClosedProfileDef IfcArbitraryClosedProfileDef <|-- IfcArbitraryProfileDefWithVoids

Earcut Algorithm

flowchart LR subgraph Input["Input"] Poly["Polygon with Holes"] end subgraph Process["earcutr Process"] Flatten["Flatten coordinates"] Ear["Find ear"] Clip["Clip ear"] Repeat["Repeat until done"] end subgraph Output["Output"] Indices["Triangle indices"] end Input --> Process --> Output
use earcutr::earcut;

fn triangulate_profile(
    outer: &[Point2],
    holes: &[Vec<Point2>]
) -> Vec<u32> {
    // Flatten to coordinate array
    let mut coords: Vec<f64> = Vec::new();
    let mut hole_indices: Vec<usize> = Vec::new();

    // Add outer boundary
    for p in outer {
        coords.push(p.x);
        coords.push(p.y);
    }

    // Add holes
    for hole in holes {
        hole_indices.push(coords.len() / 2);
        for p in hole {
            coords.push(p.x);
            coords.push(p.y);
        }
    }

    // Triangulate
    earcut(&coords, &hole_indices, 2)
        .unwrap()
        .into_iter()
        .map(|i| i as u32)
        .collect()
}

Brep Processing

FacetedBrep Pipeline

flowchart TB subgraph Input["IfcFacetedBrep"] Shell["IfcClosedShell"] Faces["IfcFace[]"] end subgraph Process["Processing"] Extract["Extract face bounds"] Orient["Check orientation"] Triangulate["Fan triangulation"] Normals["Compute normals"] end subgraph Output["Output"] Mesh["Triangle Mesh"] end Input --> Process --> Output

Face Triangulation

graph LR subgraph Polygon["Face Polygon"] V0["V0"] V1["V1"] V2["V2"] V3["V3"] V4["V4"] end subgraph Triangles["Fan Triangulation"] T1["V0-V1-V2"] T2["V0-V2-V3"] T3["V0-V3-V4"] end V0 --> T1 V1 --> T1 V2 --> T1 V0 --> T2 V2 --> T2 V3 --> T2

Boolean Operations

CSG Pipeline

flowchart TB subgraph Input["Input"] First["First Operand"] Second["Second Operand"] Op["Operator"] end subgraph Prepare["Preparation"] Mesh1["Triangulate First"] Mesh2["Triangulate Second"] end subgraph CSG["CSG Operation"] Intersect["Find Intersections"] Classify["Classify Triangles"] Combine["Combine Result"] end subgraph Output["Output"] Result["Result Mesh"] end Input --> Prepare --> CSG --> Output

Boolean Operators

Operator Description Common Use
DIFFERENCE A - B Wall openings
UNION A + B Composite shapes
INTERSECTION A ∩ B Clipping

Coordinate Transformations

Placement Stack

flowchart TB subgraph Stack["Transformation Stack"] World["World Origin"] Site["Site Placement"] Building["Building Placement"] Storey["Storey Placement"] Element["Element Placement"] Local["Local Placement"] end subgraph Matrix["Combined Matrix"] M["4x4 Transform"] end World --> Site --> Building --> Storey --> Element --> Local Local --> M

Matrix Operations

use nalgebra::{Matrix4, Point3, Vector3};

fn compute_transform(placements: &[Placement]) -> Matrix4<f64> {
    let mut result = Matrix4::identity();

    for placement in placements {
        let local = Matrix4::new_translation(&placement.location)
            * Matrix4::from_axis_angle(&placement.axis, placement.angle);
        result = result * local;
    end

    result
}

fn transform_point(point: Point3<f64>, matrix: &Matrix4<f64>) -> Point3<f64> {
    matrix.transform_point(&point)
}

Large Coordinate Handling

flowchart LR subgraph Problem["Problem"] Large["Large Coords<br/>(487234.5, 5234891.2, 0)"] Float32["Float32 Precision<br/>(7 digits)"] Jitter["Visual Jitter"] end subgraph Solution["Solution"] Detect["Detect large values"] Shift["Compute origin shift"] Apply["Apply to all vertices"] Store["Store offset"] end Problem --> Solution
function computeOriginShift(bounds: BoundingBox): Vector3 {
  const threshold = 10000; // Shift if > 10km from origin

  if (Math.abs(bounds.center.x) > threshold ||
      Math.abs(bounds.center.y) > threshold) {
    return {
      x: -bounds.center.x,
      y: -bounds.center.y,
      z: 0
    };
  }

  return { x: 0, y: 0, z: 0 };
}

Quality Modes

Curve Discretization

graph LR subgraph Circle["Circle Approximation"] Fast["FAST: 8 segments"] Balanced["BALANCED: 16 segments"] High["HIGH: 32 segments"] end
Mode Segments Triangles Use Case
FAST 8 Fewer Mobile, preview
BALANCED 16 Medium Default
HIGH 32 More Detailed viewing

Instancing

MappedItem Processing

flowchart TB subgraph Definition["Mapped Representation"] Source["Source Geometry"] Transform["Transform Matrix"] end subgraph Detection["Instance Detection"] Hash["Hash source ID"] Lookup["Lookup in cache"] end subgraph Output["Output"] Reuse["Reuse existing mesh"] Transforms["Instance transforms[]"] end Definition --> Detection Detection -->|"Cache hit"| Reuse Detection -->|"Cache miss"| Source Source --> Reuse Reuse --> Transforms

Instance Data Structure

interface InstancedMesh {
  baseMesh: Mesh;
  transforms: Matrix4[];
  expressIds: number[];
}

// GPU instancing data
interface InstanceData {
  positions: Float32Array;    // Shared geometry
  normals: Float32Array;
  indices: Uint32Array;
  instanceMatrices: Float32Array;  // Per-instance transforms
  instanceColors: Float32Array;    // Per-instance colors
}

Streaming Pipeline

sequenceDiagram participant Parser participant Queue as Entity Queue participant Router participant Processor participant Collector as Mesh Collector participant GPU Parser->>Queue: Entities with geometry loop Batch Processing Queue->>Router: Entity batch Router->>Processor: Dispatch by type Processor->>Processor: Triangulate Processor->>Collector: Mesh batch Collector->>GPU: Upload buffers end

Batch Processing

async function processGeometryBatches(
  entities: Entity[],
  batchSize: number,
  onBatch: (batch: MeshBatch) => Promise<void>
): Promise<void> {
  const geoEntities = entities.filter(e => e.hasGeometry);

  for (let i = 0; i < geoEntities.length; i += batchSize) {
    const batch = geoEntities.slice(i, i + batchSize);
    const meshes = await Promise.all(
      batch.map(e => processEntity(e))
    );

    await onBatch({
      meshes,
      bounds: computeBounds(meshes),
      progress: (i + batch.length) / geoEntities.length
    });
  }
}

CSG Kernel

Two boolean / CSG kernels coexist behind a Cargo feature flag.

Default (legacy BSP)

rust/geometry/src/bsp_csg.rs — a Rust port of csg.js (Evan Wallace, MIT). Triangle-mesh BSP. Hard-caps at 24 polygons per operand (csg.rs:117). On cap exceeded or kernel error, falls back to the un-cut host mesh and emits a structured BoolFailure record (drainable via GeometryRouter::take_csg_failures). This is the default for the wasm32-unknown-unknown build target since the alternative (Manifold) has unresolved upstream toolchain dependencies on that target.

Optional (Manifold)

Behind --features manifold-csg. Uses Manifold via the manifold-csg crate (Apache-2/MIT, native C++ kernel built through cmake). No operand cap, manifold-by-construction output, real solid-solid IfcBooleanResult.{DIFFERENCE, UNION, INTERSECTION}. A vertex-weld pre-pass in rust/geometry/src/manifold_kernel.rs collapses the polygon-soup mesh layout ifc-lite's extruded-solid builder produces (24 verts per cube → 8) so Manifold accepts the input.

BoolFailure records and GeometryRouter::take_csg_failures work identically under both kernels. Sprint 2 acceptance gates assert total_failures == 0 on AC20-FZK-Haus.ifc and C20-Institute-Var-2.ifc under --features manifold-csg; both pass.

WASM status

--features manifold-csg-wasm-uu (which implies manifold-csg plus upstream's unstable-wasm-uu) is currently blocked on a libc++ / wasm-cxx-shim incompatibility in the manifold-csg-sys crate:

  • libc++-18: _LIBCPP_AVAILABILITY_VERBOSE_ABORT undefined when the shim's __assertion_handler is loaded.
  • libc++-20: musl locale headers (__locale_dir/locale_base_api/musl.h) are pulled in despite the shim's _LIBCPP_HAS_LOCALIZATION 0 define; locale.h then fails to resolve in the wasm32-unknown-unknown no-libc environment.

Both are upstream issues (zmerlynn/manifold-csg-sys and its bundled zmerlynn/wasm-cxx-shim); not patchable from this repo. Track upstream and re-attempt once the shim is updated for current libc++ versions. Until then manifold-csg stays opt-in for native builds only and wasm32-unknown-unknown builds continue to use the legacy BSP path.

Performance Metrics

Operation Time (typical) Notes
Profile extraction 0.1 ms Per entity
Earcut triangulation 0.5 ms Simple profile
Extrusion 0.2 ms Per entity
Boolean operation 5-50 ms Complex
Transform application 0.01 ms Per vertex

Throughput

  • Simple extrusions: ~2000 entities/sec
  • Complex Breps: ~200 entities/sec
  • Boolean operations: ~20 entities/sec

Next Steps