web: MODULARIZE build + embedded JS-integration example + selection callback

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>
This commit is contained in:
Dion Moult
2026-07-09 16:46:29 +10:00
parent b2ecfab86e
commit ed21dd7ecc
10 changed files with 783 additions and 424 deletions
+19 -6
View File
@@ -31,7 +31,6 @@
#include "InstanceCompose.h"
#include "Log.h"
#include <boost/math/constants/constants.hpp>
#include <algorithm>
#include <cmath>
@@ -47,7 +46,7 @@ namespace {
// updateCamera convention so framing aligns between backends.
Eigen::Vector3f orbitEye(const float target[3], float dist,
float yaw_deg, float pitch_deg) {
constexpr float kDeg2Rad = boost::math::constants::pi<float>() / 180.0f;
constexpr float kDeg2Rad = kPiF / 180.0f;
const float yaw = yaw_deg * kDeg2Rad;
const float pit = pitch_deg * kDeg2Rad;
const float cp = std::cos(pit), sp = std::sin(pit);
@@ -178,7 +177,7 @@ void ViewportCore::buildViewProj(Eigen::Matrix4f& view_out,
: 1.0f;
Eigen::Matrix4f p;
if (projection_ortho_) {
constexpr float kDeg2Rad = boost::math::constants::pi<float>() / 180.0f;
constexpr float kDeg2Rad = kPiF / 180.0f;
const float half_h = camera_distance_
* std::tan(camera_fov_y_deg_ * 0.5f * kDeg2Rad);
const float half_w = half_h * aspect;
@@ -387,7 +386,7 @@ void ViewportCore::composeInstanceFromPlacement(InstanceInfo& inst,
void ViewportCore::frameAabb(const float mn[3], const float mx[3],
float padding) {
constexpr float kDeg2Rad = boost::math::constants::pi<float>() / 180.0f;
constexpr float kDeg2Rad = kPiF / 180.0f;
const float cx = 0.5f * (mn[0] + mx[0]);
const float cy = 0.5f * (mn[1] + mx[1]);
const float cz = 0.5f * (mn[2] + mx[2]);
@@ -513,7 +512,7 @@ void ViewportCore::orbitBy(float dx_px, float dy_px) {
}
void ViewportCore::panBy(float dx_px, float dy_px, int viewport_height_px) {
constexpr float kDeg2Rad = boost::math::constants::pi<float>() / 180.0f;
constexpr float kDeg2Rad = kPiF / 180.0f;
// Pan in the camera's screen-space plane. Within 1° of straight
// up/down the world-Z up-reference degenerates (cross with forward
@@ -3732,6 +3731,20 @@ void ViewportCore::logSelectedObjectGuidWeb(std::uint32_t object_id) {
? m.string_table.substr(e.guid_offset, e.guid_length)
: std::string("(none)");
Log::info() << "pick: object " << object_id << " GUID " << guid;
// Load-order index of the object's model (sorted by session id — the
// same order as streamingModelProgress and the JS model list); -1 if
// not found. Lets host pages show which model the pick belongs to.
std::vector<std::uint32_t> model_ids;
model_ids.reserve(models_gpu_.size());
for (const auto& [id, mm] : models_gpu_) model_ids.push_back(id);
std::sort(model_ids.begin(), model_ids.end());
const auto pos = std::find(model_ids.begin(), model_ids.end(), session_model_id);
const int model_index = (pos != model_ids.end()) ? int(pos - model_ids.begin()) : -1;
// Surface the selection to JS so host pages can react (e.g. show the
// GUID + model). Fires Module.__ifcvOnSelect(object_id, guid, modelIndex).
EM_ASM({
if (Module.__ifcvOnSelect) Module.__ifcvOnSelect($0, UTF8ToString($1), $2);
}, object_id, guid.c_str(), model_index);
return;
}
Log::info() << "pick: object " << object_id << " not in element table";
@@ -6121,7 +6134,7 @@ namespace {
// degrees → radians. Inline-only, used inside render() for the
// focal-length derivation.
constexpr float degreesToRadians(float deg) {
return deg * boost::math::constants::pi<float>() / 180.0f;
return deg * kPiF / 180.0f;
}
// Format a float with N decimals into the running Log line. Used to