@xeofoundry/xeoifc-loader-plugin

xeokit Viewer plugin that loads IFC (.ifc, .ifczip) files directly in the browser - with their geometry, or as the metadata of a model whose geometry comes from XKT or glTF. The xeoIFC engine runs as a WebAssembly module in a Web Worker, parses and tessellates the file and hands one packed geometry buffer plus an object tree to the plugin, which builds a xeokit SceneModel and MetaModel from them. No conversion step, no server, no upload.

Documentation and live examples: https://xeofoundry.github.io/xeoIFC/xeokit-direct-ifc-loading/

Live examples

Two ways to use it

IFC is the only file format involved; how much work happens in the browser is your choice:

GeometryMetadata, propertiesWork in the browserPick it when
1. Directthe IFCthe same IFCparse + full geometry incl. booleans and opening cutsfiles come from users, nothing can be prepared
2. Metadata onlyXKT or glTF from the xeoIFC converteran IFC, or the metadata IFC of the converterparse onlylarge models: geometry as XKT, IFC as the metadata format instead of a JSON metamodel

1 is load() (Usage); 2 is loadMetadata() next to the xeokit loader of the geometry. In both, objects are identified by IFC GlobalId and getProperties() reads property sets on demand from the IFC kept in the engine.

Metadata only

The converter writes geometry and metadata from one IFC. With an .ifc or .ifczip metadata path, the metadata file is the input IFC without its geometry (Duplex.ifc: 2.4 MB become 0.6 MB, 0.15 MB as .ifczip), not a JSON metamodel:

xeoifc -i model.ifc -o model.xkt -m model.meta.ifc
import {Viewer, XKTLoaderPlugin} from "@xeokit/xeokit-sdk";
import {XeoIFCLoaderPlugin} from "@xeofoundry/xeoifc-loader-plugin";

const xktLoader = new XKTLoaderPlugin(viewer);
const xeoIFCLoader = new XeoIFCLoaderPlugin(viewer);

// Parses the IFC, builds no geometry: tree and types as xeokit metamodel JSON
const metaModelJSON = await xeoIFCLoader.loadMetadata({id: "myModel", src: "model.meta.ifc"});

// Geometry from XKT; same model ID, same object IDs (IFC GlobalId)
const sceneModel = xktLoader.load({id: "myModel", src: "model.xkt", metaModelJSON});

// Property sets on demand, as after load()
const propertySets = await xeoIFCLoader.getProperties(objectId);

loadMetadata() also accepts the full IFC, so the original file can serve as the metadata of an XKT made earlier. The result is plain xeokit metamodel JSON ({metaObjects: [{id, name, type, parent}]}): any xeokit loader with a metaModelJSON parameter takes it (XKTLoaderPlugin, GLTFLoaderPlugin, ...), and TreeViewPlugin and friends work as usual. Property sets are not part of it; they stay in the engine and are decoded per object by getProperties().

Objects are matched by GlobalId. When a file uses one GlobalId for several objects, the converter names the repeats <GlobalId>_<n> in its XKT / GLB output; the engine applies the same rule in the same order, so loadMetadata() and load() return those object IDs too and every object finds its own metadata and properties.

Runnable page: examples/metadata-with-xkt.html.

Install

npm install @xeofoundry/xeoifc-loader-plugin @xeokit/xeokit-sdk

@xeokit/xeokit-sdk is a peer dependency: the plugin uses the same xeokit instance as your application.

Usage

import {Viewer, TreeViewPlugin} from "@xeokit/xeokit-sdk";
import {XeoIFCLoaderPlugin} from "@xeofoundry/xeoifc-loader-plugin";

const viewer = new Viewer({canvasId: "myCanvas"});
new TreeViewPlugin(viewer, {containerElement: document.getElementById("treeViewContainer")});

const xeoIFCLoader = new XeoIFCLoaderPlugin(viewer);

const sceneModel = xeoIFCLoader.load({
    id: "myModel",
    src: "Duplex.ifc",                  // or file: aFile, or data: anArrayBuffer
    edges: true,
    excludeTypes: ["IfcSpace"],
    onProgress: (phase, done, total) => console.log(phase, done, total)
});

sceneModel.on("loaded", () => viewer.cameraFlight.jumpTo(sceneModel));
sceneModel.on("error", (message) => console.error(message));

// Property sets are queried on demand from the model retained inside the WebAssembly engine
viewer.cameraControl.on("picked", async (pickResult) => {
    const propertySets = await xeoIFCLoader.getProperties(pickResult.entity.id);
    // [{pset: "Pset_WallCommon", props: [{name: "IsExternal", value: "T", unit: null}, ...]}, ...]
});

new XeoIFCLoaderPlugin(viewer, cfg?)

OptionDefault
id"XeoIFCLoader"Plugin ID in viewer.plugins
workerSrcpackage-relativeURL of xeoifc.worker.js, see "Without a bundler"
includeTypes, excludeTypes, globalizeObjectIdsDefaults for the load parameters of the same name

load(params) returns SceneModel

