Skip to content

Latest commit

 

History

History
674 lines (547 loc) · 16.6 KB

File metadata and controls

674 lines (547 loc) · 16.6 KB

Architecture Overview

This document describes the high-level architecture of IFClite, including both client-side and server-side processing paradigms.

System Architecture

IFClite supports two processing paradigms: client-side (WASM in browser) and server-side (native Rust). It provides multi-model federation for loading and managing multiple IFC models simultaneously with unified selection, visibility control, and coordinated ID spaces.

Layer Overview

flowchart TB
    subgraph Clients["Clients"]
        direction LR
        Web["Web App"]
        Desktop["Desktop"]
        CLI["CLI"]
    end

    subgraph Features["Feature Layers"]
        direction LR
        Federation["Multi-Model Federation"]
        BCFFeature["BCF"]
        IDSFeature["IDS"]
        Drawings["2D Drawings"]
        MutationsFeature["Mutations"]
    end

    subgraph APIs["APIs"]
        direction LR
        TS["TypeScript"]
        WASM["WASM"]
        Rust["Rust"]
    end

    subgraph Storage["Storage"]
        direction LR
        Tables["Columnar Tables"]
        Graph["Relationship Graph"]
        GPU["GPU Buffers"]
    end

    Clients --> Features
    Features --> APIs
    APIs --> Storage
Loading

Processing Paradigms

The system offers two processing paths depending on your needs:

flowchart LR
    subgraph ClientPath["Client-Side (WASM)"]
        direction TB
        C1["Parser"]
        C2["Geometry"]
        C3["Renderer"]
        C1 --> C2 --> C3
    end

    subgraph ServerPath["Server-Side (Native)"]
        direction TB
        S1["Parser"]
        S2["Geometry"]
        S3["Parquet Encoder"]
        S4["Content Cache"]
        S1 --> S2 --> S3 --> S4
    end
Loading

Client vs Server Paradigm

Aspect Client-Side (WASM) Server-Side (Rust)
Processing Single-threaded Multi-threaded (Rayon)
Memory 4GB WASM limit System RAM
Caching Browser storage Content-addressable disk
Format Raw geometry Parquet (15-50x smaller)
Best For Privacy, offline Teams, large files

Design Principles

1. Zero-Copy Where Possible

Data flows through the system with minimal copying:

flowchart LR
    subgraph Traditional["Traditional Approach"]
        T1["File Buffer"]
        T2["Parse to Objects"]
        T3["Convert to Arrays"]
        T4["Upload to GPU"]
        T1 -->|copy| T2 -->|copy| T3 -->|copy| T4
    end

    subgraph IFCLite["IFClite Approach"]
        I1["File Buffer"]
        I2["Direct Index"]
        I3["TypedArrays"]
        I4["GPU Upload"]
        I1 -->|reference| I2 -->|view| I3 -->|share| I4
    end

    style Traditional fill:#dc2626,stroke:#7f1d1d,color:#fff
    style IFCLite fill:#16a34a,stroke:#14532d,color:#fff
Loading

2. Streaming First

Process data incrementally for responsive UIs:

sequenceDiagram
    participant File
    participant Parser
    participant Processor
    participant Renderer
    participant User

    File->>Parser: Chunk 1
    Parser->>Processor: Entities 1-100
    Processor->>Renderer: Meshes 1-50
    Renderer->>User: First render (300ms)

    File->>Parser: Chunk 2
    Parser->>Processor: Entities 101-200
    Processor->>Renderer: Meshes 51-100
    Note over User: Progressive loading

    File->>Parser: Chunk N
    Parser->>Processor: All entities
    Processor->>Renderer: All meshes
    Renderer->>User: Complete
Loading

3. On-Demand Property Extraction

Properties parsed lazily for faster initial load:

flowchart LR
    subgraph TraditionalParsing["Traditional"]
        T1["Parse All Entities"]
        T2["Parse All Properties"]
        T3["Build All Tables"]
        T1 --> T2 --> T3
    end

    subgraph OnDemand["IFClite On-Demand"]
        O1["Parse Entities"]
        O2["Build Index"]
        O3["Map: entityId → psetIds"]
        O4["Parse on Access"]
        O1 --> O2 --> O3
        O3 -.->|"lazy"| O4
    end

    style TraditionalParsing fill:#dc2626,stroke:#7f1d1d,color:#fff
    style OnDemand fill:#16a34a,stroke:#14532d,color:#fff
Loading

4. Columnar Storage

Store data in columnar format for cache-efficient access:

