Restructure the web viewer so the wasm is a reusable module and add a
second example that drives it from ordinary page DOM.
Build:
- Emit IfcViewerWeb.js (a `createIfcViewer` factory, MODULARIZE) + .wasm
instead of a single baked page (dropped --shell-file); copy the static
example pages next to it at build time.
- Unbreak the web build: CameraMath.h / ViewportCore.cpp used
boost::math::constants::pi just for pi, pulling all of boost/math into a
header shared with the Emscripten build (no Boost in its sysroot). Replace
with a constexpr kPiF — identical value, no dependency, desktop unaffected.
JS integration (web/ifcviewer.js):
- A small helper wraps the factory: boots the viewer on a canvas, runs the
RAF loop from onRuntimeInitialized (NOT a post-await .then, which stalls
Dawn-web's device callback and leaves the device half-initialised), and
exposes addFile/addUrl, clearScene, model list/progress, and onSelect(...).
- ViewportCore/main_web emit each pick to JS via Module.__ifcvOnSelect
(object id + IFC GlobalId + model index; empty on deselect); onSelect also
dispatches an 'ifcviewer:select' DOM event.
- Fix input coords for a non-fullscreen canvas: mousemove/mouseup are
window-targeted, so convert their coords to canvas-relative via the canvas
client-rect origin (marquee + box-pick were offset when embedded).
Examples:
- IfcViewerWeb.html: the fullscreen viewer (same DOM/behaviour as before,
now loading the module) — the Playwright smoke suite still targets it.
- embedded.html: a sized viewer with DOM outside it to add models (file or
URL), list loaded models with streaming progress, and show the model +
GlobalId of the clicked object. Starts empty (drops the wasm's embedded
sample, which the fullscreen page/tests still use).
- index.html links both.
Federation note: the web viewer already streams multiple models into one
scene (a byte-source per file/URL); it doesn't need the desktop Federation
document for this. Verified: 11/11 web smoke tests pass; embedded example
loads models, reports the picked model + GUID, and the marquee aligns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CPU cull (frustum + contribution + LOD + opaque/transparent partition)
and its companion GPU-upload step now live in ViewportCore. The HiZ
occlusion test stays in VW — the pyramid + async readback machinery
hasn't migrated yet — and is plumbed through a
ViewportCore::HizOccludedFn callback the render path binds when HiZ
is enabled-and-fresh. Null callback means "no occlusion test", which
keeps the cull path host-agnostic.
extractFrustumPlanes + aabbInFrustum moved up into CameraMath.h so
both VW's render() (where the planes are extracted) and core's cull
(where they're tested) can share without one #including the other.
LOD-debug counters (lod1_dbg_count_, lod0_dbg_eligible_count_,
lod0_dbg_no_lod1_count_, lod1_dbg_tris_saved_) moved to core too —
they're written by cull and read/reset by VW's still-here per-frame
[frame] heartbeat through reference aliases.
Replace Qt math wrappers with Eigen across ViewportWindow, OverlayRenderer,
Federation, and the bonsai-side viewport modules. Eigen was already the
canonical type for the actually-important matrix work (InstanceCompose,
ModelGpuData, federation matrices); QVector3D/QVector4D/QMatrix4x4 were
leftover from when Qt was the path of least resistance. They offered
nothing over Eigen for our use case beyond a few graphics helpers
(lookAt / perspective / ortho) which were 30 lines to write.
Substitutions:
QMatrix4x4 → Eigen::Matrix4f
QVector2D → Eigen::Vector2f
QVector3D → Eigen::Vector3f
QVector4D → Eigen::Vector4f
API rewrites:
.lengthSquared() → .squaredNorm()
.length() → .norm()
.isNull() → .isZero()
.setToIdentity() → .setIdentity()
.constData() → .data()
.toVector3D() → .head<3>()
.inverted(&ok) → tryInvert4f(M, out)
Q::dotProduct(a,b) → a.dot(b)
Q::crossProduct(a,b) → a.cross(b)
QMat4x4(... row-major) → Eigen::Map<const Matrix4f>(col-major buf)
QMat4x4().lookAt(...) → lookAtRH(eye, target, up)
QMat4x4().perspective(.) → perspectiveYFovGL(fovy, aspect, n, f)
QMat4x4().ortho(...) → orthoGL(l, r, b, t, n, f)
Default-init divergence handled explicitly (QMatrix4x4() = identity,
QVector3D() = zero; Eigen leaves both uninitialized). Public API
(CameraState, HomeView, ViewportWindow::computeObjectAabb, the
addSectionPlaneAtSurface / pickSurfaceAt / raycast signatures) follows
through to Eigen too; bonsai-side View.cpp and Commands.cpp updated to
match.
Camera helpers (lookAtRH, perspectiveYFovGL, orthoGL, tryInvert4f)
extracted to a new CameraMath.h so OverlayRenderer's gizmo MVP and
ViewportWindow's buildViewProj share the same definitions. Federation
drops its <QVector3D> include in favour of <Eigen/Dense> (already had
the latter for the georef matrices).
Builds: desktop IfcViewerMinimal ✓, BonsaiViewer ✓, web IfcViewerWeb ✓.
Tests: 100/100 pass. Closes#78 + #79; opens the door for #80-#83.