diff --git a/src/ifcviewer-web/CMakeLists.txt b/src/ifcviewer-web/CMakeLists.txt
index 1a5b2ce29f..13678a7ac0 100644
--- a/src/ifcviewer-web/CMakeLists.txt
+++ b/src/ifcviewer-web/CMakeLists.txt
@@ -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.
diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp
index b2aac97ab8..99cdd9c0af 100644
--- a/src/ifcviewer-web/main_web.cpp
+++ b/src/ifcviewer-web/main_web.cpp
@@ -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) {
diff --git a/src/ifcviewer-web/web/ifcviewer.js b/src/ifcviewer-web/web/ifcviewer.js
index 62931aab46..fb1d6c8cdc 100644
--- a/src/ifcviewer-web/web/ifcviewer.js
+++ b/src/ifcviewer-web/web/ifcviewer.js
@@ -5,7 +5,8 @@
//
//
//
//
// 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;
}