graph TB
    subgraph RowBased["Row-Based (Traditional)"]
        R1["Entity 1: id=1, type=WALL, name='A'"]
        R2["Entity 2: id=2, type=DOOR, name='B'"]
        R3["Entity 3: id=3, type=WALL, name='C'"]
    end

    subgraph Columnar["Columnar (IFClite)"]
        C1["IDs: Uint32Array [1, 2, 3, ...]"]
        C2["Types: Uint16Array [WALL, DOOR, WALL, ...]"]
        C3["Names: StringTable ['A', 'B', 'C', ...]"]
    end
Loading

5. Hybrid Data Model

Combine the best of different data structures:

Data Structure Use Case Access Pattern
Columnar Tables Bulk queries, filtering Sequential scan
CSR Graph Relationship traversal Adjacency lookup
On-Demand Maps Property access Hash lookup
BVH Raycasting Tree traversal

Package Architecture

The monorepo contains 18 TypeScript packages, 4 Rust crates, and multiple application targets.

graph TB
    subgraph Rust["Rust Crates"]
        Core["ifc-lite-core<br/>Parsing"]
        Geo["ifc-lite-geometry<br/>Triangulation"]
        Wasm["ifc-lite-wasm<br/>Bindings"]
        Server["ifc-lite-server<br/>HTTP API"]
    end

    subgraph TS["TypeScript Packages (18)"]
        Parser["@ifc-lite/parser"]
        IFCX["@ifc-lite/ifcx"]
        Geometry["@ifc-lite/geometry"]
        Renderer["@ifc-lite/renderer"]
        ServerClient["@ifc-lite/server-client"]
        ServerBin["@ifc-lite/server-bin"]
        Cache["@ifc-lite/cache"]
        Query["@ifc-lite/query"]
        Data["@ifc-lite/data"]
        Export["@ifc-lite/export"]
        BCF["@ifc-lite/bcf"]
        IDS["@ifc-lite/ids"]
        Drawing2D["@ifc-lite/drawing-2d"]
        Mutations["@ifc-lite/mutations"]
        Spatial["@ifc-lite/spatial"]
        Codegen["@ifc-lite/codegen"]
        WasmTS["@ifc-lite/wasm"]
        CreateCLI["@ifc-lite/create-ifc-lite"]
    end

    subgraph Apps["Applications"]
        Viewer["Viewer App"]
        Desktop["Desktop (Tauri)"]
        CLI["create-ifc-lite"]
    end

    Wasm --> Core
    Wasm --> Geo
    Parser --> WasmTS
    WasmTS --> Wasm
    Geometry --> WasmTS
    Renderer --> Geometry
    ServerClient --> Server
    Query --> Data
    Export --> Data
    BCF --> Data
    IDS --> Data
    Drawing2D --> Geometry
    Mutations --> Data
    Spatial --> Data
    Viewer --> Parser
    Viewer --> Renderer
    Viewer --> ServerClient
    Desktop --> Core
    Desktop --> Geo
Loading

Server Architecture

flowchart TB
    subgraph Client["Browser Client"]
        Hash["SHA-256 Hash"]
        Upload["File Upload"]
        Decode["Parquet Decoder"]
        Render["WebGPU Renderer"]
    end

    subgraph Server["Rust Server (Axum)"]
        Router["API Router"]
        Parser["IFC Parser"]
        GeoProc["Geometry Processor"]
        DataModel["Data Model Extractor"]
        Serializer["Parquet Serializer"]
    end

    subgraph Cache["Cache Layer"]
        DiskCache[(Disk Cache)]
    end

    Hash -->|"check"| Router
    Router -->|"hit"| DiskCache
    DiskCache --> Decode
    Upload -->|"miss"| Parser
    Parser --> GeoProc
    Parser --> DataModel
    GeoProc --> Serializer
    DataModel --> Serializer
    Serializer --> DiskCache
    Serializer --> Decode
    Decode --> Render

    style Client fill:#6366f1,stroke:#312e81,color:#fff
    style Server fill:#10b981,stroke:#064e3b,color:#fff
    style Cache fill:#f59e0b,stroke:#7c2d12,color:#fff
Loading

Server Cache Strategy

sequenceDiagram
    participant Client
    participant Server
    participant Cache

    Client->>Client: Compute SHA-256 hash
    Client->>Server: GET /cache/check/{hash}

    alt Cache Hit
        Server->>Cache: Lookup
        Cache-->>Server: Parquet data
        Server-->>Client: 200 (skip upload!)
    else Cache Miss
        Server-->>Client: 404
        Client->>Server: POST /parse/parquet
        Server->>Server: Parse (parallel)
        Server->>Cache: Store
        Server-->>Client: Parquet response
    end
Loading

Data Flow

Client-Side Parse Flow

Each model is parsed independently and then registered with the FederationRegistry, which assigns non-overlapping ID ranges (idOffset) so that multiple models can coexist with unique global IDs (globalId = localExpressId + model.idOffset).

