From e3722cf243e1286b6a932eda756b79ae254e1f79 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 29 Jun 2026 11:19:49 +1000 Subject: [PATCH] ifcviewer-web: load user sidecars via a file-browse button (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Open .ifcview…" button that opens the browser's native file chooser. The picked file's bytes are written into MEMFS and a new exported entry point, load_uploaded_model_c, reads them back via the existing loadSidecarFromPath: it resetScene()s the current model (replace, not append), loads the sidecar, and viewAll()s it. Geometry becomes resident over subsequent frames through render()'s inline driveStreamingLoads, same as the embedded sample. Deliberately a file picker, not drag-drop: browser file drag-drop needs an X11 drag source (a file manager) to drag *from*, which a minimal WM (ratpoison) doesn't provide. The native chooser is WM-independent. Exports _load_uploaded_model_c and the FS runtime method; URL/byte-range fetch of remote models stays for #88. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer-web/CMakeLists.txt | 15 +++++++++---- src/ifcviewer-web/main_web.cpp | 26 ++++++++++++++++++++++ src/ifcviewer-web/shell.html | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/ifcviewer-web/CMakeLists.txt b/src/ifcviewer-web/CMakeLists.txt index 2ecff3b225..fdcaab0598 100644 --- a/src/ifcviewer-web/CMakeLists.txt +++ b/src/ifcviewer-web/CMakeLists.txt @@ -96,10 +96,17 @@ target_link_options(IfcViewerWeb PRIVATE # main() to set noExitRuntime as a side effect — registers a RAF # that starves the device promise (observed: ~10s delay in Firefox). "-sEXIT_RUNTIME=0" - # Expose the C tick entry point to JS so shell.html's RAF loop - # can call Module._raf_tick_c. EMSCRIPTEN_KEEPALIVE alone keeps - # the symbol in the binary but doesn't add it to Module. - "-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c']" + # Expose the C entry points to JS. _raf_tick_c drives the RAF loop + # (shell.html); _load_uploaded_model_c loads a user-picked sidecar + # that JS has written into MEMFS. EMSCRIPTEN_KEEPALIVE alone keeps + # the symbols in the binary but doesn't add them to Module. + "-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c','_load_uploaded_model_c']" + # FS lets shell.html's file-browse handler write the picked file's + # bytes into MEMFS before calling _load_uploaded_model_c (which + # fopen()s the virtual path). Drag-drop is intentionally not used — + # it depends on an X11 drag source (file manager), which a minimal + # WM may not provide; the native file chooser is WM-independent. + "-sEXPORTED_RUNTIME_METHODS=['FS']" # Streaming + chunked geometry want a heap that can grow as buffers # arrive. 256 MB initial, 2 GB ceiling (matches the wasm32 pointer # cap; --shared64 / MEMORY64 would lift this later if we need it). diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp index 6a4abf48c0..5a9cc7b0a4 100644 --- a/src/ifcviewer-web/main_web.cpp +++ b/src/ifcviewer-web/main_web.cpp @@ -41,6 +41,10 @@ namespace { // WebViewportHost selector below. constexpr const char* kCanvasSelector = "#viewer-canvas"; +// MEMFS path the file-browse handler writes the picked sidecar to, and +// that load_uploaded_model_c reads back. Must match shell.html. +constexpr const char* kUploadPath = "/uploads/model.ifcview"; + struct AppState { WebViewportHost host{ kCanvasSelector }; ViewportCore core{ &host }; @@ -152,6 +156,28 @@ extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) { } } +// Called from shell.html's file-browse handler after it has written the +// picked file's bytes into MEMFS at kUploadPath. Replaces whatever is +// currently loaded (the embedded sample on first use, or a prior upload) +// with the new sidecar and frames it. Geometry becomes resident over the +// next frames via render()'s inline driveStreamingLoads. Exported to JS +// via EXPORTED_FUNCTIONS in CMakeLists.txt. +extern "C" EMSCRIPTEN_KEEPALIVE void load_uploaded_model_c() { + if (!g_app || !g_app->ready) return; + + // resetScene drops the previous model's GPU resources so a fresh load + // replaces rather than accumulates (loadSidecarFromPath appends). + g_app->core.resetScene(); + const unsigned int mid = g_app->core.loadSidecarFromPath(kUploadPath); + if (mid == 0) { + Log::warn() << "ifcviewer-web: uploaded model load failed"; + return; + } + g_app->core.viewAll(); + g_app->host.requestFrame(); + Log::info() << "ifcviewer-web: loaded uploaded model (id " << mid << ")"; +} + int main(int /*argc*/, char** /*argv*/) { Log::info() << "ifcviewer-web: starting"; g_app = new AppState(); diff --git a/src/ifcviewer-web/shell.html b/src/ifcviewer-web/shell.html index 792f83a952..ccf2bd85c8 100644 --- a/src/ifcviewer-web/shell.html +++ b/src/ifcviewer-web/shell.html @@ -14,10 +14,17 @@ background: rgba(20,22,28,.78); padding: 6px 10px; border-radius: 4px; white-space: pre-wrap; pointer-events: auto; } #status.error { background: rgba(120,30,30,.85); color: #fff; } + #open-btn { position: fixed; top: 8px; right: 12px; z-index: 10; + background: #2b6cb0; color: #fff; border: none; padding: 6px 12px; + border-radius: 4px; font-size: 12px; cursor: pointer; } + #open-btn:hover { background: #3182ce; } + #file-input { display: none; } + +
Starting…