@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
- Viewer: direct loading of
Duplex.ifc, or of your own IFC file by file dialog or drag and drop, with the object tree and the property sets of a clicked object. - Metadata + XKT:
the tree and types from
loadMetadata(), the geometry fromXKTLoaderPlugin, and the property sets of a clicked object.
Two ways to use it
IFC is the only file format involved; how much work happens in the browser is your choice:
| Geometry | Metadata, properties | Work in the browser | Pick it when | |
|---|---|---|---|---|
| 1. Direct | the IFC | the same IFC | parse + full geometry incl. booleans and opening cuts | files come from users, nothing can be prepared |
| 2. Metadata only | XKT or glTF from the xeoIFC converter | an IFC, or the metadata IFC of the converter | parse only | large 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?)
| Option | Default | |
|---|---|---|
id | "XeoIFCLoader" | Plugin ID in viewer.plugins |
workerSrc | package-relative | URL of xeoifc.worker.js, see "Without a bundler" |
includeTypes, excludeTypes, globalizeObjectIds | Defaults for the load parameters of the same name |
load(params) returns SceneModel
| Parameter | Default | |
|---|---|---|
id | generated | SceneModel / MetaModel ID |
src | file | data | URL, File/Blob, or ArrayBuffer. .ifc or .ifczip is detected from the content | |
includeTypes / excludeTypes | Filter objects by type, eg. ["IfcSpace"] | |
globalizeObjectIds | false | Prefix object IDs with the model ID, to load the same file twice |
reuseGeometries | true | false shares no geometry between meshes (as in XKTLoaderPlugin): fewest draw calls, most memory |
maxReusedGeometries | 500 | Upper bound of shared geometries, see "Frame rate" |
circleSegments | 18 | Tessellation segments of a full circle (4..64) |
onProgress | (phase, done, total); phases "read" (MB), "parse", "geom", "pack", "edges" (worker) and "scene" (SceneModel build) | |
| anything else | Passed to the SceneModel constructor: edges, dtxEnabled, origin, position, rotation, scale, backfaces, saoEnabled, ... |
loadMetadata(params) returns Promise<{metaObjects}>
| Parameter | Default | |
|---|---|---|
id | required | ID of the model the metadata belongs to: the id given to the loader of the geometry |
src | file | data | URL, File/Blob, or ArrayBuffer of an .ifc / .ifczip: a full IFC or a metadata IFC | |
includeTypes / excludeTypes | Filter objects by type; set the same filter on the geometry loader | |
globalizeObjectIds | false | Prefix 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.js | The plugin (main thread) |
src/xeoifc.worker.js | Module worker hosting the engine |
src/packEdges.js | Edge indices of the packed meshes (worker) |
wasm/viewer_wasm*.{js,wasm} | xeoIFC engine modules, each fetched on first use |
examples/metadata-with-xkt.html | Metadata-only page (no bundler) |
index.d.ts | TypeScript declarations |
LICENSE.txt | xeoIFC 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.