flowchart TB
    Input["IFC File<br/>(ArrayBuffer)"]

    subgraph Tokenize["1. Tokenize"]
        STEP["STEP Lexer"]
        Tokens["Token Stream"]
    end

    subgraph Scan["2. Scan"]
        EntityScan["Entity Scanner"]
        Index["Entity Index"]
    end

    subgraph Decode["3. Decode"]
        Decoder["Entity Decoder"]
        Attrs["Attributes"]
    end

    subgraph Store["4. Store"]
        Tables["Columnar Tables"]
        Graph["Relationship Graph"]
        OnDemand["On-Demand Maps"]
    end

    subgraph Federate["5. Federate"]
        Registry["FederationRegistry"]
        GlobalIDs["Global ID Assignment"]
    end

    Output["IfcDataStore"]

    Input --> Tokenize
    Tokenize --> Scan
    Scan --> Decode
    Decode --> Store
    Store --> Federate
    Federate --> Output

    style Input fill:#6366f1,stroke:#312e81,color:#fff
    style Tokenize fill:#2563eb,stroke:#1e3a8a,color:#fff
    style Scan fill:#10b981,stroke:#064e3b,color:#fff
    style Decode fill:#f59e0b,stroke:#7c2d12,color:#fff
    style Store fill:#a855f7,stroke:#581c87,color:#fff
    style Federate fill:#ec4899,stroke:#831843,color:#fff
    style Output fill:#16a34a,stroke:#14532d,color:#fff
Loading

Server-Side Parse Flow

flowchart TB
    Input["IFC File"]

    subgraph Parse["1. Parse (Parallel)"]
        Tokenize["STEP Tokenizer"]
        Extract["Entity Extractor"]
    end

    subgraph Process["2. Process (Parallel)"]
        Geo["Geometry Extraction"]
        Data["Data Model Extraction"]
    end

    subgraph Serialize["3. Serialize"]
        ParquetGeo["Parquet Geometry"]
        ParquetData["Parquet Data Model"]
    end

    subgraph Cache["4. Cache"]
        Store["Disk Cache"]
    end

    Output["Parquet Response"]

    Input --> Parse
    Parse --> Process
    Geo --> ParquetGeo
    Data --> ParquetData
    ParquetGeo --> Store
    ParquetData --> Store
    Store --> Output

    style Input fill:#6366f1,stroke:#312e81,color:#fff
    style Parse fill:#2563eb,stroke:#1e3a8a,color:#fff
    style Process fill:#10b981,stroke:#064e3b,color:#fff
    style Serialize fill:#f59e0b,stroke:#7c2d12,color:#fff
    style Cache fill:#a855f7,stroke:#581c87,color:#fff
Loading

Render Flow

flowchart TB
    subgraph Input["Input"]
        Meshes["Mesh Data"]
        Camera["Camera State"]
    end

    subgraph Process["Processing"]
        Cull["Frustum Culling"]
        Sort["Depth Sort"]
        Batch["Batching"]
    end

    subgraph Upload["GPU Upload"]
        Vertex["Vertex Buffers"]
        Index["Index Buffers"]
        Uniform["Uniforms"]
    end

    subgraph Render["Render"]
        Pass["Render Pass"]
        Section["Section Planes"]
        Draw["Draw Calls"]
    end

    Output["Canvas"]

    Input --> Process
    Process --> Upload
    Upload --> Render
    Render --> Output

    style Input fill:#6366f1,stroke:#312e81,color:#fff
    style Process fill:#2563eb,stroke:#1e3a8a,color:#fff
    style Upload fill:#10b981,stroke:#064e3b,color:#fff
    style Render fill:#f59e0b,stroke:#7c2d12,color:#fff
    style Output fill:#a855f7,stroke:#581c87,color:#fff
Loading

Memory Architecture

In a multi-model federation scenario, each loaded model maintains its own data store in the JS heap. The FederationRegistry tracks ID ranges per model to enable O(1) global-to-local ID resolution without duplicating entity data across models.

graph TB
    subgraph JS["JavaScript Heap"]
        Strings["String Table"]
        Metadata["Entity Metadata"]
        Query["Query Results"]
        OnDemand["On-Demand Maps"]
        FedRegistry["FederationRegistry"]
        Models["Models Map"]
    end

    subgraph Wasm["WASM Linear Memory"]
        Parser["Parser State"]
        Geometry["Geometry Processing"]
        Buffers["Mesh Buffers"]
    end

    subgraph GPU["GPU Memory"]
        VBO["Vertex Buffers"]
        IBO["Index Buffers"]
        UBO["Uniform Buffers"]
        ID["ID Buffer (Picking)"]
    end

    Wasm -->|"Zero-copy view"| JS
    Wasm -->|"Direct upload"| GPU
