From 4c38e52741fb2911e2ab11233ed3963ec59263e1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 1 Jul 2026 10:03:24 +1000 Subject: [PATCH] ifcviewer-web: multi-file loading (federation) via a per-model byte-source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scene core is already multi-model — models_gpu_ is a map, applyCachedModel APPENDS, and per-model model_id / object_id rebasing / georef+transformation are how the desktop federates today. The only web-specific gap was the byte source: web had ONE global source (__ifcvFile/__ifcvUrl) and reset the scene on every load, so it could show one file at a time. Desktop meanwhile carries a per-model source (streaming_file_path). Mirror that on web: give each model its own web_source_id into a JS source registry (Module.__ifcvSources[id] = a picked File or a sized remote URL). beginWebChunkLoad, the metadata bootstrap, and the on-demand deferred fetch all read from the owning model's source, so several files stream concurrently into one federated scene — reusing all the shared machinery (viewAll, picking, the GUID fetch) untouched. - webReadRangesAsync / ifcvReadRangeInto / ifcvSourceSize take a source id. - loadSidecarMetadataWeb(source_id, …) appends (no resetScene); main_web exposes load_sidecar_from_source_c(id) + clear_scene_c(). - URL size resolution moved to JS (shell.html registers + sizes sources via HEAD/Range), retiring the C-side ifcvBeginUrlSource / ifcv_source_ready dance. - shell.html: source registry + "Open" (replace) / "Add" (append) buttons, multi-file selection; ?model= registers a URL source then loads. Verified: two sidecars from two sources stream into one scene, both fully resident, zero GPU errors. 111/111 unit + 6/6 web smoke pass. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer-web/CMakeLists.txt | 2 +- src/ifcviewer-web/main_web.cpp | 35 +++---- src/ifcviewer-web/shell.html | 67 ++++++++++-- src/ifcviewer-web/tests/smoke.spec.mjs | 4 +- src/ifcviewer/ModelGpuData.h | 5 + src/ifcviewer/ViewportCore.cpp | 136 ++++++++----------------- src/ifcviewer/ViewportCore.h | 14 ++- 7 files changed, 125 insertions(+), 138 deletions(-) diff --git a/src/ifcviewer-web/CMakeLists.txt b/src/ifcviewer-web/CMakeLists.txt index b48348c387..f3eecfb032 100644 --- a/src/ifcviewer-web/CMakeLists.txt +++ b/src/ifcviewer-web/CMakeLists.txt @@ -104,7 +104,7 @@ target_link_options(IfcViewerWeb PRIVATE # EMSCRIPTEN_KEEPALIVE alone keeps the symbols in the binary but doesn't # add them to Module. ccall lets shell.html pass a JS string (the ?model # URL) to load_sidecar_from_url_c without manual heap marshalling. - "-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c','_load_sidecar_from_blob_c','_load_sidecar_from_url_c','_ifcv_on_range_done','_ifcv_source_ready','_ifcv_chunks_resident_c','_ifcv_chunks_total_c']" + "-sEXPORTED_FUNCTIONS=['_main','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c']" # ccall: shell.html passes the ?model URL string to load_sidecar_from_url_c. # HEAPU8: lets tooling/tests read the wasm heap size (e.g. to verify a large # sidecar streams by range instead of loading whole). Standard, zero-cost. diff --git a/src/ifcviewer-web/main_web.cpp b/src/ifcviewer-web/main_web.cpp index 031ceb6803..b1a1b7f029 100644 --- a/src/ifcviewer-web/main_web.cpp +++ b/src/ifcviewer-web/main_web.cpp @@ -191,32 +191,23 @@ extern "C" EMSCRIPTEN_KEEPALIVE void raf_tick_c(void* user) { } } -// Called from shell.html's file-browse handler after it has stashed the -// picked File on Module.__ifcvFile. Replaces whatever is currently loaded -// (the embedded sample on first use, or a prior pick) with the new sidecar. -// Byte-range (#88): the whole file is NOT copied into the wasm heap — the -// metadata is read via Blob.slice and chunk bytes stream per-chunk, so a -// 500 MB sidecar stays in the browser File object. Asynchronous: this -// returns immediately and the model frames itself from the JS completion -// callback. Exported to JS via EXPORTED_FUNCTIONS in CMakeLists.txt. -extern "C" EMSCRIPTEN_KEEPALIVE void load_sidecar_from_blob_c() { +// 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; - - // resetScene drops the previous model's GPU resources so a fresh load - // replaces rather than accumulates (loadSidecar* appends). - g_app->core.resetScene(); - g_app->core.loadSidecarFromBlobWeb(); + g_app->core.loadSidecarMetadataWeb(source_id, "source"); } -// Called from shell.html (e.g. a ?model=URL query param) to stream a sidecar -// hosted at `url` via HTTP Range requests — the same per-chunk byte-range path -// as the local File load, but the bytes come off the network instead of a -// Blob. Asynchronous; the model frames itself once metadata lands. Exported -// to JS via EXPORTED_FUNCTIONS in CMakeLists.txt. -extern "C" EMSCRIPTEN_KEEPALIVE void load_sidecar_from_url_c(const char* url) { - if (!g_app || !g_app->ready || !url) return; +// 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(); - g_app->core.loadSidecarFromUrlWeb(url); } // Streaming progress for the loading bar (shell.html polls these each frame). diff --git a/src/ifcviewer-web/shell.html b/src/ifcviewer-web/shell.html index 8254d22b15..fc1d14343a 100644 --- a/src/ifcviewer-web/shell.html +++ b/src/ifcviewer-web/shell.html @@ -21,10 +21,13 @@ #status.error { background: rgba(120,30,30,.85); color: #fff; } /* Errors re-expand and re-opaque even after the ready-collapse. */ #status.ready.error { max-height: 28vh; opacity: 1; } - #open-btn { position: fixed; top: 8px; right: 12px; z-index: 10; + #open-btn, #add-btn { position: fixed; top: 8px; z-index: 10; background: #2b6cb0; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; } + #open-btn { right: 12px; } + #add-btn { right: 120px; background: #2d3748; } #open-btn:hover { background: #3182ce; } + #add-btn:hover { background: #3b465c; } #file-input { display: none; } /* Streaming loading bar: a thin top progress strip + a centred caption. Shown only while a network/file model streams; hidden once resident. */ @@ -42,8 +45,9 @@
+ - +
Starting…