// ifcviewer.js — a small JavaScript integration layer over the Emscripten
// module (IfcViewerWeb.js). Load this AFTER IfcViewerWeb.js, which defines the
// global `createIfcViewer` factory.
//
//
//
//
//
// The canvas element MUST have id="viewer-canvas" — the wasm side hard-codes
// that selector for its WebGPU surface and input handlers.
//
// Model identity. addFile/addUrl return a source id: the handle for that model,
// minted the moment it is registered and stable for the session. Objects come
// back tagged with both their `sourceId` and a `model` index (the model's slot
// in load order). Map an object to the file it came from through the source id
// — the index is a POSITION, so it shifts down if an earlier model fails to
// load, and a host keying its own list off it then attributes objects to the
// wrong file.
//
// Object identity. Everything the scripting API takes or returns is keyed by
// `objectId`: a u32 the renderer assigns, unique across the federation but only
// meaningful for this session. IFC GlobalIds are the stable identity, and every
// id-taking call also accepts them — resolved through the element table that
// getObjects() fetches. Because that fetch is lazy (per model, so first paint
// never waits on it), a GUID can only be resolved once getObjects() has
// resolved at least once; passing one before that throws rather than silently
// selecting nothing.
(function (global) {
'use strict';
// Resolve a remote sidecar's total size so the loader can bound its ranged
// reads: HEAD Content-Length, falling back to a 0-0 Range's Content-Range.
async function sizeUrl(url) {
const head = await fetch(url, { method: 'HEAD' });
const len = head.ok ? parseInt(head.headers.get('Content-Length') || '0', 10) : 0;
if (len > 0) return len;
const probe = await fetch(url, { headers: { Range: 'bytes=0-0' } });
const cr = probe.headers.get('Content-Range'); // "bytes 0-0/12345"
return cr ? parseInt(cr.split('/')[1] || '0', 10) : 0;
}
// Mouse navigation schemes the wasm's classifyPress understands. Named so a
// typo is an error here rather than a silent fall-back to blender in the core.
const NAV_PRESETS = ['blender', 'rhino', 'revit', 'web'];
// Pack a colour into the u32 the shader unpacks: 0xAABBGGRR. Accepts
// '#rgb' / '#rrggbb' / '#rrggbbaa', or {r, g, b, a} with 0-255 channels
// (alpha defaulting to opaque). An alpha of 0 is the "no override" sentinel
// on the wasm side, so a fully transparent colour is a clear, not a colour —
// hide the object instead if that is what you meant.
function packColor(color) {
if (color === null || color === undefined) return 0;
let r, g, b, a = 255;
if (typeof color === 'string') {
let hex = color.replace(/^#/, '');
if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');
if (hex.length !== 6 && hex.length !== 8) throw new Error('bad colour: ' + color);
r = parseInt(hex.slice(0, 2), 16);
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
if (hex.length === 8) a = parseInt(hex.slice(6, 8), 16);
} else {
r = color.r | 0; g = color.g | 0; b = color.b | 0;
if (color.a !== undefined) a = color.a | 0;
}
const clamp = function (v) { return Math.max(0, Math.min(255, v | 0)); };
return ((clamp(a) << 24) | (clamp(b) << 16) | (clamp(g) << 8) | clamp(r)) >>> 0;
}
// Boot a viewer bound to `opts.canvas`. Resolves to the API object once the
// wasm runtime is initialised; `api.ready` resolves once the GPU app is live.
async function create(opts) {
opts = opts || {};
const factory = opts.moduleFactory || global.createIfcViewer;
if (typeof factory !== 'function') {
throw new Error('createIfcViewer not found — load IfcViewerWeb.js first');
}
const selectListeners = [];
const selectionListeners = [];
const modelLoadedListeners = [];
let api = null; // built below; the RAF loop only reads it after that
let live = false;
let resolveReady;
const ready = new Promise(function (r) { resolveReady = r; });
// Both built from the last getObjects(); null until then. objectIndex backs
// GlobalId resolution (see the identity note at the top of the file);
// objectsById puts the IFC identity back onto a selection read.
let objectIndex = null;
let objectsById = null;
// The per-frame loop: poll for the app pointer (published once the GPU
// device is ready), then drive the C tick. It is registered from
// onRuntimeInitialized (a clean callback context) rather than after
// `await factory(...)` — that Promise.then continuation is exactly the
// nesting that stalls Dawn-web's device callback and leaves the GPU device
// half-initialised (every buffer then reports "invalid"). Learned during
// the original web bring-up; kept here deliberately.
function startLoop(Module) {
function tick() {
if (Module._app_ptr && Module._raf_tick_c) {
if (!live) {
live = true;
resolveReady(api);
if (opts.onReady) opts.onReady(api);
}
Module._raf_tick_c(Module._app_ptr);
if (opts.onFrame) opts.onFrame(api);
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
const Module = await factory({
canvas: opts.canvas,
// Keep the runtime alive after main() returns so Dawn-web's async
// adapter/device callbacks land (they set Module._app_ptr).
noExitRuntime: true,
print: opts.print || function (t) { console.log(t); },
printErr: opts.printErr || function (t) { console.warn(t); },
onRuntimeInitialized: function () { startLoop(this); },
});
// ---- wasm heap marshalling ---------------------------------------------
//
// Arrays cross the boundary as (pointer, count) into the wasm heap. Both
// helpers own the malloc/free pair so no call site can leak one.
// Copy `ids` into a scratch u32 buffer and hand (ptr, count) to `fn`.
function withIdArray(ids, fn) {
const n = ids.length;
if (n === 0) return fn(0, 0);
const ptr = Module._malloc(n * 4);
try {
Module.HEAPU32.set(Uint32Array.from(ids), ptr >>> 2);
return fn(ptr, n);
} finally {
Module._free(ptr);
}
}
// Run an "ask twice" getter: `fn(ptr, max)` fills at most `max` u32s and
// returns the TOTAL count. Call it empty to size, then again to fill.
function readIdArray(fn) {
const total = fn(0, 0);
if (total <= 0) return [];
const ptr = Module._malloc(total * 4);
try {
fn(ptr, total);
return Array.from(Module.HEAPU32.subarray(ptr >>> 2, (ptr >>> 2) + total));
} finally {
Module._free(ptr);
}
}
// The one selection mutator: mode 0 replaces (an empty list clears), 1 adds,
// 2 removes. Notifies listeners itself, so a programmatic change reaches
// onSelectionChange through the same path a click does.
function applySelection(x, mode) {
withIdArray(resolveIds(x), function (ptr, n) {
Module._ifcv_apply_selection_c(ptr, n, mode);
});
Module.__ifcvOnSelectionChange();
}
// Normalise whatever the caller passed into a flat array of objectIds.
// Accepts a number, an IFC GlobalId string, an object with an `objectId`
// or `guid` field (so an element straight out of getObjects() works), or
// an array of any of those. null/undefined is an empty selection.
function resolveIds(x) {
if (x === null || x === undefined) return [];
if (!Array.isArray(x)) x = [x];
return x.map(function (item) {
if (typeof item === 'number') return item >>> 0;
if (item && typeof item === 'object') {
if (typeof item.objectId === 'number') return item.objectId >>> 0;
item = item.guid;
}
if (typeof item !== 'string') throw new Error('not an objectId or GlobalId: ' + item);
if (!objectIndex) {
throw new Error('cannot resolve GlobalId "' + item +
'" — await viewer.getObjects() first (it loads the element table)');
}
const id = objectIndex.get(item);
if (id === undefined) throw new Error('unknown GlobalId: ' + item);
return id;
});
}
// Byte-source registry the wasm reads lazily: a picked File (Blob.slice) or
// a remote URL (HTTP Range). load_sidecar_from_source_c(sid) streams one.
Module.__ifcvSources = Module.__ifcvSources || [];
// The wasm calls this on every single-object pick; (0, '', -1, -1) means the
// selection was cleared. modelIndex is the picked object's model in load
// order (matches the modelProgress index) and sourceId the source it was
// added from, either null when unknown. A marquee box-select does NOT fire
// this (it has no single object) — use onSelectionChange for that.
Module.__ifcvOnSelect = function (objectId, guid, modelIndex, sourceId) {
const detail = {
objectId: objectId >>> 0,
guid: guid || null,
modelIndex: (typeof modelIndex === 'number' && modelIndex >= 0) ? modelIndex : null,
sourceId: (typeof sourceId === 'number' && sourceId >= 0) ? sourceId : null,
};
selectListeners.forEach(function (cb) {
try { cb(detail); } catch (e) { console.error(e); }
});
try {
document.dispatchEvent(new CustomEvent('ifcviewer:select', { detail: detail }));
} catch (_) { /* older browsers */ }
};
// The wasm pings this whenever it mutates the selection (pick, marquee,
// hide-selected); the programmatic setters below call it too. It carries no
// payload — listeners pull the current id set back through getSelection(),
// so there is exactly one source of truth.
Module.__ifcvOnSelectionChange = function () {
const ids = api.getSelection();
selectionListeners.forEach(function (cb) {
try { cb(ids); } catch (e) { console.error(e); }
});
try {
document.dispatchEvent(new CustomEvent('ifcviewer:selectionchange', { detail: ids }));
} catch (_) { /* older browsers */ }
};
// The wasm calls this once a model is fully in the scene, with the source
// id it was loaded from and the session model id the core assigned it. By
// the time it fires the federation layer has already applied any staged
// transform and (for the first model) the false-origin guess, so a handler
// sees the model where it will actually sit rather than mid-placement.
Module.__ifcvOnModelLoaded = function (sourceId, sessionModelId) {
const detail = { sourceId: sourceId | 0, sessionModelId: sessionModelId >>> 0 };
modelLoadedListeners.forEach(function (cb) {
try { cb(detail); } catch (e) { console.error(e); }
});
try {
document.dispatchEvent(new CustomEvent('ifcviewer:modelloaded', { detail: detail }));
} catch (_) { /* older browsers */ }
};
// Completion side of ifcv_request_objects_c: the element tables have all
// landed and the scene's objects are ready as JSON. `token` matches the
// request to its pending Promise.
const pendingObjects = new Map();
let objectsToken = 0;
Module.__ifcvOnObjects = function (token, json) {
const resolve = pendingObjects.get(token);
if (!resolve) return;
pendingObjects.delete(token);
resolve(JSON.parse(json));
};
// Some test harnesses / the fullscreen page want the raw module on window.
if (opts.exposeAsModuleGlobal) global.Module = Module;
function registerFile(file) {
const sid = Module.__ifcvSources.length;
Module.__ifcvSources.push({ file: file, url: null, size: file.size });
return sid;
}
async function registerUrl(url) {
const size = await sizeUrl(url);
if (!size) throw new Error('could not size ' + url + ' (need HEAD or Range support)');
const sid = Module.__ifcvSources.length;
Module.__ifcvSources.push({ file: null, url: url, size: size });
return sid;
}
api = {
module: Module,
ready: ready,
isLive: function () { return live; },
// ---- Events ----------------------------------------------------------
// Single-object picks (click). Fires with
// {objectId, guid, modelIndex, sourceId}. Returns an unsubscribe function.
onSelect: function (cb) {
selectListeners.push(cb);
return function () {
const i = selectListeners.indexOf(cb);
if (i >= 0) selectListeners.splice(i, 1);
};
},
// Any selection change — click, box-select, or a programmatic setter.
// Fires with the full array of selected objectIds.
onSelectionChange: function (cb) {
selectionListeners.push(cb);
return function () {
const i = selectionListeners.indexOf(cb);
if (i >= 0) selectionListeners.splice(i, 1);
};
},
// ---- Camera ----------------------------------------------------------
// The orbit camera's full state. `eye` is derived from the other four by
// the wasm (it owns the orbit convention) and is read-only — to move the
// camera, set target/distance/yaw/pitch.
getCamera: function () {
const ptr = Module._malloc(9 * 4);
try {
Module._ifcv_get_camera_c(ptr);
const f = Module.HEAPF32.subarray(ptr >>> 2, (ptr >>> 2) + 9);
return {
target: [f[0], f[1], f[2]],
distance: f[3],
yaw: f[4], // degrees, about world +Z
pitch: f[5], // degrees above the XY plane, clamped to ±89.9
eye: [f[6], f[7], f[8]],
ortho: Module._projection_is_ortho_c() !== 0,
};
} finally {
Module._free(ptr);
}
},
// Set any subset of the camera state; omitted fields keep their current
// value, so `setCamera({ yaw: 90 })` is a pure turn.
setCamera: function (c) {
c = c || {};
const cur = this.getCamera();
const t = c.target || cur.target;
Module._ifcv_set_camera_c(
t[0], t[1], t[2],
c.distance !== undefined ? c.distance : cur.distance,
c.yaw !== undefined ? c.yaw : cur.yaw,
c.pitch !== undefined ? c.pitch : cur.pitch);
if (c.ortho !== undefined) Module._ifcv_set_ortho_c(c.ortho ? 1 : 0);
},
// ---- Background ------------------------------------------------------
// Clear colour, as '#rgb' / '#rrggbb' / '#rrggbbaa' or {r, g, b, a} with
// 0-255 channels. Alpha defaults to opaque.
//
// Unlike setColor, an alpha of 0 here is meaningful rather than the "no
// override" sentinel: it clears the canvas to nothing, letting whatever
// is behind it in the DOM show through. Stack anything under the canvas
// — another 3D view, a map, ordinary page content — and the model draws
// on top of it. There is no depth interaction: the layer behind is
// strictly behind, so it reads as a backdrop and cannot occlude the
// model. Requires a platform that composites the canvas with alpha;
// where it does not, the colour still applies and the alpha is ignored.
setBackground: function (color) {
let r, g, b, a = 255;
if (typeof color === 'string') {
let hex = color.replace(/^#/, '');
if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');
if (hex.length !== 6 && hex.length !== 8) {
throw new Error('setBackground: expected #rgb, #rrggbb or #rrggbbaa, got ' + color);
}
r = parseInt(hex.slice(0, 2), 16);
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
if (hex.length === 8) a = parseInt(hex.slice(6, 8), 16);
} else if (color && typeof color === 'object') {
r = color.r | 0; g = color.g | 0; b = color.b | 0;
if (color.a !== undefined) a = color.a | 0;
} else {
throw new Error('setBackground: expected a colour, got ' + color);
}
Module._ifcv_set_background_c(r / 255, g / 255, b / 255, a / 255);
},
// ---- Navigation ------------------------------------------------------
// Which mouse buttons orbit / pan / select, as one of NAV_PRESETS:
// blender MMB orbit · Shift+MMB pan · LMB select
// rhino RMB orbit · Shift+RMB pan · LMB select
// revit Shift+MMB orbit · MMB pan · LMB select
// web LMB orbit · MMB pan · RMB select (the default)
// The wheel zooms under all four. Also settable up-front as the
// `navPreset` option to create().
setNavPreset: function (name) {
if (NAV_PRESETS.indexOf(name) < 0) {
throw new Error('unknown nav preset: ' + name +
' (expected one of ' + NAV_PRESETS.join(', ') + ')');
}
Module.ccall('ifcv_set_nav_preset_c', null, ['string'], [name]);
},
// id: 0 Front, 1 Back, 2 Left, 3 Right, 4 Top, 5 Bottom.
setStandardView: function (id) { Module._standard_view_c(id | 0); },
viewAll: function () { Module._view_all_c(); },
frameSelection: function () { Module._frame_selection_c(); },
// ---- Selection -------------------------------------------------------
// The selected objectIds, ascending.
getSelection: function () {
return readIdArray(function (ptr, max) {
return Module._ifcv_get_selection_c(ptr, max);
});
},
// The last single-clicked object — what a properties panel should show.
// 0 when nothing is selected.
getActiveObject: function () { return Module._ifcv_get_active_object_c() >>> 0; },
// Selected objects with their IFC identity attached. Requires the element
// table (getObjects()); falls back to bare ids before then.
getSelectedObjects: function () {
return api.getSelection().map(function (id) {
return (objectsById && objectsById.get(id)) || { objectId: id };
});
},
// Each takes an objectId, a GlobalId, an element from getObjects(), or an
// array of any of those. setSelection replaces (empty/null clears).
setSelection: function (x) { applySelection(x, 0); },
addToSelection: function (x) { applySelection(x, 1); },
removeFromSelection: function (x) { applySelection(x, 2); },
clearSelection: function () { applySelection([], 0); },
// ---- Objects ---------------------------------------------------------
// Every object in the scene:
// [{objectId, guid, name, type, model, sourceId}], where `model` is the
// index into the load-ordered model list (same index as modelProgress)
// and `sourceId` the source the model was added from — see the model
// identity note at the top of the file. Asynchronous — the element tables
// are fetched lazily per model so first paint never waits on them.
// Resolving this is also what lets every other call accept GlobalIds; the
// result is cached for that.
getObjects: function () {
const token = ++objectsToken;
return new Promise(function (resolve) {
pendingObjects.set(token, resolve);
Module._ifcv_request_objects_c(token);
}).then(function (objects) {
objectIndex = new Map();
objectsById = new Map();
objects.forEach(function (o) {
if (o.guid) objectIndex.set(o.guid, o.objectId);
objectsById.set(o.objectId, o);
});
return objects;
});
},
// ---- Visibility ------------------------------------------------------
hide: function (x) { this.setVisible(x, false); },
show: function (x) { this.setVisible(x, true); },
showAll: function () { Module._show_all_c(); },
hideAll: function () { Module._hide_all_c(); },
setVisible: function (x, visible) {
withIdArray(resolveIds(x), function (ptr, n) {
Module._ifcv_set_visible_c(ptr, n, visible ? 1 : 0);
});
},
// The hidden objectIds, ascending.
getHidden: function () {
return readIdArray(function (ptr, max) {
return Module._ifcv_get_hidden_c(ptr, max);
});
},
// Selection-driven visibility, matching the H / Shift+H / Alt+H hotkeys.
hideSelected: function () { Module._hide_selected_c(); },
isolateSelected: function () { Module._isolate_selected_c(); },
// The white-on-dark halo drawn around selected objects (on by default).
// The renderer also tints the selection blue, which says nothing when
// the object is already blue — hence a cue that does not depend on the
// object's colour. Turn it off to get the tint alone.
setSelectionOutline: function (on) {
Module._ifcv_set_selection_outline_c(on ? 1 : 0);
},
selectionOutlineEnabled: function () {
return Module._ifcv_selection_outline_is_on_c() !== 0;
},
// ---- Colour ----------------------------------------------------------
// Paint objects a flat colour, replacing whatever the model baked in.
// `color` is '#rrggbb' / '#rrggbbaa' / {r,g,b,a}, or null to restore the
// model's own colour. An alpha below 255 makes the object translucent —
// it moves to the transparent pass on the next frame.
setColor: function (x, color) {
const rgba = packColor(color);
withIdArray(resolveIds(x), function (ptr, n) {
Module._ifcv_set_color_c(ptr, n, rgba);
});
},
// Drop every override in the scene at once.
clearColors: function () { Module._ifcv_clear_colors_c(); },
// ---- Scene -----------------------------------------------------------
// Drops every model. The element table goes with them, so GlobalIds stop
// resolving until the next getObjects().
clearScene: function () {
objectIndex = null;
objectsById = null;
if (Module._clear_scene_c) Module._clear_scene_c();
},
toggleXray: function () { Module._toggle_xray_c(); },
xrayActive: function () { return Module._xray_is_active_c() !== 0; },
// Model bookkeeping (ordered by load). Progress is per-model chunk counts.
modelCount: function () { return Module._ifcv_model_count_c ? Module._ifcv_model_count_c() : 0; },
modelProgress: function (i) {
return {
resident: Module._ifcv_model_resident_c ? Module._ifcv_model_resident_c(i) : 0,
total: Module._ifcv_model_total_c ? Module._ifcv_model_total_c(i) : 0,
};
},
bytes: function () {
return {
total: Module._ifcv_bytes_total_c ? Module._ifcv_bytes_total_c() : 0,
needed: Module._ifcv_bytes_needed_c ? Module._ifcv_bytes_needed_c() : 0,
loaded: Module._ifcv_bytes_loaded_c ? Module._ifcv_bytes_loaded_c() : 0,
};
},
// The latest frame's statistics — what BonsaiViewer's status bar shows.
// `vram` is the streamed-geometry cache: bytes held by resident chunks,
// the pool's current capacity, and the budget it may grow to (0 while
// unbounded). `workingSet` is what the camera wants resident: chunks in
// view and large enough to draw, how many of those are not resident,
// and their size — transiently non-zero after a camera move, and
// persistently non-zero when the scene does not fit in GPU memory
// (unload a model to make room). Null before the first frame.
stats: function () {
const n = 13;
const ptr = Module._malloc(n * 8);
try {
if (!Module._ifcv_get_frame_stats_c(ptr, n)) return null;
const d = Module.HEAPF64.subarray(ptr >>> 3, (ptr >>> 3) + n);
return {
fps: d[0],
frameTimeMs: d[1],
objects: { visible: d[3], total: d[2] },
triangles: { visible: d[5], total: d[4] },
drawCalls: d[6],
vram: { usedBytes: d[7], capacityBytes: d[8], budgetBytes: d[9] },
workingSet: { chunks: d[10], chunksMissing: d[11], missingBytes: d[12] },
};
} finally {
Module._free(ptr);
}
},
// GPU residency per model, by source id. Unloading frees everything
// the model holds on the GPU while it stays in the scene (its
// visibility untouched); loading brings it back, streaming the
// geometry in again on demand. loadModel resolves false when the
// device cannot fit the model's buffers. This is the lever when
// stats().workingSet.chunksMissing stays above zero.
unloadModel: function (sourceId) { Module._ifcv_unload_model_c(sourceId | 0); },
loadModel: function (sourceId) { return Module._ifcv_load_model_c(sourceId | 0) !== 0; },
modelUnloaded: function (sourceId) { return Module._ifcv_model_unloaded_c(sourceId | 0) !== 0; },
modelVramBytes: function (sourceId) { return Module._ifcv_model_vram_bytes_c(sourceId | 0); },
registerFileSource: registerFile,
registerUrlSource: registerUrl,
// Add a model to the scene. `replace: true` drops the current scene first;
// otherwise it appends (a lightweight federation of streamed models).
// Returns the source id: the federation handle for this model. It is
// valid immediately, before the model has streamed, so a transform or
// name can be set against it right away — see setModelTransform.
addFile: async function (file, o) {
if (o && o.replace) this.clearScene();
const sid = registerFile(file);
if (o && o.name) this.setModelName(sid, o.name);
Module._load_sidecar_from_source_c(sid);
return sid;
},
addUrl: async function (url, o) {
if (o && o.replace) this.clearScene();
const sid = await registerUrl(url);
if (o && o.name) this.setModelName(sid, o.name);
Module._load_sidecar_from_source_c(sid);
return sid;
},
// ---- Federation ------------------------------------------------------
//
// The concepts an .ifcfed file carries, without the file format: a
// federation unit, a false origin, and a per-model transform. Parse
// .ifcfed (or any manifest) in your own code and drive these.
//
// Models resolve to global coordinates by default: each one's
// IfcCoordinateOperation is baked into its sidecar and applied on load,
// so models with different map conversions line up. The first model also
// sets a false origin automatically, which keeps the scene near the
// origin — necessary because per-instance transforms are float32 and
// surveyor coordinates would otherwise quantise to ~0.5 m. Call
// setFalseOrigin yourself to override; that suppresses the guess.
onModelLoaded: function (cb) {
modelLoadedListeners.push(cb);
return function () {
const i = modelLoadedListeners.indexOf(cb);
if (i >= 0) modelLoadedListeners.splice(i, 1);
};
},
// Federation unit: an IfcSIUnit name with optional SI prefix
// ({name:'METRE', prefix:'MILLI'}) or a conversion-based unit
// ({name:'foot'}). This is the value space for the false origin and for
// a transform's b/pivot.
setFederationUnit: function (u) {
u = u || {};
Module.ccall('ifcv_set_federation_unit_c', null, ['string', 'string'],
[u.name || 'METRE', u.prefix || '']);
},
// Nominate a point as the scene origin, with an optional grid-north
// heading in degrees. Setting this turns off the automatic guess.
setFalseOrigin: function (o) {
o = o || {};
const xyz = o.xyz || [0, 0, 0];
Module._ifcv_set_false_origin_c(xyz[0], xyz[1], xyz[2], o.rzDeg || 0);
},
// The active origin, including one the automatic guess produced.
// `explicit` is true when it was set rather than guessed.
getFalseOrigin: function () {
const ptr = Module._malloc(5 * 8);
try {
Module._ifcv_get_false_origin_c(ptr);
const d = Module.HEAPF64.subarray(ptr >>> 3, (ptr >>> 3) + 5);
return { xyz: [d[0], d[1], d[2]], rzDeg: d[3], explicit: d[4] !== 0 };
} finally {
Module._free(ptr);
}
},
// Place a model: rotate it about `pivot`, then translate so that point
// `a` lands on point `b`.
//
// `aFrame` picks the frame `a` is expressed in: 'global' (default) means
// post-CoordinateOperation, in the model's map unit — i.e. real-world
// coordinates. 'local' means pre-CoordinateOperation, in the model's own
// project unit. b, pivot and the origin are in the federation unit;
// rotation is degrees, intrinsic XYZ.
//
// Safe to call before the model has finished streaming; it is applied
// when the load completes, so the model never visibly jumps.
setModelTransform: function (sourceId, xf) {
xf = xf || {};
const a = xf.a || [0, 0, 0], b = xf.b || [0, 0, 0];
const r = xf.rotationDeg || [0, 0, 0], p = xf.pivot || [0, 0, 0];
Module._ifcv_set_model_transform_c(
sourceId | 0, xf.aFrame === 'local' ? 0 : 1,
a[0], a[1], a[2], b[0], b[1], b[2],
r[0], r[1], r[2], p[0], p[1], p[2]);
},
clearModelTransform: function (sourceId) {
Module._ifcv_clear_model_transform_c(sourceId | 0);
},
setModelName: function (sourceId, name) {
Module.ccall('ifcv_set_model_name_c', null, ['number', 'string'],
[sourceId | 0, String(name)]);
},
// The georeferencing a model actually carries, read back from its
// sidecar: {hasCoordinateOperation, matrix (16, column-major, metres),
// projectLengthToMeters, mapUnitToMeters}. Null until the model has
// finished loading.
getModelGeoref: function (sourceId) {
const ptr = Module._malloc(19 * 8);
try {
if (!Module._ifcv_get_model_georef_c(sourceId | 0, ptr)) return null;
const d = Module.HEAPF64.subarray(ptr >>> 3, (ptr >>> 3) + 19);
return {
hasCoordinateOperation: d[0] !== 0,
matrix: Array.from(d.subarray(1, 17)),
projectLengthToMeters: d[17],
mapUnitToMeters: d[18],
};
} finally {
Module._free(ptr);
}
},
};
// The nav preset is pure button-table state on the wasm side, so it can be
// set the moment main() has run (which is by the time `factory` resolved) —
// no need to wait on `ready`. Doing it here means the very first drag uses
// the host's scheme rather than the "web" default for a frame or two.
if (opts.navPreset) api.setNavPreset(opts.navPreset);
return api;
}
global.IfcViewer = { create: create, sizeUrl: sizeUrl, packColor: packColor };
})(window);