Loading

Memory Efficiency

Component Memory Strategy
Strings Deduplicated string table (30% reduction)
Entity IDs Uint32Array (fixed-size)
Types Uint16Array enum (2 bytes vs ~20 for string)
Properties On-demand parsing (not pre-loaded)
Geometry Streaming + dispose after upload
Server Parquet (15-50x smaller transfer)

Threading Model

Client-Side

flowchart LR
    subgraph Main["Main Thread"]
        UI["UI Events"]
        Render["Rendering"]
        Query["Queries"]
    end

    subgraph Worker["Web Worker"]
        Parse["Parsing"]
        Geo["Geometry"]
    end

    Main <-->|"Transferable"| Worker
Loading

Server-Side

flowchart TB
    subgraph Axum["Axum Runtime"]
        Router["Async Router"]
        Handlers["Request Handlers"]
    end

    subgraph Rayon["Rayon Thread Pool"]
        Parser1["Parser Thread 1"]
        Parser2["Parser Thread 2"]
        ParserN["Parser Thread N"]
    end

    subgraph Tokio["Tokio Runtime"]
        Cache["Cache I/O"]
        Response["Response Stream"]
    end

    Router --> Handlers
    Handlers -->|"spawn_blocking"| Rayon
    Handlers --> Tokio
Loading

IFC5 (IFCX) Architecture

flowchart TB
    subgraph Input["IFCX File"]
        JSON["JSON Structure"]
        Header["Header"]
        Schemas["Schemas"]
        Data["Data Nodes"]
    end

    subgraph Composition["ECS Composition"]
        Flatten["Flatten Layers"]
        Inherit["Resolve Inheritance"]
        Tree["Build Tree"]
    end

    subgraph Extract["Extraction"]
        Entities["Entity Extractor"]
        Props["Property Extractor"]
        Geo["Geometry Extractor"]
        Hierarchy["Hierarchy Builder"]
    end

    subgraph Output["Output"]
        Store["IfcxParseResult"]
        Meshes["Pre-tessellated Meshes"]
    end

    Input --> Composition
    Composition --> Extract
    Extract --> Output

    style Input fill:#6366f1,stroke:#312e81,color:#fff
    style Composition fill:#10b981,stroke:#064e3b,color:#fff
    style Extract fill:#f59e0b,stroke:#7c2d12,color:#fff
    style Output fill:#a855f7,stroke:#581c87,color:#fff
Loading

Extension Points

graph TB
    subgraph Core["Core System"]
        Parser["Parser"]
        Geometry["Geometry"]
        Renderer["Renderer"]
    end

    subgraph Extensions["Extension Points"]
        CustomExtractor["Custom Extractors"]
        CustomProcessor["Custom Processors"]
        CustomShaders["Custom Shaders"]
        Plugins["Plugins"]
    end

    CustomExtractor -.->|extends| Parser
    CustomProcessor -.->|extends| Geometry
    CustomShaders -.->|extends| Renderer
    Plugins -.->|hooks| Core
Loading

Adding Custom Geometry Processor

import { GeometryProcessor, ProcessorRegistry } from '@ifc-lite/geometry';

class CustomProcessor extends GeometryProcessor {
  canProcess(entity: Entity): boolean {
    return entity.type === 'IFCMYCUSTOMTYPE';
  }

  process(entity: Entity): Mesh {
    // Custom processing logic
    return mesh;
  }
}

ProcessorRegistry.register(new CustomProcessor());

Technology Stack

graph TB
    subgraph Languages["Languages"]
        Rust["Rust"]
        TS["TypeScript"]
        WGSL["WGSL (Shaders)"]
    end

    subgraph Runtime["Runtime"]
        WASM["WebAssembly"]
        WebGPU["WebGPU"]
        Browser["Browser"]
        Node["Node.js"]
        Tauri["Tauri"]
    end

    subgraph Build["Build Tools"]
        Cargo["Cargo"]
        Vite["Vite"]
        WasmPack["wasm-pack"]
        Turborepo["Turborepo"]
    end

    subgraph Formats["Data Formats"]
        STEP["STEP (IFC4)"]
        IFCX["IFCX (IFC5)"]
        Parquet["Apache Parquet"]
        Cache["Binary Cache"]
    end

    Rust --> WASM
    Rust --> Tauri
    TS --> Browser
    TS --> Node
    WGSL --> WebGPU

    style Languages fill:#6366f1,stroke:#312e81,color:#fff
    style Runtime fill:#10b981,stroke:#064e3b,color:#fff
    style Build fill:#f59e0b,stroke:#7c2d12,color:#fff
    style Formats fill:#a855f7,stroke:#581c87,color:#fff
Loading

Next Steps