All showcases

A browser IFC viewer on the xeoIFC WebAssembly library

xeoIFC Web is an IFC viewer that runs in the browser. It loads large IFC files and many files at once, shows them in 3D with the model tree and the properties, finds, measures and cuts, checks BCF, IDS and clashes, and writes a selection or several models to a new IFC file.

The IFC work is done by the xeoIFC WebAssembly library, the browser form of xeoifc.dll. Its engine runs in a Web Worker: it reads the files, tessellates them, answers queries and writes IFC. Its renderer draws with WebGPU into a canvas on the page. Nothing is uploaded: the files stay on your computer. This page shows the calls and what the library can do.

1. The application

This is the viewer itself, not a screenshot: xeoIFC Web has loaded the Viadotto Acerno bridge (IFC 4.3). Rotate, zoom and pan with the mouse, click elements to select them.

Move the pointer to the left edge of the viewer to open the model tree and the properties. Open in the full viewer

In the full viewer, Drag&Drop .ifc or .ifczip files from your machine to render them locally (no upload of any IFC data).

xeoIFC Web supports

2. Download the WebAssembly library

Download xeoifc-wasm.zip from the releases page for integration into your own application. It contains the prebuilt WebAssembly modules, generated JavaScript bindings, TypeScript declarations, package metadata, licence, third-party notices and a short integration README.

The xeoIFC Web viewer application is not included. To view models directly, use the hosted viewer.

3. Using the library

The library is one module with two faces: engine functions that a Web Worker calls, and the Viewer class on the page. Control is JSON; the model crosses from the worker to the page as one transferable buffer. The excerpts are from src/worker.ts, the whole bridge to the engine, and src/main.ts, shortened.

Load a file in the worker

The worker is a second instance of the same module. It gets the file in chunks and gives each chunk to the engine at once, so the text is never whole in memory. worker_load_finish tessellates the model and returns it as one packed buffer.

// src/worker.ts
import init, { worker_load_begin, worker_load_chunk, worker_load_finish } from './pkg/viewer_wasm';

const post = self.postMessage.bind(self) as (msg: unknown, transfer?: Transferable[]) => void;
const ready = init();

const onProgress = (phase: string, done: number, total: number) => {
  post({ type: 'progress', phase, done, total });                 // phases: "parse", "geom", "pack"
};
const onSnapshot = (packed: Uint8Array) => {
  post({ type: 'snapshot', packed }, [packed.buffer]);            // the scene so far, while the file loads
};

self.onmessage = async (e: MessageEvent<WorkerRequest>) => {
  await ready;
  const msg = e.data;
  switch (msg.type) {
    case 'load-begin':
      worker_load_begin(msg.circleSegments, msg.renderSharpEdges, msg.clearExisting, msg.orphanedElementsMode);
      break;
    case 'load-chunk':
      worker_load_chunk(new Uint8Array(msg.buffer));
      post({ type: 'chunk-ok' });                                 // the page can send the next chunk
      break;
    case 'load-finish': {
      const out = worker_load_finish(onProgress, onSnapshot);     // {packed, tree, sourceId, lineLayers, ..}
      post({ type: 'loaded', packed: out.packed, tree: out.tree, sourceId: out.sourceId }, [out.packed.buffer]);
      break;
    }
  }
};

The page reads the file as a stream and sends it in chunks of 8 MB. Each chunk is transferred, not copied.

// src/main.ts (ParserWorker.loadStream)
worker.postMessage({
  type: 'load-begin',
  circleSegments: settings.circleSegments,                        // 18
  clearExisting,                                                  // true: replace the loaded models
  orphanedElementsMode: settings.orphanedElementsMode,            // 'ignore', 'ifc-project' or 'ifc-site'
  renderSharpEdges: settings.renderSharpEdges,
});
const CHUNK = 8 * 1024 * 1024;
const reader = source.stream().getReader();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  queued.push(value);
  queuedBytes += value.length;
  if (queuedBytes >= CHUNK) await flush();   // one ArrayBuffer: postMessage({ type: 'load-chunk', buffer }, [buffer]),
}                                            // then wait for 'chunk-ok'
await flush();
worker.postMessage({ type: 'load-finish' });

