@aectooling/slopifc-wasm
v0.2.0
Published
WebAssembly bindings for slopifc: IFC parsing, metadata and render data for browsers and Node
Readme
@aectooling/slopifc-wasm
WebAssembly bindings for slopifc: IFC parsing, typed metadata and Three.js-shaped render data, for browsers and Node.
The package is the only supported browser-facing interface to slopifc. Applications never instantiate the module, manage linear memory, free strings or call raw exports.
Status: runtime, conversion, metadata, semantic indexes, render data and Three.js adapter.
createSlopIfc(), isolated model instances, typed lazy entities, projected metadata, file conversion, retained zero-copy semantic and render tables, Three.js wrappers and deterministic disposal are included in the package.
The current generated runtime accepts IFC2X3 (IFC2X3, IFC2X3TC1), IFC4
(IFC4, IFC4ADD2, IFC4ADD2TC1), and IFC 4.3 ADD2 (IFC4X3_ADD2). Other
schema releases are rejected at open time until their generated metadata tables
are added.
Contract
The normative contract — integer widths, ownership, prepare() defaults, the
model state machine, the close/detachment sequence, status codes and every
byte-exact descriptor layout — is
docs/spec-wasm-package-abi.md.
Current usage
Convert an IFC source into JavaScript-owned Blobs suitable for download. OBJ
returns its referenced MTL file explicitly as sidecar.
format: 'glb' produces a single model/gltf-binary Blob with metre coordinates,
+Y up, shared meshes, RGBA materials and IFC identity extras; see the
GLB contract.
const converted = await runtime.convert(file, {
format: 'obj',
geometry: { scopes: ['building'], subtractOpenings: true },
})
function save(output: { name: string; blob: Blob }) {
const url = URL.createObjectURL(output.blob)
const link = document.createElement('a')
link.href = url
link.download = output.name
link.click()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
save(converted.output)
if (converted.sidecar) save(converted.sidecar)An open model can be converted repeatedly before preparation:
import { createSlopIfc } from '@aectooling/slopifc-wasm'
const runtime = await createSlopIfc()
const model = await runtime.open(file)
const json = await model.convert({ format: 'json' })
// Selection affects emitted geometry; dependencies remain available.
const selected = await model.convert({
format: 'glb',
geometry: { expressIDs: [100n, 4294967396n], entityTypes: ['IfcElement'] },
})
const prepared = await model.prepare({
render: {
coordinateToOrigin: true,
subtractOpenings: true,
// Use false only when the renderer derives flat face normals.
includeNormals: true,
},
metadata: {
entityTypes: ['IFCPRODUCT'],
includeSubtypes: true,
attributes: ['GlobalId', 'Name', 'OwnerHistory'],
},
semanticIndex: true,
})
const names = prepared.getMetadata()?.column('Name')
const render = prepared.renderData
const relations = prepared.getSemanticIndex()
const wall = model.getEntities().byGuid('2Ab$4Ad$L9QuMOSyGoOAgE')
console.log(names?.kind === 'utf8' ? names.get(0) : undefined)
console.log(render?.geometries.count, render?.instances.count)
console.log(relations?.edgesForEntityIndex(wall?.getEntityIndex() ?? -1))
console.log(wall?.getExpressID(), wall?.getTypeName())
prepared.dispose()The semantic relation index is disabled by default. When requested, its sparse columns and edge ranges borrow sealed WASM memory just like render and metadata tables. They detach when the prepared model is disposed.
Geometry conversion uses the same ID/type/scope intersection as preparation.
Type names form a union and include subtypes by default; set includeSubtypes:
false for exact types. Omitted filters select all scoped products; explicit
empty filters select none and never trigger standalone-solid fallback. STEP/GLB
report the existing no-geometry error when selection emits nothing. Selection
adds no lookup table when omitted. Selected results retain complete-file opening,
material and placement dependencies, and conversion leaves the model open.
Lazy binary attributes are returned as
{ kind: 'binary', value: Uint8Array, bitLength: number }. The package copies
the bytes out of the transient WASM decode buffer, so the value remains owned
by JavaScript.
Viewer-style loads can return JavaScript-owned buffers from a parser worker and report real phase boundaries along the way:
const snapshot = await runtime.prepareSnapshot(file, {
render: { includeNormals: false },
includeProductStructure: true,
includeElementData: true,
metadata: {
entityTypes: ['IFCPRODUCT'],
includeSubtypes: true,
attributes: ['GlobalId', 'Name'],
},
onProgress({ phase, elapsedMs, entityCount }) {
console.log(phase, elapsedMs, entityCount)
},
})For an interactive viewer, keep a parser session alive and leave element data
out of the initial snapshot. getElementData() decodes only the selected
element, follows its assigned type, and caches the completed result plus shared
property definitions and materials:
const session = await runtime.openSession(file)
const snapshot = await session.prepareSnapshot({
render: { includeNormals: false },
includeProductStructure: true,
includeElementData: false,
metadata: {
entityTypes: ['IFCPRODUCT'],
includeSubtypes: true,
attributes: ['GlobalId', 'Name'],
},
})
showModel(snapshot.renderData)
onElementSelected(async (expressID) => {
const element = await session.getElementData(expressID)
showProperties(element)
})
onModelClosed(() => {
snapshot.dispose()
session.dispose()
})await session.getMemoryStats() reports source ownership, record indexes,
semantic-index bytes, render allocations, shared native caches, and WASM page
capacity from the model's owning thread. It works before or after preparation
and rejects after disposal. WASM pages include native allocations and allocator
slack; do not add the individual allocation counters to the page total.
The returned IfcModelSessionMemoryStats does not include JavaScript objects,
the caller's source buffer, an owned snapshot, or worker isolate overhead.
session.getCacheMetrics() separately reports element, definition, and material
cache entries and hits. By default these caches grow with selections until disposal.
Use await session.clearCache() after preparation to release them while keeping
the session open. Earlier queries finish before clearing; later queries use fresh
caches, and already returned snapshots remain valid. Hit/miss counters remain
cumulative.
runtime.openSession(source, { maxCacheEntries: 256 }) optionally caps each
element, definition and material cache separately, evicting the oldest inserted
entry. Zero disables those three caches; project units remain cached until
clearing/disposal. These are entry limits, not byte limits, and exclude in-flight
requests and caller-owned snapshots. Low caps may increase repeated decoding.
The session performs no property prefetch. Concurrent and repeated requests for
one Express ID share the same promise and result object while cached. Failed requests leave
the cache so the caller can retry. getCacheMetrics() reports element,
definition, and material hits for development checks and benchmarks.
includeProductStructure owns product-to-storey and related-type assignments.
includeElementData owns occurrence and type property sets, authored quantities,
and semantic material assignments. Both are plain worker-safe records that remain
available after the parser closes.
The phases cover worker initialization, record indexing, model structure, geometry preparation, buffer copying, and transfer. They are honest phase boundaries, not estimated per-entity completion percentages.
Compile-checked usage examples live in examples/.
Layout
| Path | Contents |
| --- | --- |
| src/index.ts | public API barrel |
| src/index-internal.ts | internal barrel for tests; not in exports |
| src/abi/ | private ABI descriptors, status codes and checked memory views |
| src/core/ | runtime/model/entity API, option validation and value decoding |
| src/tables/ | render, metadata and semantic-index descriptor adapters |
| src/snapshot/ | owned snapshots, retained sessions and lazy element data |
| src/parser.worker.ts | worker entry point for snapshots and sessions |
| src/three/ | optional adapter, published as @aectooling/slopifc-wasm/three |
| tests/fixtures/ | hand-built descriptor fixtures (no WASM required) |
The core never imports three; it is an optional peer dependency of the
/three subpath only.
Curve sampling
Pass render: { curveQuality: { chordErrorMetres: 0.001 } } to prepare(), a
snapshot or a session for opt-in physical-unit curve sampling. Optional
maxAngleRadians and maxSegments default to 0.1 and 4096. The cap is per
arc/sweep; exceeding it fails rather than relaxing the requested sampling.
Omit curveQuality to preserve existing output. Use geometry diagnostics to
observe unsupported or malformed input. Existing cleanup and float32 limits
still apply, so this is not a certified final-mesh error bound. These options
do not configure browser convert().
The CLI supports --curve-chord-error-metres, --curve-max-angle-radians and
--curve-max-segments for OBJ, GLB, STEP and DXF. The last two require an explicit
chord error; the default CLI output is unchanged.
Development
await model.querySemantics([42, 18446744073709551615n]) returns owned authored
properties, quantities, materials and units with provenance. Call it before
prepare() seals the model. The result survives sealing and closure; IDs and
integer values are decimal strings. See the shared semantic contract.
bun install
bun run typecheck
bun run test
bun run build # clean dist, build/copy WASM, emit JS and declarations
bun run check:pack # pack a throwaway tarball and verify its contents
bun run check # all of the abovezig build wasm stays a Zig-only step writing to zig-out/wasm/slopifc.wasm;
copying it into dist/ is the package build's job. Set
SLOPIFC_SKIP_ZIG_BUILD=1 to reuse an artifact a previous step already built.
Package builds omit DWARF debug sections from the copied WASM download, keeping
the compiler artifact, executable sections and function names unchanged. Set
SLOPIFC_WASM_KEEP_DEBUG=1 to retain DWARF in the package for source debugging.
Builds regenerate dist/ so deleted or renamed source modules cannot survive
in the package. The pack check rejects emitted modules without matching source
files, as well as missing public entry points or unexpected assets.
await model.getGeoreferencing() returns owned map metadata and explicit f64
matrices. Call it before prepare(); it does not change rendering. Check each
operation's status and use its representation context to choose the transform.
See georeferencing and unit composition.
