mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user