Draw into a canvas

The renderer uses wgpu on WebGPU and draws into a canvas of the page. It draws only when something changed: when the view does not move, frame() returns false at once.

// src/main.ts
import init, { Viewer } from './pkg/viewer_wasm';

await init();
viewer = await Viewer.create(canvas);                             // canvas.width and height in device pixels

// While the file loads: the snapshots from the worker
const onSnapshot = (packed: Uint8Array): void => {
  if (clearExisting) viewer.set_packed_scene(packed);
  else viewer.preview_append_scene(packed);                       // over the models that are already loaded
};

// The loaded model. The result pairs each entity id with a viewer handle for picking, selection and visibility.
const elementHandles = viewer.set_packed_scene(result.packed);   // [entity id, handle, entity id, handle, ..]

// Mouse: viewer.orbit, pan_start / pan_to / pan_end, zoom_pick. Resize: viewer.resize(width, height, devicePixelRatio).
const loop = (): void => {
  viewer.frame();
  requestAnimationFrame(loop);
};
requestAnimationFrame(loop);

Pick an element and show its properties

// src/main.ts: a click at (x, y) in device pixels, like the canvas
const rect = canvas.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) * devicePixelRatio);
const y = Math.round((e.clientY - rect.top) * devicePixelRatio);
const id = await viewer.pick(x, y);                               // the viewer handle, or undefined for the background
if (id !== undefined) {
  viewer.select(id, e.ctrlKey || e.metaKey);                      // ctrl adds to the selection
  const source = elementSources.get(id);                          // { sourceId, entityId }, from the handle pairs
  // {"type":"IfcWall","globalId":"2C1kfNlluuvc_DT_K$K6Bj","name":"Front wall between the rooms",..}
  const info = await parser.query('element_info', source.sourceId, source.entityId);
  // [{"pset":"Pset_WallCommon","props":[{"name":"IsExternal","value":"T","unit":null}]}]
  const properties = await parser.query('properties', source.sourceId, source.entityId);
}

// src/worker.ts: the 'query' message
value = msg.method === 'element_info'
  ? worker_element_info(msg.sourceId, msg.id)
  : worker_properties(msg.sourceId, msg.id);
post({ type: 'result', reqId: msg.reqId, value });

Query and export

The document API is one function, worker_call: a JSON request in, a JSON response out, with the same operations and error codes as xeoifc_call of the native library and the command-line tool. The loaded models are the documents s1, s2, ... These calls find all external walls and write three of them to a new IFC file.

// src/worker.ts: the 'call' message
post({ type: 'result', reqId: msg.reqId, value: worker_call(msg.request) });

// src/main.ts
const found = await parser.call({ op: 'query.select', doc: 's1', args: {
  text: 'is IfcWall and Pset_WallCommon.IsExternal = true',
  select: ['name', 'storey.Name'], includeRefs: true, limit: 1000 } });
// {"ok":true,"result":{"count":14,"items":[{"name":"Front wall between the rooms","storey.Name":"Ground floor"},..],
//   "refs":["#1022","#1113","#1218",..],..},..}

The viewer keeps only metadata after tessellation. So the export reads the file again, in a worker of its own that ends afterwards. More files before export-run merge the models.

// src/main.ts (runIfcExport). selection = entity ids ("#1022" is 1022); empty = the whole file.
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.postMessage({ type: 'export-reset' });
worker.postMessage({ type: 'export-chunk', buffer }, [buffer]);  // the source file again, in chunks of 8 MB
worker.postMessage({ type: 'export-file-done', selection: Uint32Array.of(1022, 1113, 1218) });
worker.postMessage({ type: 'export-run', licenceKey, bounds });
// 'export-done' brings the IFC file, then worker.terminate()

