ifcviewer-web: load user sidecars via a file-browse button (#46)

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 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-29 11:19:49 +10:00
parent d36d241da2
commit e3722cf243
3 changed files with 75 additions and 4 deletions
+11 -4
View File
@@ -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).
+26
View File
@@ -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();
+38
View File
@@ -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; }
</style>
</head>
<body>
<canvas id="viewer-canvas" width="1280" height="800"></canvas>
<button id="open-btn">Open .ifcview…</button>
<input id="file-input" type="file" accept=".ifcview">
<div id="status">Starting…</div>
<script>
// Emscripten Module hook: route stderr to the status overlay so any
@@ -69,6 +76,37 @@
statusEl.textContent = 'navigator.gpu is missing — open in a browser with WebGPU enabled';
statusEl.classList.add('error');
}
// File-browse loading. The picked file's bytes are written into MEMFS
// (the same virtual FS the wasm fopen()s) and then load_uploaded_model_c
// is invoked to read + render it. No drag-drop: that needs an X11 drag
// source (a file manager), which a minimal WM may not provide; the
// native file chooser this button opens is WM-independent.
var openBtn = document.getElementById('open-btn');
var fileInput = document.getElementById('file-input');
openBtn.addEventListener('click', function() { fileInput.click(); });
fileInput.addEventListener('change', function(ev) {
var f = ev.target.files && ev.target.files[0];
if (!f) return;
if (!Module.FS || !Module._load_uploaded_model_c) {
statusEl.textContent += 'viewer not ready yet — wait for WebGPU init\n';
return;
}
var reader = new FileReader();
reader.onload = function() {
try {
var bytes = new Uint8Array(reader.result);
try { Module.FS.mkdir('/uploads'); } catch (e) { /* already exists */ }
Module.FS.writeFile('/uploads/model.ifcview', bytes);
Module._load_uploaded_model_c();
} catch (e) {
statusEl.textContent += 'load failed: ' + e + '\n';
statusEl.classList.add('error');
}
fileInput.value = ''; // let the same file be re-picked
};
reader.readAsArrayBuffer(f);
});
</script>
<!-- IfcViewerWeb.js is emitted alongside this shell by the emcc build;
`--shell-file` injects this HTML around it. -->