mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 19:52:36 +00:00
ifcviewer-web: streaming loading bar (metadata → geometry progress)
Users had no feedback during the seconds before/while a model streams. Add a top progress strip + caption driven by the streaming state: - "Loading model… N MB" while the critical metadata downloads (no chunks yet), - "Loading geometry — R / T chunks · N MB" as chunks go resident, - "Loaded — T chunks · N MB", then it hides. ViewportCore::streamingProgress(resident, total) sums chunk residency across models; main_web exports ifcv_chunks_resident_c / ifcv_chunks_total_c for shell.html to poll each RAF. The EM_JS range reader accumulates Module.__ifcvBytesLoaded so the caption can show MB downloaded. Shown only for streamed loads (?model= URL / picked file), not the embedded sample. 6/6 web smoke pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ target_link_options(IfcViewerWeb PRIVATE
|
|||||||
# EMSCRIPTEN_KEEPALIVE alone keeps the symbols in the binary but doesn't
|
# 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
|
# 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.
|
# 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']"
|
"-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']"
|
||||||
# ccall: shell.html passes the ?model URL string to load_sidecar_from_url_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
|
# 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.
|
# sidecar streams by range instead of loading whole). Standard, zero-cost.
|
||||||
|
|||||||
@@ -216,6 +216,18 @@ extern "C" EMSCRIPTEN_KEEPALIVE void load_sidecar_from_url_c(const char* url) {
|
|||||||
g_app->core.loadSidecarFromUrlWeb(url);
|
g_app->core.loadSidecarFromUrlWeb(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 r = 0, t = 0; g_app->core.streamingProgress(r, t); return r;
|
||||||
|
}
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_chunks_total_c() {
|
||||||
|
if (!g_app) return 0;
|
||||||
|
int r = 0, t = 0; g_app->core.streamingProgress(r, t); return t;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int /*argc*/, char** /*argv*/) {
|
int main(int /*argc*/, char** /*argv*/) {
|
||||||
Log::info() << "ifcviewer-web: starting";
|
Log::info() << "ifcviewer-web: starting";
|
||||||
g_app = new AppState();
|
g_app = new AppState();
|
||||||
|
|||||||
@@ -26,10 +26,22 @@
|
|||||||
border-radius: 4px; font-size: 12px; cursor: pointer; }
|
border-radius: 4px; font-size: 12px; cursor: pointer; }
|
||||||
#open-btn:hover { background: #3182ce; }
|
#open-btn:hover { background: #3182ce; }
|
||||||
#file-input { display: none; }
|
#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. */
|
||||||
|
#progress { position: fixed; top: 0; left: 0; right: 0; height: 3px;
|
||||||
|
background: rgba(43,108,176,.2); z-index: 20; display: none; }
|
||||||
|
#progress-fill { height: 100%; width: 0%; background: #3182ce;
|
||||||
|
transition: width .15s ease; }
|
||||||
|
#progress-text { position: fixed; top: 10px; left: 50%;
|
||||||
|
transform: translateX(-50%); z-index: 20; font-size: 12px;
|
||||||
|
background: rgba(20,22,28,.85); padding: 4px 12px; border-radius: 4px;
|
||||||
|
pointer-events: none; display: none; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="viewer-canvas" width="1280" height="800"></canvas>
|
<canvas id="viewer-canvas" width="1280" height="800"></canvas>
|
||||||
|
<div id="progress"><div id="progress-fill"></div></div>
|
||||||
|
<div id="progress-text"></div>
|
||||||
<button id="open-btn">Open .ifcview…</button>
|
<button id="open-btn">Open .ifcview…</button>
|
||||||
<input id="file-input" type="file" accept=".ifcview">
|
<input id="file-input" type="file" accept=".ifcview">
|
||||||
<div id="status">Starting…</div>
|
<div id="status">Starting…</div>
|
||||||
@@ -83,8 +95,10 @@
|
|||||||
// require the host to send CORS + Accept-Ranges headers.
|
// require the host to send CORS + Accept-Ranges headers.
|
||||||
if (!urlLoadTried && modelUrl && Module.ccall) {
|
if (!urlLoadTried && modelUrl && Module.ccall) {
|
||||||
urlLoadTried = true;
|
urlLoadTried = true;
|
||||||
|
window.beginLoadProgress();
|
||||||
Module.ccall('load_sidecar_from_url_c', null, ['string'], [modelUrl]);
|
Module.ccall('load_sidecar_from_url_c', null, ['string'], [modelUrl]);
|
||||||
}
|
}
|
||||||
|
window.updateLoadProgress();
|
||||||
Module._raf_tick_c(Module._app_ptr);
|
Module._raf_tick_c(Module._app_ptr);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(shellTick);
|
requestAnimationFrame(shellTick);
|
||||||
@@ -97,6 +111,44 @@
|
|||||||
statusEl.classList.add('error');
|
statusEl.classList.add('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Streaming loading bar ------------------------------------------------
|
||||||
|
// Driven by the C-side progress exports (resident/total chunks) + the
|
||||||
|
// bytes-downloaded counter the EM_JS range reader maintains. Shown only for
|
||||||
|
// streamed loads (URL / picked file), not the tiny embedded sample.
|
||||||
|
var progEl = document.getElementById('progress');
|
||||||
|
var fillEl = document.getElementById('progress-fill');
|
||||||
|
var textEl = document.getElementById('progress-text');
|
||||||
|
var loadActive = false;
|
||||||
|
window.beginLoadProgress = function() {
|
||||||
|
loadActive = true;
|
||||||
|
Module.__ifcvBytesLoaded = 0;
|
||||||
|
fillEl.style.width = '0%';
|
||||||
|
progEl.style.display = 'block';
|
||||||
|
textEl.style.display = 'block';
|
||||||
|
textEl.textContent = 'Loading model…';
|
||||||
|
};
|
||||||
|
function endLoadProgress() { progEl.style.display = 'none'; textEl.style.display = 'none'; }
|
||||||
|
window.updateLoadProgress = function() {
|
||||||
|
if (!loadActive) return;
|
||||||
|
var R = Module._ifcv_chunks_resident_c ? Module._ifcv_chunks_resident_c() : 0;
|
||||||
|
var T = Module._ifcv_chunks_total_c ? Module._ifcv_chunks_total_c() : 0;
|
||||||
|
var mb = ((Module.__ifcvBytesLoaded || 0) / 1e6).toFixed(1);
|
||||||
|
if (T === 0) { // still fetching critical metadata
|
||||||
|
fillEl.style.width = '6%';
|
||||||
|
textEl.textContent = 'Loading model… ' + mb + ' MB';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pct = Math.round(100 * R / T);
|
||||||
|
fillEl.style.width = pct + '%';
|
||||||
|
if (R >= T) { // all chunks resident → done
|
||||||
|
textEl.textContent = 'Loaded — ' + T + ' chunks · ' + mb + ' MB';
|
||||||
|
loadActive = false;
|
||||||
|
setTimeout(endLoadProgress, 900);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textEl.textContent = 'Loading geometry — ' + R + ' / ' + T + ' chunks · ' + mb + ' MB';
|
||||||
|
};
|
||||||
|
|
||||||
// File-browse loading (#88, byte-range). The picked File object is stashed
|
// File-browse loading (#88, byte-range). The picked File object is stashed
|
||||||
// on Module.__ifcvFile and load_sidecar_from_blob_c reads it lazily via
|
// on Module.__ifcvFile and load_sidecar_from_blob_c reads it lazily via
|
||||||
// Blob.slice — the file is NOT copied into the wasm heap, so a 500 MB
|
// Blob.slice — the file is NOT copied into the wasm heap, so a 500 MB
|
||||||
@@ -116,6 +168,7 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Module.__ifcvFile = f; // kept alive for lazy Blob.slice reads
|
Module.__ifcvFile = f; // kept alive for lazy Blob.slice reads
|
||||||
|
window.beginLoadProgress();
|
||||||
Module._load_sidecar_from_blob_c();
|
Module._load_sidecar_from_blob_c();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent += 'load failed: ' + e + '\n';
|
statusEl.textContent += 'load failed: ' + e + '\n';
|
||||||
|
|||||||
@@ -3219,7 +3219,10 @@ EM_JS(double, ifcvFileSize, (void), {
|
|||||||
// works (just without the bandwidth saving).
|
// works (just without the bandwidth saving).
|
||||||
EM_JS(void, ifcvReadRangeInto, (int reqId, double offset, double size, void* dst), {
|
EM_JS(void, ifcvReadRangeInto, (int reqId, double offset, double size, void* dst), {
|
||||||
var deliver = function(ok, buf) {
|
var deliver = function(ok, buf) {
|
||||||
if (ok && buf) HEAPU8.set(new Uint8Array(buf), dst);
|
if (ok && buf) {
|
||||||
|
HEAPU8.set(new Uint8Array(buf), dst);
|
||||||
|
Module["__ifcvBytesLoaded"] = (Module["__ifcvBytesLoaded"] || 0) + buf.byteLength;
|
||||||
|
}
|
||||||
Module["_ifcv_on_range_done"](reqId, ok ? 1 : 0);
|
Module["_ifcv_on_range_done"](reqId, ok ? 1 : 0);
|
||||||
};
|
};
|
||||||
var f = Module["__ifcvFile"];
|
var f = Module["__ifcvFile"];
|
||||||
@@ -3608,6 +3611,17 @@ void ViewportCore::loadDeferredMetadataWeb(std::uint32_t model_id,
|
|||||||
}
|
}
|
||||||
#endif // __EMSCRIPTEN__
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
|
void ViewportCore::streamingProgress(int& resident_chunks, int& total_chunks) const {
|
||||||
|
resident_chunks = 0;
|
||||||
|
total_chunks = 0;
|
||||||
|
for (const auto& [mid, m] : models_gpu_) {
|
||||||
|
for (const auto& c : m.chunks) {
|
||||||
|
++total_chunks;
|
||||||
|
if (c.is_resident) ++resident_chunks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
void ViewportCore::finalizeModel(std::uint32_t model_id) {
|
||||||
auto it = pending_direct_loads_.find(model_id);
|
auto it = pending_direct_loads_.find(model_id);
|
||||||
if (it == pending_direct_loads_.end()) {
|
if (it == pending_direct_loads_.end()) {
|
||||||
|
|||||||
@@ -386,6 +386,11 @@ public:
|
|||||||
void beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_idx);
|
void beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_idx);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Streaming progress for a loading UI: resident vs total streaming chunks
|
||||||
|
// across all models. total == 0 means no streaming model is set up yet
|
||||||
|
// (still in the metadata phase). Cheap; safe to poll every frame.
|
||||||
|
void streamingProgress(int& resident_chunks, int& total_chunks) const;
|
||||||
|
|
||||||
// Direct-load (bonsai-side) entry points. Bonsai's SceneLoader feeds
|
// Direct-load (bonsai-side) entry points. Bonsai's SceneLoader feeds
|
||||||
// the viewer one mesh + one instance at a time, then calls
|
// the viewer one mesh + one instance at a time, then calls
|
||||||
// finalizeModel once everything's staged. The staging map lives on
|
// finalizeModel once everything's staged. The staging map lives on
|
||||||
|
|||||||
Reference in New Issue
Block a user