ifcviewer-web: wire mouse + wheel navigation (#85)

Register emscripten HTML5 pointer/wheel handlers that translate raw
events into ViewportCore::orbitBy / panBy / dollyBy: left-drag orbits,
middle/right-drag pans, wheel zooms. mousedown binds to the canvas;
mousemove/up bind to the window so a drag keeps tracking off-canvas. A
contextmenu suppressor lets right-drag pan without the browser menu.

Pure callbacks — no Asyncify, no sync spin — so none of the web init
gotchas apply. Pick stays deferred until the async buffer-readback
rewrite. The embedded sample is now navigable on Chrome + Firefox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-29 10:50:11 +10:00
parent d92f6e6226
commit c0c0c987c4
+85 -1
View File
@@ -33,11 +33,16 @@
#include "Log.h"
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
namespace {
// CSS selector for the host <canvas>; must match shell.html + the
// WebViewportHost selector below.
constexpr const char* kCanvasSelector = "#viewer-canvas";
struct AppState {
WebViewportHost host{ "#viewer-canvas" };
WebViewportHost host{ kCanvasSelector };
ViewportCore core{ &host };
int last_w = 0;
int last_h = 0;
@@ -45,12 +50,86 @@ struct AppState {
// raf_tick_c skips render() until then; before that point the wgpu
// state pointers inside core are still null and any draw would crash.
bool ready = false;
// ---- Mouse navigation state ----
// A drag is armed on mousedown and released on mouseup. button is the
// DOM button code (0 left, 1 middle, 2 right). Left orbits; middle or
// right pans — matches common web 3D-viewer bindings and covers both
// three-button mice and trackpad (right-drag) users.
bool nav_active = false;
int nav_button = 0;
};
// One global so the JS-side RAF loop can recover state through a
// pointer round-trip (set into Module._app_ptr from on_complete).
AppState* g_app = nullptr;
// Logical (CSS-pixel) height of the canvas. Mouse movementX/Y deltas are
// in CSS pixels, so pan's world-units-per-pixel must use CSS-pixel height
// too (not the DPR-scaled framebuffer height).
int canvasCssHeight() {
double w = 0.0, h = 0.0;
emscripten_get_element_css_size(kCanvasSelector, &w, &h);
return (h > 1.0) ? int(h) : 1;
}
EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void* user) {
auto* app = static_cast<AppState*>(user);
if (e->button == 0 || e->button == 1 || e->button == 2) {
app->nav_active = true;
app->nav_button = e->button;
}
return EM_TRUE;
}
EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void* user) {
auto* app = static_cast<AppState*>(user);
if (!app->ready || !app->nav_active) return EM_FALSE;
const float dx = float(e->movementX);
const float dy = float(e->movementY);
if (app->nav_button == 0) {
app->core.orbitBy(dx, dy);
} else {
app->core.panBy(dx, dy, canvasCssHeight());
}
return EM_TRUE;
}
EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* /*e*/, void* user) {
static_cast<AppState*>(user)->nav_active = false;
return EM_TRUE;
}
EM_BOOL onWheel(int, const EmscriptenWheelEvent* e, void* user) {
auto* app = static_cast<AppState*>(user);
if (!app->ready) return EM_FALSE;
// Normalise to "notches" like the desktop wheel (one notch ≈ 120
// angle-units ≈ 100 px of deltaY). Line/page modes are scaled to a
// comparable pixel magnitude. Negate so wheel-up (deltaY < 0) zooms in.
double dy = e->deltaY;
if (e->deltaMode == DOM_DELTA_LINE) dy *= 16.0;
else if (e->deltaMode == DOM_DELTA_PAGE) dy *= 800.0;
app->core.dollyBy(-float(dy) / 100.0f);
return EM_TRUE; // consume so the page doesn't scroll
}
// Register pointer + wheel handlers once the app is live. mousedown binds
// to the canvas; mousemove/up bind to the window so a drag keeps tracking
// when the pointer leaves the canvas. A JS-side contextmenu suppressor
// lets right-drag pan without popping the browser menu.
void installInputHandlers(AppState* app) {
emscripten_set_mousedown_callback(kCanvasSelector, app, EM_FALSE, onMouseDown);
emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, app, EM_FALSE, onMouseMove);
emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, app, EM_FALSE, onMouseUp);
emscripten_set_wheel_callback(kCanvasSelector, app, EM_FALSE, onWheel);
EM_ASM({
var c = document.querySelector(UTF8ToString($0));
if (c) c.addEventListener('contextmenu', function(ev) { ev.preventDefault(); });
}, kCanvasSelector);
}
} // namespace
// Called from shell.html's RAF tick (via Module._raf_tick_c). Exported
@@ -102,6 +181,11 @@ int main(int /*argc*/, char** /*argv*/) {
g_app->ready = true;
// Now that the camera + render path are live, start listening for
// orbit/pan/zoom input. (Pick is still deferred — it needs the
// async buffer-readback rewrite before it's safe on web.)
installInputHandlers(g_app);
// Hand the app pointer to the JS-side RAF loop (set up in
// shell.html's onRuntimeInitialized). The loop polls for
// Module._app_ptr before invoking _raf_tick_c.