// src/worker.ts
case 'export-reset':     worker_export_reset(); break;
case 'export-chunk':     worker_export_chunk(new Uint8Array(msg.buffer)); post({ type: 'chunk-ok' }); break;
case 'export-file-done': worker_export_file_done(msg.selection); post({ type: 'export-file-ok' }); break;
case 'export-run': {
  const out = worker_export_run(msg.licenceKey, Date.now() / 1000, msg.bounds ? Float64Array.from(msg.bounds) : undefined);
  post({ type: 'export-done', data: out.data, warnings: out.warnings, entities: out.entities }, [out.data.buffer]);
  break;
}

The same in a script

The Script panel (in the Lab menu) runs JavaScript with the xeo API of xeoifc run, the API that made the villa, in a sandboxed worker. Document calls go to worker_call, and their changes reach the model only on Apply; xeo.viewer calls go to the Viewer. These two lines colour the external walls red:

const walls = await xeo.query.select({ text: 'is IfcWall and Pset_WallCommon.IsExternal = true',
  select: ['name', 'storey.Name'], includeRefs: true, limit: 1000 });
await xeo.viewer.color({ refs: walls.refs, color: 'red' });

4. What the library can do

Document API111 operations in 27 groups through worker_call: queries, entities, property and quantity sets, materials, classifications, types, spatial structure, placements, new elements with openings, transactions with undo and redo, validation, diff and merge. api.describe returns the whole API as JSON or as TypeScript declarations. A wrong call gets an answer with a hint, for example the allowed values of an enumeration.
Queriesis IfcWall and Pset_WallCommon.IsExternal = true and storey.Name = "Level 1", with columns such as name, globalId, Pset_WallCommon.FireRating or storey.Name
Files.ifc and .ifczip, IFC 2x3, IFC 4 and IFC 4.3, streamed in chunks. Separate modules, fetched on first use: JT, STEP CAD, point clouds (LAS, LAZ, COPC, E57, PLY, XYZ, PTS) and meshes (OBJ, STL, PLY, RVM, LandXML)
Split and mergeworker_export_run writes a selection, a whole model or several models to one IFC file, with the same writer as the command-line tool and the native library
LoadingIn a Web Worker, with progress, a preview while the file loads, and cancel. Any number of files into one scene. After tessellation the model is pruned to its metadata, so large models fit into the 4 GiB of a wasm instance.
Rendererwgpu on WebGPU, into a canvas. Many models in one scene, each with its own placement. Pick, rectangle pick and hover; selection, visibility, transparency, colours and clip planes. Camera: orbit about the point under the cursor, anchored pan, zoom to the cursor, fit, view directions, orthographic, save and restore. Sharp edges, 5 drafting line layers, point clouds and terrain.
InterfaceOne ES module with TypeScript declarations: 22 engine functions and the Viewer class with 76 methods. No framework.
ApplicationxeoIFC Web, TypeScript: model tree, properties, search, measure, section planes, 2D section drawings, storey shift, minimap, walk and saved views, BCF, IDS, clash check, compare revisions, properties as a table, GIS terrain, GLB and XKT readers. Lab: heat flow, script editor, AI assistant with your own API key.
BrowsersChrome and Edge 113 or newer. On Linux, WebGPU can need chrome://flags/#enable-unsafe-webgpu.

5. Licence

The xeoIFC WebAssembly library is licensed under the xeoIFC Testing License: you can use and redistribute it unmodified, in binary form and free of charge, solely for testing and evaluation. For commercial or production use, contact xeoFoundry. All functions work without a licence key. Without a key, each IFC file that the library writes gets one more element: an IfcBuildingElementProxy with the text "xeoIFC evaluation version" next to the model. A licence key from xeoFoundry removes it. In xeoIFC Web, enter the key in the Settings panel. In your own worker, set the key for the saves of worker_call and give it to each export:

// saves of worker_call; returns {"state":"licensed"|"evaluation"|"invalid"|"expired","expires":..}
worker_set_licence_key('YOUR-KEY', Date.now() / 1000);
// exports: the key is the first argument
worker_export_run('YOUR-KEY', Date.now() / 1000, bounds);