ifcviewer-web: multi-file loading (federation) via a per-model byte-source

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 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-01 10:03:24 +10:00
parent 681de6f817
commit 4c38e52741
7 changed files with 125 additions and 138 deletions
+1 -1
View File
@@ -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.
+13 -22
View File
@@ -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).
+57 -10
View File
@@ -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 @@
<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="add-btn" title="Add file(s) to the current scene (federation)">Add</button>
<button id="open-btn">Open .ifcview…</button>
<input id="file-input" type="file" accept=".ifcview">
<input id="file-input" type="file" accept=".ifcview" multiple>
<div id="status">Starting…</div>
<script>
// Emscripten Module hook: route stderr to the status overlay so any
@@ -93,10 +97,16 @@
// ?model=URL streams a remote sidecar via HTTP Range once the app
// is live (one-shot). Same-origin needs no CORS; cross-origin URLs
// require the host to send CORS + Accept-Ranges headers.
if (!urlLoadTried && modelUrl && Module.ccall) {
if (!urlLoadTried && modelUrl && Module._load_sidecar_from_source_c) {
urlLoadTried = true;
window.beginLoadProgress();
Module.ccall('load_sidecar_from_url_c', null, ['string'], [modelUrl]);
registerUrlSource(modelUrl).then(function(sid) {
Module._clear_scene_c(); // replace the embedded sample
Module._load_sidecar_from_source_c(sid);
}).catch(function(e) {
statusEl.textContent += 'url load failed: ' + e + '\n';
statusEl.classList.add('error');
});
}
window.updateLoadProgress();
Module._raf_tick_c(Module._app_ptr);
@@ -111,6 +121,33 @@
statusEl.classList.add('error');
}
// --- Byte-source registry (multi-file federation) -------------------------
// Each model streams from its own source: a picked File (Blob.slice) or a
// remote URL (HTTP Range), registered here and read lazily by the wasm side
// via Module.__ifcvSources[id]. URLs are sized up front (HEAD, else a 0-0
// Range's Content-Range) so the loader can bound its reads.
Module.__ifcvSources = Module.__ifcvSources || [];
function registerFileSource(file) {
var sid = Module.__ifcvSources.length;
Module.__ifcvSources.push({ file: file, url: null, size: file.size });
return sid;
}
function registerUrlSource(url) {
return fetch(url, { method: 'HEAD' }).then(function(resp) {
var len = resp.ok ? parseInt(resp.headers.get('Content-Length') || '0', 10) : 0;
if (len > 0) return len;
return fetch(url, { headers: { Range: 'bytes=0-0' } }).then(function(r2) {
var cr = r2.headers.get('Content-Range'); // "bytes 0-0/12345"
return cr ? parseInt(cr.split('/')[1] || '0', 10) : 0;
});
}).then(function(size) {
if (!size) throw new Error('could not size ' + url + ' (need HEAD or Range)');
var sid = Module.__ifcvSources.length;
Module.__ifcvSources.push({ file: null, url: url, size: size });
return sid;
});
}
// --- 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
@@ -156,20 +193,30 @@
// ever cross into wasm. 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.
// "Open" replaces the scene with the picked file(s); "Add" appends them to
// the current scene (federation). Multiple files can be picked at once. Each
// File is registered as its own byte-source (kept alive in __ifcvSources for
// lazy Blob.slice reads) and streamed independently.
var openBtn = document.getElementById('open-btn');
var addBtn = document.getElementById('add-btn');
var fileInput = document.getElementById('file-input');
openBtn.addEventListener('click', function() { fileInput.click(); });
var pendingMode = 'replace';
openBtn.addEventListener('click', function() { pendingMode = 'replace'; fileInput.click(); });
addBtn.addEventListener('click', function() { pendingMode = 'add'; fileInput.click(); });
fileInput.addEventListener('change', function(ev) {
var f = ev.target.files && ev.target.files[0];
if (!f) return;
if (!Module._load_sidecar_from_blob_c) {
var files = ev.target.files;
if (!files || !files.length) return;
if (!Module._load_sidecar_from_source_c) {
statusEl.textContent += 'viewer not ready yet — wait for WebGPU init\n';
return;
}
try {
Module.__ifcvFile = f; // kept alive for lazy Blob.slice reads
if (pendingMode === 'replace') Module._clear_scene_c();
window.beginLoadProgress();
Module._load_sidecar_from_blob_c();
for (var i = 0; i < files.length; i++) {
var sid = registerFileSource(files[i]);
Module._load_sidecar_from_source_c(sid);
}
} catch (e) {
statusEl.textContent += 'load failed: ' + e + '\n';
statusEl.classList.add('error');
+2 -2
View File
@@ -167,7 +167,7 @@ test('loads a user-picked sidecar through the Blob.slice byte-range path', async
// side to confirm the blob load landed (logged to stderr → console).
const samplePath = resolve(__dirname, '..', 'sample.ifcview');
const loaded = page.waitForEvent('console', {
predicate: (m) => /loaded sidecar \(blob:/.test(m.text()),
predicate: (m) => /loaded sidecar \(source/.test(m.text()),
timeout: 15_000,
});
await page.locator('#file-input').setInputFiles(samplePath);
@@ -208,7 +208,7 @@ test('streams a remote sidecar over HTTP Range (?model= URL backend)', async ({
// Wait for the C side to confirm the URL-sourced load landed.
const loaded = page.waitForEvent('console', {
predicate: (m) => /loaded sidecar \(net:/.test(m.text()),
predicate: (m) => /loaded sidecar \(source/.test(m.text()),
timeout: 20_000,
});
await page.goto('/IfcViewerWeb.html?model=/sample.ifcview');
+5
View File
@@ -287,6 +287,11 @@ struct ModelGpuData {
// so driveStreamingLoads routes this model through the async web path
// instead of the MEMFS sync read.
bool streaming_from_web = false;
// Web analog of streaming_file_path: which registered JS byte-source
// (Module.__ifcvSources[id] = a picked File or a remote URL) this model's
// chunk + deferred reads pull from. Lets several federated models stream
// from different files at once, mirroring the desktop per-model path.
int web_source_id = 0;
// v15 deferred property metadata (web, on-demand). The IFC element tree
// (elements + string_table — names/GUIDs/hierarchy, for UI/picking, never
+41 -95
View File
@@ -3203,22 +3203,22 @@ std::uint32_t ViewportCore::loadSidecarFromPath(const std::string& path) {
namespace {
// Active byte-source size in bytes, or 0 if none. Bounds the metadata tail
// read (index-section end .. EOF). The source is either a picked File
// (Module.__ifcvFile, local) or a remote URL whose total length was resolved
// up front (Module.__ifcvUrlSize). The File takes precedence if both are set.
EM_JS(double, ifcvFileSize, (void), {
if (Module["__ifcvFile"] && Module["__ifcvFile"].size) return Module["__ifcvFile"].size;
if (Module["__ifcvUrl"]) return Module["__ifcvUrlSize"] || 0;
return 0;
// Byte-source registry (multi-file federation). Module.__ifcvSources[id] is
// { file: File|null, url: string|null, size: number } — a picked File or a
// remote URL, registered + sized by shell.html before the load. Several models
// can stream from different sources at once (the web analog of the desktop
// per-model streaming_file_path). Size of source `sid`, or 0 if unknown.
EM_JS(double, ifcvSourceSize, (int sid), {
var s = Module["__ifcvSources"] && Module["__ifcvSources"][sid];
return s ? (s.size || 0) : 0;
});
// Read [offset, offset+size) of the active source into dst (which must hold
// `size` bytes), then call back _ifcv_on_range_done(reqId, ok). Async. Local:
// Blob.slice. Remote: an HTTP Range request — if a server ignores Range and
// Read [offset, offset+size) of source `sid` into dst (which must hold `size`
// bytes), then call back _ifcv_on_range_done(reqId, ok). Async. File:
// Blob.slice. URL: an HTTP Range request — if a server ignores Range and
// returns the whole body (200), slice out the requested window so it still
// works (just without the bandwidth saving).
EM_JS(void, ifcvReadRangeInto, (int reqId, double offset, double size, void* dst), {
EM_JS(void, ifcvReadRangeInto, (int sid, int reqId, double offset, double size, void* dst), {
var deliver = function(ok, buf) {
if (ok && buf) {
HEAPU8.set(new Uint8Array(buf), dst);
@@ -3226,17 +3226,16 @@ EM_JS(void, ifcvReadRangeInto, (int reqId, double offset, double size, void* dst
}
Module["_ifcv_on_range_done"](reqId, ok ? 1 : 0);
};
var f = Module["__ifcvFile"];
if (f) {
f.slice(offset, offset + size).arrayBuffer()
var s = Module["__ifcvSources"] && Module["__ifcvSources"][sid];
if (s && s.file) {
s.file.slice(offset, offset + size).arrayBuffer()
.then(function(buf) { deliver(1, buf); })
.catch(function(e) { deliver(0, null); });
return;
}
var url = Module["__ifcvUrl"];
if (url) {
if (s && s.url) {
var end = offset + size - 1;
fetch(url, { headers: { "Range": "bytes=" + offset + "-" + end } })
fetch(s.url, { headers: { "Range": "bytes=" + offset + "-" + end } })
.then(function(resp) {
if (resp.status !== 206 && resp.status !== 200) { deliver(0, null); return; }
var full = resp.status === 200;
@@ -3251,32 +3250,11 @@ EM_JS(void, ifcvReadRangeInto, (int reqId, double offset, double size, void* dst
Module["_ifcv_on_range_done"](reqId, 0);
});
// Switch the active source to a remote URL and resolve its total byte length
// (needed to bound the metadata tail read), then call _ifcv_source_ready(ok).
// Tries a HEAD's Content-Length first, then a 0-0 ranged GET's Content-Range
// total. Clears any local File so the URL source is used.
EM_JS(void, ifcvBeginUrlSource, (const char* urlPtr), {
var url = UTF8ToString(urlPtr);
Module["__ifcvFile"] = null;
Module["__ifcvUrl"] = url;
Module["__ifcvUrlSize"] = 0;
var ready = function(ok) { Module["_ifcv_source_ready"](ok ? 1 : 0); };
fetch(url, { method: "HEAD" }).then(function(resp) {
var len = resp.ok ? parseInt(resp.headers.get("Content-Length") || "0", 10) : 0;
if (len > 0) { Module["__ifcvUrlSize"] = len; ready(1); return; }
return fetch(url, { headers: { "Range": "bytes=0-0" } }).then(function(r2) {
var cr = r2.headers.get("Content-Range"); // "bytes 0-0/12345"
var total = cr ? parseInt(cr.split("/")[1] || "0", 10) : 0;
Module["__ifcvUrlSize"] = total;
ready(total > 0);
});
}).catch(function(e) { ready(0); });
});
// One in-flight multi-range read: a sequence of coalesced plans, each read
// into `scratch` then scattered into `out`. `done(ok, out)` fires once every
// plan has landed, or on the first failure.
struct WebRangeRead {
int source_id = 0; // Module.__ifcvSources index
std::vector<SidecarReadPlan> plans;
std::size_t plan_idx = 0;
std::vector<std::uint8_t> scratch;
@@ -3287,11 +3265,6 @@ struct WebRangeRead {
std::unordered_map<int, WebRangeRead> g_web_reads;
int g_web_read_next = 1;
// The ViewportCore awaiting an async URL-source size resolution. Set by
// loadSidecarFromUrlWeb; consumed by the ifcv_source_ready callback. One
// ViewportCore exists on web, so a single slot suffices.
ViewportCore* g_url_ready_core = nullptr;
// Issue the current plan's Blob.slice, or finish (success) if all plans done.
void webIssueCurrentPlan(int id) {
auto it = g_web_reads.find(id);
@@ -3306,7 +3279,7 @@ void webIssueCurrentPlan(int id) {
}
const SidecarReadPlan& p = r.plans[r.plan_idx];
r.scratch.assign(std::size_t(p.read_size), 0);
ifcvReadRangeInto(id, double(p.file_offset), double(p.read_size),
ifcvReadRangeInto(r.source_id, id, double(p.file_offset), double(p.read_size),
r.scratch.data());
}
@@ -3314,6 +3287,7 @@ void webIssueCurrentPlan(int id) {
// in input order, then call done(true, bytes). On any failure: done(false,{}).
// `section_offset` makes the offsets absolute (pass 0 if already absolute).
void webReadRangesAsync(
int source_id,
std::uint64_t section_offset,
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges,
std::function<void(bool, std::vector<std::uint8_t>&&)> done) {
@@ -3321,6 +3295,7 @@ void webReadRangesAsync(
for (const auto& rg : ranges) total += rg.second;
WebRangeRead r;
r.source_id = source_id;
r.out.assign(std::size_t(total), 0);
// Coalesce within 1 MB: each Blob.slice is an async round trip, so a
// generous gap trades a few wasted bytes for far fewer JS hops.
@@ -3360,21 +3335,6 @@ extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_on_range_done(int reqId, int ok) {
webIssueCurrentPlan(reqId);
}
// JS callback once a remote URL source's total size has been resolved. Runs
// the shared metadata bootstrap against the now-active URL source. Exported as
// _ifcv_source_ready (see ifcviewer-web/CMakeLists.txt).
extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_source_ready(int ok) {
ViewportCore* core = g_url_ready_core;
g_url_ready_core = nullptr;
if (!core) return;
if (!ok) {
Log::warn() << "ifcviewer-web: could not resolve remote sidecar size "
"(server must support HEAD or Range)";
return;
}
core->loadSidecarMetadataWeb("net:model.ifcview");
}
void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_idx) {
auto it = models_gpu_.find(model_id);
if (it == models_gpu_.end()) return;
@@ -3382,6 +3342,7 @@ void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_i
if (chunk_idx >= m.chunks.size()) return;
const StreamingThread::Request req = makeChunkRequest(m, chunk_idx, model_id);
const int sid = m.web_source_id; // which registered byte-source to read from
const std::uint64_t vsec = req.vertex_section_offset;
const std::uint64_t isec = req.index_section_offset;
const std::vector<std::pair<std::uint64_t, std::uint64_t>> v_ranges = req.v_ranges;
@@ -3438,14 +3399,14 @@ void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_i
host_->requestFrame();
};
webReadRangesAsync(vsec, v_ranges,
webReadRangesAsync(sid, vsec, v_ranges,
[join, finish](bool ok, std::vector<std::uint8_t>&& vbytes) {
join->v_ok = ok;
join->vbytes = std::move(vbytes);
join->v_done = true;
finish();
});
webReadRangesAsync(isec, i_byte_ranges,
webReadRangesAsync(sid, isec, i_byte_ranges,
[join, finish](bool ok, std::vector<std::uint8_t>&& ibytes) {
join->i_ok = ok;
join->idx.resize(ibytes.size() / sizeof(std::uint32_t));
@@ -3465,19 +3426,19 @@ void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_i
// here — they stream per chunk through beginWebChunkLoad. `source_label` is a
// log/identity tag stored as file_path (chunk reads go through the JS source,
// not this path).
void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
void ViewportCore::loadSidecarMetadataWeb(int source_id, std::string source_label) {
if (!device_ || !queue_) {
Log::warn() << "loadSidecarMetadataWeb: wgpu not initialised";
return;
}
const double fsize = ifcvFileSize();
const double fsize = ifcvSourceSize(source_id);
if (fsize <= 0.0) {
Log::warn() << "loadSidecarMetadataWeb: no active source / zero size";
Log::warn() << "loadSidecarMetadataWeb: source " << source_id << " has zero size";
return;
}
webReadRangesAsync(0, {{0, SIDECAR_HEAD_BYTES}},
[this, fsize, source_label](bool ok, std::vector<std::uint8_t>&& head) {
webReadRangesAsync(source_id, 0, {{0, SIDECAR_HEAD_BYTES}},
[this, fsize, source_id, source_label](bool ok, std::vector<std::uint8_t>&& head) {
std::uint32_t nvb = 0;
if (!ok || !parseSidecarHead(head.data(), head.size(), nvb)) {
Log::warn() << "loadSidecarMetadataWeb: bad sidecar head";
@@ -3486,8 +3447,8 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
const std::uint64_t vsec = SIDECAR_HEAD_BYTES;
const std::uint64_t idx_count_off = std::uint64_t(SIDECAR_HEAD_BYTES) + nvb;
webReadRangesAsync(0, {{idx_count_off, 4}},
[this, fsize, nvb, vsec, idx_count_off, source_label]
webReadRangesAsync(source_id, 0, {{idx_count_off, 4}},
[this, fsize, nvb, vsec, idx_count_off, source_id, source_label]
(bool ok2, std::vector<std::uint8_t>&& cnt) {
if (!ok2 || cnt.size() < 4) {
Log::warn() << "loadSidecarMetadataWeb: short index count";
@@ -3505,8 +3466,9 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
// render-critical metadata only. The deferred block
// (elements/strings) is fetched on demand later — first
// paint no longer waits on the property tree.
webReadRangesAsync(0, {{crit_size_off, 8}},
[this, vsec, nvb, isec, num_indices, source_label, crit_size_off, fsize]
webReadRangesAsync(source_id, 0, {{crit_size_off, 8}},
[this, vsec, nvb, isec, num_indices, source_id, source_label,
crit_size_off, fsize]
(bool okc, std::vector<std::uint8_t>&& cb) {
if (!okc || cb.size() < 8) {
Log::warn() << "loadSidecarMetadataWeb: short critical size";
@@ -3519,8 +3481,8 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
Log::warn() << "loadSidecarMetadataWeb: critical block past EOF";
return;
}
webReadRangesAsync(0, {{crit_off, crit_bytes}},
[this, vsec, nvb, isec, num_indices, source_label,
webReadRangesAsync(source_id, 0, {{crit_off, crit_bytes}},
[this, vsec, nvb, isec, num_indices, source_id, source_label,
crit_off, crit_bytes, fsize]
(bool ok3, std::vector<std::uint8_t>&& crit) {
if (!ok3) {
@@ -3544,6 +3506,7 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
auto mit = models_gpu_.find(mid);
if (mit != models_gpu_.end()) {
mit->second.streaming_from_web = true;
mit->second.web_source_id = source_id;
// Deferred block: [end of critical, EOF).
mit->second.deferred_meta_offset = crit_off + crit_bytes;
mit->second.deferred_meta_bytes =
@@ -3560,30 +3523,13 @@ void ViewportCore::loadSidecarMetadataWeb(std::string source_label) {
});
}
void ViewportCore::loadSidecarFromBlobWeb() {
// The picked File is already on Module.__ifcvFile (set by shell.html); the
// metadata bootstrap reads it via Blob.slice.
loadSidecarMetadataWeb("blob:model.ifcview");
}
void ViewportCore::loadSidecarFromUrlWeb(std::string url) {
if (!device_ || !queue_) {
Log::warn() << "loadSidecarFromUrlWeb: wgpu not initialised";
return;
}
// Resolve the URL's total size first (async); ifcv_source_ready then runs
// the shared bootstrap once Module.__ifcvUrl/__ifcvUrlSize are populated.
g_url_ready_core = this;
ifcvBeginUrlSource(url.c_str());
}
void ViewportCore::loadDeferredMetadataWeb(std::uint32_t model_id,
std::function<void(bool)> done) {
// On-demand fetch of the v15 deferred block (element tree + string table)
// for a web-streamed model — the property data a UI needs (tree, selected-
// object name, search) but rendering doesn't. Fetches at most once. Valid
// only for the currently-loaded model, since the JS byte-source
// (Module.__ifcvUrl/File) tracks the latest load.
// object name, search) but rendering doesn't. Fetches at most once. Reads
// from the model's own registered byte-source, so it works per-model even
// with several federated files loaded.
auto it = models_gpu_.find(model_id);
if (it == models_gpu_.end()) { if (done) done(false); return; }
ModelGpuData& m = it->second;
@@ -3592,7 +3538,7 @@ void ViewportCore::loadDeferredMetadataWeb(std::uint32_t model_id,
if (done) done(true);
return;
}
webReadRangesAsync(0, {{m.deferred_meta_offset, m.deferred_meta_bytes}},
webReadRangesAsync(m.web_source_id, 0, {{m.deferred_meta_offset, m.deferred_meta_bytes}},
[this, model_id, done](bool ok, std::vector<std::uint8_t>&& buf) {
auto mit = models_gpu_.find(model_id);
if (mit == models_gpu_.end()) { if (done) done(false); return; }
+6 -8
View File
@@ -362,14 +362,12 @@ public:
// are read via byte ranges, the streaming model is built, and it is tagged
// web-sourced so each chunk's vertex/index ranges are pulled lazily.
// Asynchronous — returns immediately and frames the model from the JS
// completion callback. resetScene() first to replace.
//
// - Blob: the picked File on Module.__ifcvFile (Blob.slice).
// - URL: a remote sidecar (HTTP Range); resolves total size first, then
// runs the shared bootstrap via the ifcv_source_ready callback.
void loadSidecarFromBlobWeb();
void loadSidecarFromUrlWeb(std::string url);
void loadSidecarMetadataWeb(std::string source_label);
// completion callback. APPENDS the model (federation); call resetScene()
// first to replace. `source_id` indexes the JS byte-source registry
// (Module.__ifcvSources[source_id] — a picked File or remote URL, already
// sized by shell.html); each federated model streams from its own source.
// `source_label` is a log/identity tag.
void loadSidecarMetadataWeb(int source_id, std::string source_label);
// On-demand fetch of the v15 deferred property block (element tree + string
// table) for a web-streamed model — what a UI (object tree / selected-name