mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
f1d97ac5aa
Make orbit/pan/select mouse bindings pure data owned by ViewportCore so both
hosts and every preset share one source of truth, and add a "Web" preset. This
rounds out the matrix: the desktop gains a web-style scheme and the web inherits
all presets, with no per-platform hardcoding.
- Core: NavBindings { orbit, pan, select button + modifier } + setNavPreset
("blender" default | "rhino" | "revit" | "web") + navBindings(). Select is
preset-driven too (was hardcoded LMB) so "web" moves it to RMB. web = orbit
LMB, pan MMB, select RMB (LMB drag orbits with no click/drag ambiguity; RMB
click-selects / drag-marquees). NavMod uses "Plain" not "None" (X11 #defines
None to 0L).
- Desktop ViewportWindow: applyNavPreset sources the core table (mapped to Qt);
marquee-arm / single-pick dispatch keys off select_button_. Default stays
blender → no behaviour change.
- Desktop config: AppSettings::NavPreset gains Web + navPresetName(); the
Settings dialog lists it. This also FIXES a pre-existing gap — the preset combo
was persisted but never applied (only WGPU_NAV_PRESET env worked). MainWindow
now applies the persisted preset at startup (env override still wins) and live
on navPresetChanged, so all four presets actually work from the dialog.
- Web main_web: classifyPress routes the pressed button through navBindings()
(orbit/pan/select), defaulting to the "web" preset; context menu already
suppressed so RMB is free.
Tests: setNavPreset table (Catch2, 123 total); web smoke select tests use RMB.
BonsaiViewer builds; 9/9 web smoke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
510 lines
24 KiB
C++
510 lines
24 KiB
C++
/********************************************************************************
|
|
* *
|
|
* This file is part of IfcOpenShell. *
|
|
* *
|
|
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the Lesser GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3.0 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* IfcOpenShell is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* Lesser GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the Lesser GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
* *
|
|
********************************************************************************/
|
|
|
|
// Web entry point. Wires a WebViewportHost to a ViewportCore, brings up
|
|
// wgpu via emdawnwebgpu (the spec-compatible WebGPU header set that
|
|
// shipped with Dawn), loads the embedded sample sidecar, and drives
|
|
// render() per requestAnimationFrame from JS (shell.html).
|
|
//
|
|
// The RAF loop lives in shell.html — NOT here — because any call into
|
|
// Emscripten's main-loop / RAF helpers (or even raw
|
|
// requestAnimationFrame via EM_ASM) made from inside Dawn-web's wgpu
|
|
// promise-resolution chain stalls the device callback. Having JS drive
|
|
// the tick keeps the wasm init path callback-only.
|
|
|
|
#include "ViewportCore.h"
|
|
#include "WebViewportHost.h"
|
|
#include "Log.h"
|
|
|
|
#include <emscripten/emscripten.h>
|
|
#include <emscripten/html5.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
namespace {
|
|
|
|
// CSS selector for the host <canvas>; must match shell.html + the
|
|
// WebViewportHost selector below.
|
|
constexpr const char* kCanvasSelector = "#viewer-canvas";
|
|
|
|
// What a mouse drag drives, decided against the active nav preset's bindings.
|
|
enum class NavKind { None, Orbit, Pan, Select };
|
|
|
|
struct AppState {
|
|
WebViewportHost host{ kCanvasSelector };
|
|
ViewportCore core{ &host };
|
|
int last_w = 0;
|
|
int last_h = 0;
|
|
// Set true by the init callback once the device + pipelines are up.
|
|
// 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. What the press
|
|
// drives (orbit / pan / select) is decided against the active nav preset's
|
|
// bindings (ViewportCore::navBindings), so any preset works on web too.
|
|
bool nav_active = false;
|
|
NavKind nav_kind = NavKind::None;
|
|
// Accumulated |movement| since mousedown, in CSS px. A select-button release
|
|
// under the click threshold (no real drag) is treated as a pick; a drag will
|
|
// become a marquee. Captures the down position (canvas-relative CSS px).
|
|
float nav_drag_px = 0.0f;
|
|
long down_x = 0;
|
|
long down_y = 0;
|
|
|
|
// ---- Fly (first-person) mode ----
|
|
// Shift+F enters (pointer-locks the canvas), Esc exits. While flying, held
|
|
// W/A/S/D/Q/E + Shift are integrated each frame via ViewportCore::flyMove,
|
|
// and pointer-lock mouse deltas drive flyLook. dt from fly_last_ms.
|
|
bool fly_mode = false;
|
|
// True once the browser actually granted pointer lock for this fly session.
|
|
// Distinguishes "lock lost" (Esc/click-out → exit fly) from "lock denied on
|
|
// entry" (headless / permission) where fly stays keyboard-drivable.
|
|
bool fly_locked = false;
|
|
bool key_w_pressed = false;
|
|
bool key_a_pressed = false;
|
|
bool key_s_pressed = false;
|
|
bool key_d_pressed = false;
|
|
bool key_q_pressed = false;
|
|
bool key_e_pressed = false;
|
|
bool key_shift_pressed = false;
|
|
double fly_last_ms = 0.0;
|
|
};
|
|
|
|
// Click vs drag threshold (CSS px). Below this total travel a left release is
|
|
// a pick, above it the gesture was an orbit.
|
|
constexpr float kClickDragThresholdPx = 4.0f;
|
|
|
|
// 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;
|
|
|
|
// Defined below (with the fly helpers); onMouseDown needs it to exit fly on click.
|
|
void setFlyMode(AppState* app, bool on);
|
|
|
|
// 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;
|
|
}
|
|
|
|
NavKind classifyPress(const ViewportCore::NavBindings& b, int em_button,
|
|
bool shift, bool ctrl, bool alt) {
|
|
using MB = ViewportCore::MouseBtn; using M = ViewportCore::NavMod;
|
|
const MB btn = (em_button == 0) ? MB::Left : (em_button == 1) ? MB::Middle : MB::Right;
|
|
const M mod = shift ? M::Shift : ctrl ? M::Ctrl : alt ? M::Alt : M::Plain;
|
|
if (btn == b.orbit && mod == b.orbit_mod) return NavKind::Orbit;
|
|
if (btn == b.pan && mod == b.pan_mod) return NavKind::Pan;
|
|
if (btn == b.select) return NavKind::Select; // Shift/Ctrl = add/remove
|
|
return NavKind::None;
|
|
}
|
|
|
|
EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
// In fly mode a click exits (matches the desktop app).
|
|
if (app->fly_mode) { setFlyMode(app, false); return EM_TRUE; }
|
|
const NavKind kind = classifyPress(app->core.navBindings(), e->button,
|
|
e->shiftKey, e->ctrlKey, e->altKey);
|
|
if (kind != NavKind::None) {
|
|
app->nav_active = true;
|
|
app->nav_kind = kind;
|
|
app->nav_drag_px = 0.0f;
|
|
app->down_x = e->targetX; // canvas-relative CSS px
|
|
app->down_y = e->targetY;
|
|
}
|
|
return EM_TRUE;
|
|
}
|
|
|
|
EM_BOOL onMouseMove(int, const EmscriptenMouseEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
if (!app->ready) return EM_FALSE;
|
|
// Fly mode: pointer-locked mouse deltas turn the camera in place.
|
|
if (app->fly_mode) {
|
|
app->core.flyLook(float(e->movementX), float(e->movementY));
|
|
return EM_TRUE;
|
|
}
|
|
if (!app->nav_active) return EM_FALSE;
|
|
|
|
const float dx = float(e->movementX);
|
|
const float dy = float(e->movementY);
|
|
app->nav_drag_px += std::abs(dx) + std::abs(dy);
|
|
if (app->nav_kind == NavKind::Orbit) app->core.orbitBy(dx, dy);
|
|
else if (app->nav_kind == NavKind::Pan) app->core.panBy(dx, dy, canvasCssHeight());
|
|
// NavKind::Select drag → marquee box-select (next step).
|
|
return EM_TRUE;
|
|
}
|
|
|
|
EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
const bool was_active = app->nav_active;
|
|
const NavKind kind = app->nav_kind;
|
|
app->nav_active = false;
|
|
app->nav_kind = NavKind::None;
|
|
|
|
// Select-button release with no real drag → pick the object under the cursor
|
|
// and route it through selection (Shift add, Ctrl remove, plain replace).
|
|
// Async readback: the highlight appears a frame after the result lands.
|
|
if (was_active && kind == NavKind::Select && app->ready &&
|
|
app->nav_drag_px <= kClickDragThresholdPx) {
|
|
const double dpr = emscripten_get_device_pixel_ratio();
|
|
const int px = int(app->down_x * dpr);
|
|
const int py = int(app->down_y * dpr);
|
|
const bool add = e->shiftKey;
|
|
const bool remove = e->ctrlKey;
|
|
app->core.pickObjectAtAsync(px, py, [app, add, remove](std::uint32_t id) {
|
|
app->core.applyPickToSelection(id, add, remove);
|
|
// Demo the v15 on-demand deferred fetch: log the picked object's
|
|
// IFC GUID (first pick fetches the property block off the network).
|
|
if (id != 0) app->core.logSelectedObjectGuidWeb(id);
|
|
app->host.requestFrame();
|
|
});
|
|
}
|
|
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;
|
|
// In fly mode the wheel tunes move speed (Blender convention), not zoom.
|
|
if (app->fly_mode) { app->core.flyAdjustSpeed(-float(dy) / 100.0f); return EM_TRUE; }
|
|
app->core.dollyBy(-float(dy) / 100.0f);
|
|
return EM_TRUE; // consume so the page doesn't scroll
|
|
}
|
|
|
|
// Track a held fly movement key (W/A/S/D/Q/E/Shift). Returns true if `code` was
|
|
// one. Physical `.code` so it's keymap-independent.
|
|
bool setFlyKey(AppState* app, const char* code, bool down) {
|
|
if (!std::strcmp(code, "KeyW")) app->key_w_pressed = down;
|
|
else if (!std::strcmp(code, "KeyA")) app->key_a_pressed = down;
|
|
else if (!std::strcmp(code, "KeyS")) app->key_s_pressed = down;
|
|
else if (!std::strcmp(code, "KeyD")) app->key_d_pressed = down;
|
|
else if (!std::strcmp(code, "KeyQ")) app->key_q_pressed = down;
|
|
else if (!std::strcmp(code, "KeyE")) app->key_e_pressed = down;
|
|
else if (!std::strcmp(code, "ShiftLeft") || !std::strcmp(code, "ShiftRight"))
|
|
app->key_shift_pressed = down;
|
|
else return false;
|
|
return true;
|
|
}
|
|
|
|
// Enter/leave fly mode: pointer-lock the canvas for mouse-look on enter, release
|
|
// on exit. Shared camera math is ViewportCore::flyMove/flyLook.
|
|
void setFlyMode(AppState* app, bool on) {
|
|
if (app->fly_mode == on) return;
|
|
app->fly_mode = on;
|
|
if (on) {
|
|
app->fly_last_ms = emscripten_get_now();
|
|
emscripten_request_pointerlock(kCanvasSelector, EM_TRUE);
|
|
Log::info() << "[fly] on — WASD/QE move, mouse looks, Shift boosts, wheel = speed, Esc exits";
|
|
} else {
|
|
app->key_w_pressed = app->key_a_pressed = app->key_s_pressed = false;
|
|
app->key_d_pressed = app->key_q_pressed = app->key_e_pressed = false;
|
|
app->key_shift_pressed = false;
|
|
app->fly_locked = false;
|
|
emscripten_exit_pointerlock();
|
|
Log::info() << "[fly] off";
|
|
}
|
|
app->host.requestFrame();
|
|
}
|
|
|
|
// Viewport hotkeys, matching the desktop bindings (ViewportWindow):
|
|
// Home view all · F zoom to selected · P ortho/persp · X/Y/Z (+Shift) views
|
|
// Shift+F enter fly · Esc exit fly · while flying: W/A/S/D/Q/E held + Shift.
|
|
EM_BOOL onKeyDown(int, const EmscriptenKeyboardEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
if (!app->ready) return EM_FALSE;
|
|
const char* code = e->code;
|
|
const bool shift = e->shiftKey;
|
|
// Shift+F toggles fly; Esc leaves it.
|
|
if (!std::strcmp(code, "KeyF") && shift && !e->repeat) { setFlyMode(app, !app->fly_mode); return EM_TRUE; }
|
|
if (!std::strcmp(code, "Escape") && app->fly_mode) { setFlyMode(app, false); return EM_TRUE; }
|
|
// While flying, WASDQE/Shift are held-movement keys, not hotkeys.
|
|
if (app->fly_mode) { if (setFlyKey(app, code, true)) return EM_TRUE; return EM_FALSE; }
|
|
if (e->repeat) return EM_FALSE;
|
|
const bool alt = e->altKey;
|
|
// Visibility + X-ray, matching desktop: H hide selected · Shift+H isolate ·
|
|
// Alt+H show all · Alt+X x-ray.
|
|
if (!std::strcmp(code, "KeyH")) {
|
|
if (alt) app->core.showAll();
|
|
else if (shift) app->core.isolateSelected();
|
|
else app->core.hideSelected();
|
|
return EM_TRUE;
|
|
}
|
|
if (!std::strcmp(code, "KeyX") && alt) { app->core.toggleXray(); return EM_TRUE; }
|
|
using SV = ViewportCore::StandardView;
|
|
if (!std::strcmp(code, "Home")) app->core.viewAll();
|
|
else if (!std::strcmp(code, "KeyF") && !shift) app->core.frameSelection();
|
|
else if (!std::strcmp(code, "KeyP") && !shift) app->core.toggleProjection();
|
|
else if (!std::strcmp(code, "KeyX")) app->core.setStandardView(shift ? SV::Back : SV::Front);
|
|
else if (!std::strcmp(code, "KeyY")) app->core.setStandardView(shift ? SV::Left : SV::Right);
|
|
else if (!std::strcmp(code, "KeyZ")) app->core.setStandardView(shift ? SV::Bottom : SV::Top);
|
|
else return EM_FALSE; // let every other key through to the browser
|
|
app->host.requestFrame();
|
|
return EM_TRUE;
|
|
}
|
|
|
|
EM_BOOL onKeyUp(int, const EmscriptenKeyboardEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
return setFlyKey(app, e->code, false) ? EM_TRUE : EM_FALSE;
|
|
}
|
|
|
|
// Pointer-lock is the fly-look mechanism. When it's LOST (the browser eats the
|
|
// first Esc to release it, or the user clicks out) leave fly mode — this is what
|
|
// makes a single Esc exit cleanly. `fly_locked` guards against a denied lock on
|
|
// entry (headless / permission) firing this and insta-exiting: we only treat a
|
|
// loss as an exit if we'd actually acquired the lock.
|
|
EM_BOOL onPointerLockChange(int, const EmscriptenPointerlockChangeEvent* e, void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
if (e->isActive) {
|
|
app->fly_locked = true;
|
|
} else if (app->fly_locked && app->fly_mode) {
|
|
setFlyMode(app, false);
|
|
}
|
|
return EM_TRUE;
|
|
}
|
|
|
|
// 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);
|
|
emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, app, EM_FALSE, onKeyDown);
|
|
emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, app, EM_FALSE, onKeyUp);
|
|
emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, app, EM_FALSE,
|
|
onPointerLockChange);
|
|
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
|
|
// to JS by EXPORTED_FUNCTIONS in CMakeLists.txt; EMSCRIPTEN_KEEPALIVE
|
|
// also keeps the symbol alive under -O*.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) {
|
|
auto* app = static_cast<AppState*>(user);
|
|
if (!app->ready) return;
|
|
|
|
int w = 0, h = 0;
|
|
app->host.framebufferSize(w, h);
|
|
if (w != app->last_w || h != app->last_h) {
|
|
app->core.configureSurface(w, h);
|
|
app->last_w = w;
|
|
app->last_h = h;
|
|
}
|
|
|
|
// Fly mode: integrate held-key movement each frame (dt from wall clock).
|
|
// flyMove schedules a frame when it actually moves, so a still fly camera
|
|
// costs nothing.
|
|
if (app->fly_mode) {
|
|
const double now = emscripten_get_now();
|
|
const float dt = float((now - app->fly_last_ms) / 1000.0);
|
|
app->fly_last_ms = now;
|
|
app->core.flyMove(app->key_w_pressed, app->key_s_pressed,
|
|
app->key_d_pressed, app->key_a_pressed,
|
|
app->key_e_pressed, app->key_q_pressed,
|
|
app->key_shift_pressed, dt);
|
|
}
|
|
|
|
if (app->host.consumeFrameRequest()) {
|
|
app->core.render();
|
|
}
|
|
}
|
|
|
|
// Stream a sidecar from a registered JS byte-source and APPEND it to the scene
|
|
// (federation). shell.html registers the source first — a picked File or a
|
|
// remote URL, sized up front — into Module.__ifcvSources[source_id], then calls
|
|
// this. Byte-range: the file is never copied whole into the wasm heap; metadata
|
|
// is read via ranges and chunks stream per-chunk, so a 500 MB sidecar stays in
|
|
// the File / on the server. Asynchronous; the model frames itself from the JS
|
|
// completion callback. Call clear_scene_c first to replace instead of append.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void load_sidecar_from_source_c(int source_id) {
|
|
if (!g_app || !g_app->ready) return;
|
|
g_app->core.loadSidecarMetadataWeb(source_id, "source");
|
|
}
|
|
|
|
// Drop all loaded models (used by shell.html to replace the embedded sample /
|
|
// a prior federation before loading a fresh set).
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void clear_scene_c() {
|
|
if (!g_app || !g_app->ready) return;
|
|
g_app->core.resetScene();
|
|
}
|
|
|
|
// Viewport-navigation entry points for the shell.html toolbar (buttons that
|
|
// mirror the keyboard hotkeys). Each schedules a frame.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void view_all_c() {
|
|
if (!g_app || !g_app->ready) return;
|
|
g_app->core.viewAll(); g_app->host.requestFrame();
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void frame_selection_c() {
|
|
if (!g_app || !g_app->ready) return;
|
|
g_app->core.frameSelection(); g_app->host.requestFrame();
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void toggle_projection_c() {
|
|
if (!g_app || !g_app->ready) return;
|
|
g_app->core.toggleProjection(); g_app->host.requestFrame();
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int projection_is_ortho_c() {
|
|
return (g_app && g_app->ready && g_app->core.projectionOrtho()) ? 1 : 0;
|
|
}
|
|
// Toggle fly (first-person) mode from the toolbar. The button click is a user
|
|
// gesture, so the pointer-lock request inside succeeds.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void toggle_fly_c() {
|
|
if (g_app && g_app->ready) setFlyMode(g_app, !g_app->fly_mode);
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int fly_is_active_c() {
|
|
return (g_app && g_app->ready && g_app->fly_mode) ? 1 : 0;
|
|
}
|
|
|
|
// Visibility + X-ray, for the toolbar (same ops as the H/Shift+H/Alt+H/Alt+X keys).
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void hide_selected_c() { if (g_app && g_app->ready) g_app->core.hideSelected(); }
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void isolate_selected_c() { if (g_app && g_app->ready) g_app->core.isolateSelected(); }
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void show_all_c() { if (g_app && g_app->ready) g_app->core.showAll(); }
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void toggle_xray_c() { if (g_app && g_app->ready) g_app->core.toggleXray(); }
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int xray_is_active_c() { return (g_app && g_app->ready && g_app->core.xrayActive()) ? 1 : 0; }
|
|
|
|
// id: 0 Front, 1 Back, 2 Left, 3 Right, 4 Top, 5 Bottom.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE void standard_view_c(int id) {
|
|
if (!g_app || !g_app->ready || id < 0 || id > 5) return;
|
|
using SV = ViewportCore::StandardView;
|
|
static const SV map[6] = { SV::Front, SV::Back, SV::Left, SV::Right, SV::Top, SV::Bottom };
|
|
g_app->core.setStandardView(map[id]);
|
|
g_app->host.requestFrame();
|
|
}
|
|
|
|
// Streaming progress for the loading bar (shell.html polls these each frame).
|
|
// total == 0 while still fetching metadata; resident climbs to total as
|
|
// geometry chunks arrive.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_chunks_resident_c() {
|
|
if (!g_app) return 0;
|
|
int resident_chunks = 0, total_chunks = 0;
|
|
g_app->core.streamingProgress(resident_chunks, total_chunks);
|
|
return resident_chunks;
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_chunks_total_c() {
|
|
if (!g_app) return 0;
|
|
int resident_chunks = 0, total_chunks = 0;
|
|
g_app->core.streamingProgress(resident_chunks, total_chunks);
|
|
return total_chunks;
|
|
}
|
|
|
|
// Per-model progress for the federation loading panel: how many models are in
|
|
// the scene, and the idx-th model's resident/total chunks (idx ordered by load).
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_count_c() {
|
|
return g_app ? g_app->core.streamingModelCount() : 0;
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_resident_c(int idx) {
|
|
if (!g_app) return 0;
|
|
int resident_chunks = 0, total_chunks = 0;
|
|
g_app->core.streamingModelProgress(idx, resident_chunks, total_chunks);
|
|
return resident_chunks;
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_total_c(int idx) {
|
|
if (!g_app) return 0;
|
|
int resident_chunks = 0, total_chunks = 0;
|
|
g_app->core.streamingModelProgress(idx, resident_chunks, total_chunks);
|
|
return total_chunks;
|
|
}
|
|
|
|
// Combined byte progress for the loading bar: total geometry, bytes the current
|
|
// view needs (contribution-culled), and how much of that is loaded. Doubles so
|
|
// JS gets exact byte counts well past 2 GB.
|
|
extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_total_c() {
|
|
if (!g_app) return 0.0;
|
|
std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0;
|
|
g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes);
|
|
return double(total_bytes);
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_needed_c() {
|
|
if (!g_app) return 0.0;
|
|
std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0;
|
|
g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes);
|
|
return double(needed_bytes);
|
|
}
|
|
extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_loaded_c() {
|
|
if (!g_app) return 0.0;
|
|
std::uint64_t total_bytes = 0, needed_bytes = 0, loaded_bytes = 0;
|
|
g_app->core.streamingByteProgress(total_bytes, needed_bytes, loaded_bytes);
|
|
return double(loaded_bytes);
|
|
}
|
|
|
|
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.
|
|
g_app->core.setNavPreset("web");
|
|
g_app->core.initWgpuAsyncWeb([](bool ok) {
|
|
if (!ok) {
|
|
Log::warn() << "ifcviewer-web: wgpu init failed";
|
|
return;
|
|
}
|
|
if (!g_app->core.buildPipelines()) {
|
|
Log::warn() << "ifcviewer-web: buildPipelines failed";
|
|
return;
|
|
}
|
|
// HiZ + edge + pick pipelines: built up-front to match the
|
|
// desktop path. ViewportCore::shutdown expects each resource
|
|
// to be either constructed or null, so building them all here
|
|
// keeps teardown symmetric.
|
|
g_app->core.buildHizPipeline();
|
|
g_app->core.buildEdgePipeline();
|
|
g_app->core.buildPickPipeline();
|
|
|
|
// Load the embedded sample sidecar (mounted into MEMFS via
|
|
// --embed-file in CMakeLists.txt). The sample stays on the
|
|
// synchronous MEMFS read; user-picked files go through the
|
|
// Blob.slice byte-range path (load_sidecar_from_blob_c) so large
|
|
// sidecars never enter the wasm heap.
|
|
if (!g_app->core.loadSidecarFromPath("/sample.ifcview")) {
|
|
Log::warn() << "ifcviewer-web: sample sidecar load failed";
|
|
}
|
|
|
|
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.
|
|
EM_ASM({ Module._app_ptr = $0; }, (void*)g_app);
|
|
});
|
|
return 0;
|
|
}
|