ParameterDefault
idgeneratedSceneModel / MetaModel ID
src | file | dataURL, File/Blob, or ArrayBuffer. .ifc or .ifczip is detected from the content
includeTypes / excludeTypesFilter objects by type, eg. ["IfcSpace"]
globalizeObjectIdsfalsePrefix object IDs with the model ID, to load the same file twice
reuseGeometriestruefalse shares no geometry between meshes (as in XKTLoaderPlugin): fewest draw calls, most memory
maxReusedGeometries500Upper bound of shared geometries, see "Frame rate"
circleSegments18Tessellation segments of a full circle (4..64)
onProgress(phase, done, total); phases "read" (MB), "parse", "geom", "pack", "edges" (worker) and "scene" (SceneModel build)
anything elsePassed to the SceneModel constructor: edges, dtxEnabled, origin, position, rotation, scale, backfaces, saoEnabled, ...

loadMetadata(params) returns Promise<{metaObjects}>

ParameterDefault
idrequiredID of the model the metadata belongs to: the id given to the loader of the geometry
src | file | dataURL, File/Blob, or ArrayBuffer of an .ifc / .ifczip: a full IFC or a metadata IFC
includeTypes / excludeTypesFilter objects by type; set the same filter on the geometry loader
globalizeObjectIdsfalsePrefix object IDs with the model ID; set it on the geometry loader too
onProgress(phase, done, total); phases "read" (MB) and "parse"

No geometry is built and no CSG runs; the engine keeps the parsed file for getProperties() until the model with this ID is destroyed or unloadMetadata(id) is called.

IFC objects get their IFC GlobalId as Entity#id and MetaObject#id, like the other xeokit loaders; objects without a GlobalId get <modelId>#<entity id>. IfcSpaces load hidden. Geometry used more than once is instanced. The model is converted from Z-up to xeokit's Y-up and placed with a double-precision origin, so georeferenced files render without jitter.

The engine in this package is an evaluation build: it adds an "Evaluation version" text to every model, flat on the ground just outside the footprint (25% of the model width, the same mark the xeoIFC command-line converter writes without a licence key). The text is one more object of the model, <modelId>#4294967295, and is not affected by includeTypes / excludeTypes.

The worker reads the file as a stream and feeds IFC text to the engine chunk by chunk, so files larger than the 2 GB ArrayBuffer limit load; the practical limits are the 4 GB wasm heap of the engine and what xeokit and the GPU hold for the resulting triangles.

The worker also computes the edge indices of every mesh (edges sharper than 30 degrees, like the xeoIFC viewer), because xeokit's own edge generation on the main thread dominated the load time of large models. The SceneModel is then built in 40 ms time slices, so the page stays responsive and onProgress keeps reporting.

Frame rate

xeokit draws every shared (instanced) geometry with a draw call of its own in each render pass, so a model with thousands of reused geometries becomes draw-call bound, while sharing nothing multiplies the vertex data. The plugin therefore shares only the maxReusedGeometries geometries whose sharing saves the most vertices and batches the rest. Measured on a stadium model with 670 k instances, 76 M triangles and 7177 reused geometries (RTX 4070 Laptop, edges on): all shared 194 ms per frame, 500 shared (default) 50 ms, none shared 250 ms.

Several models can be loaded side by side (loads run one after the other in the worker). The engine keeps each parsed file for getProperties() and frees it when its model is destroyed (or its metadata unloaded); once no model of the plugin is left the engine is cleared as a whole. plugin.destroy() terminates the worker.

Bundlers

The package is plain ES modules. The plugin starts its worker with new Worker(new URL("./xeoifc.worker.js", import.meta.url)) and the worker fetches the .wasm files the same way, which Vite, webpack 5, Rollup, Parcel and esbuild-based setups resolve on their own.

Verified with Vite 8 (dev server and production build) without any configuration. If an older Vite dev server loses the worker or wasm URLs during dependency pre-bundling, exclude the package from it:

// vite.config.js
export default {
    optimizeDeps: {exclude: ["@xeofoundry/xeoifc-loader-plugin"]}
};

Without a bundler

Serve the package folder (src/ and wasm/ next to each other) from your own origin and map the xeokit import:

<script type="importmap">
    {"imports": {"@xeokit/xeokit-sdk": "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk@2.6.114/dist/xeokit-sdk.min.es.js"}}
</script>
<script type="module">
    import {Viewer} from "@xeokit/xeokit-sdk";
    import {XeoIFCLoaderPlugin} from "./xeoifc-loader-plugin/src/index.js";
</script>

Browsers only start same-origin workers, so the plugin itself cannot be loaded from a CDN as is. If the files must live on another origin, host a same-origin copy of src/xeoifc.worker.js (with its two imports pointing at the wasm location) and pass it as workerSrc. The server has to send .wasm as application/wasm.

Package contents

Path
src/XeoIFCLoaderPlugin.jsThe plugin (main thread)
src/xeoifc.worker.jsModule worker hosting the engine
src/packEdges.jsEdge indices of the packed meshes (worker)
wasm/viewer_wasm*.{js,wasm}xeoIFC engine modules, each fetched on first use
examples/metadata-with-xkt.htmlMetadata-only page (no bundler)
index.d.tsTypeScript declarations
LICENSE.txtxeoIFC Testing License

Licence

The whole package, including the plugin JavaScript and the xeoIFC engine in wasm/, is under the xeoIFC Testing License (LICENSE.txt): use for testing and evaluation. The package takes no licence key, so the engine adds the "Evaluation version" text described above to every model.