ifcviewer-web: let host pages choose the mouse nav preset

ViewportCore has had a preset table (blender / rhino / revit / web) since the
nav bindings were shared with the desktop, and the web input handlers already
classify presses against it — but main() hard-coded "web" and nothing could
reach setNavPreset from JS, so every page was stuck on LMB-orbit.

Export ifcv_set_nav_preset_c and wrap it as IfcViewer.create's `navPreset`
option plus a setNavPreset() method. The preset is only the button/modifier
table, so it needs no GPU state: it applies as soon as the module resolves,
which means the first drag already uses the host's scheme rather than
flipping after a frame or two.

Names are validated in JS against the four the core knows. The core silently
falls back to blender for anything unrecognised, which would turn a typo into
a mystery change of scheme rather than an error.

The default stays "web", so existing pages and the smoke tests are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-28 15:44:37 +10:00
parent 6bcfb175a3
commit 8cc96127ce
3 changed files with 50 additions and 5 deletions
+6 -4
View File
@@ -108,13 +108,15 @@ target_link_options(IfcViewerWeb PRIVATE
# sidecar via HTTP Range; _ifcv_on_range_done / _ifcv_source_ready are the
# JS→C completion callbacks for a landed range / a resolved URL size.
# The _ifcv_{get,set,apply}_* family is the scripting API (camera, selection,
# visibility, colour override, object enumeration) that web/ifcviewer.js
# wraps; _malloc/_free let it marshal id arrays into the wasm heap.
# visibility, colour override, object enumeration, mouse nav preset) that
# web/ifcviewer.js wraps; _malloc/_free let it marshal id arrays into the
# wasm heap.
# EMSCRIPTEN_KEEPALIVE alone keeps the symbols in the binary but doesn't
# add them to Module. ccall lets the host page (web/ifcviewer.js) pass a JS string (the ?model
# URL) to load_sidecar_from_url_c without manual heap marshalling.
"-sEXPORTED_FUNCTIONS=['_main','_malloc','_free','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c','_hide_selected_c','_isolate_selected_c','_show_all_c','_hide_all_c','_toggle_xray_c','_xray_is_active_c','_toggle_section_c','_clear_section_c','_section_is_active_c','_ifcv_get_camera_c','_ifcv_set_camera_c','_ifcv_set_ortho_c','_ifcv_get_selection_c','_ifcv_get_active_object_c','_ifcv_apply_selection_c','_ifcv_set_visible_c','_ifcv_get_hidden_c','_ifcv_set_color_c','_ifcv_clear_colors_c','_ifcv_request_objects_c']"
# ccall: the host page (web/ifcviewer.js) passes the ?model URL string to load_sidecar_from_url_c.
"-sEXPORTED_FUNCTIONS=['_main','_malloc','_free','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c','_hide_selected_c','_isolate_selected_c','_show_all_c','_hide_all_c','_toggle_xray_c','_xray_is_active_c','_toggle_section_c','_clear_section_c','_section_is_active_c','_ifcv_get_camera_c','_ifcv_set_camera_c','_ifcv_set_ortho_c','_ifcv_set_nav_preset_c','_ifcv_get_selection_c','_ifcv_get_active_object_c','_ifcv_apply_selection_c','_ifcv_set_visible_c','_ifcv_get_hidden_c','_ifcv_set_color_c','_ifcv_clear_colors_c','_ifcv_request_objects_c']"
# ccall: the host page (web/ifcviewer.js) passes the ?model URL string to load_sidecar_from_url_c,
# and the nav-preset name to ifcv_set_nav_preset_c.
# HEAPU8: lets tooling/tests read the wasm heap size (e.g. to verify a large
# sidecar streams by range instead of loading whole). HEAPU32/HEAPF32: the
# scripting API marshals object-id arrays and the camera state through them.
+13
View File
@@ -627,6 +627,17 @@ extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_set_ortho_c(int on) {
if (bool(on) != g_app->core.projectionOrtho()) g_app->core.toggleProjection();
}
// Mouse navigation scheme: "blender" | "rhino" | "revit" | "web". The preset
// only rewrites the button/modifier table classifyPress reads, so unlike the
// rest of the scripting API it does not need the GPU app to be live — a host
// page can pick its scheme the moment the module resolves. Unknown names fall
// back to blender inside the core; web/ifcviewer.js rejects them before they
// get here so a typo is an error rather than a silent scheme change.
extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_set_nav_preset_c(const char* name) {
if (!g_app || !name) return;
g_app->core.setNavPreset(name);
}
// Selection.
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_get_selection_c(std::uint32_t* out, int max) {
if (!g_app || !g_app->ready) return 0;
@@ -778,6 +789,8 @@ int main(int /*argc*/, char** /*argv*/) {
Log::info() << "ifcviewer-web: starting";
g_app = new AppState();
// Default to the web mouse scheme: LMB orbit, MMB pan, RMB select/marquee.
// Host pages override it with ifcv_set_nav_preset_c (IfcViewer.create's
// `navPreset` option) — e.g. "blender" for MMB-orbit.
g_app->core.setNavPreset("web");
g_app->core.initWgpuAsyncWeb([](bool ok) {
if (!ok) {
+31 -1
View File
@@ -5,7 +5,8 @@
// <script src="IfcViewerWeb.js"></script>
// <script src="ifcviewer.js"></script>
// <script>
// const viewer = await IfcViewer.create({ canvas: myCanvas });
// const viewer = await IfcViewer.create({ canvas: myCanvas,
// navPreset: 'blender' });
// await viewer.ready; // GPU app is live
// await viewer.addFile(file, { replace: true });
// await viewer.addUrl('/model.ifcview'); // appends (federation)
@@ -14,6 +15,7 @@
// viewer.setSelection(['3vB2YO$MX4xv5uCqZZG05x']);
// viewer.setColor(objects.filter(o => o.type === 'IfcWall'), '#ff8800');
// viewer.setCamera({ yaw: 45, pitch: 30 });
// viewer.setNavPreset('rhino'); // or switch schemes later
// </script>
//
// The canvas element MUST have id="viewer-canvas" — the wasm side hard-codes
@@ -41,6 +43,10 @@
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
@@ -314,6 +320,23 @@
if (c.ortho !== undefined) Module._ifcv_set_ortho_c(c.ortho ? 1 : 0);
},
// ---- 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(); },
@@ -447,6 +470,13 @@
Module._load_sidecar_from_source_c(await registerUrl(url));
},
};
// 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;
}