mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 22:50:21 +00:00
ifcviewer-web: stream remote sidecars over HTTP Range (?model=URL)
Adds a network byte-source alongside the local Blob one. The async-chunk
infra is source-agnostic — only the two JS primitives knew it was a Blob —
so this generalises them and reuses everything else:
- ifcvReadRangeInto: local → Blob.slice; remote → fetch() with a Range
header (206). If a server ignores Range and returns 200, the requested
window is sliced out so it still works (without the bandwidth saving).
- ifcvFileSize: Blob size, or the URL's total length resolved up front.
- ifcvBeginUrlSource: resolves total size (HEAD Content-Length, else a
0-0 ranged GET's Content-Range) then fires _ifcv_source_ready.
- The metadata bootstrap is extracted into a source-agnostic
loadSidecarMetadataWeb(label); loadSidecarFromBlobWeb / FromUrlWeb are
thin entries. streaming_from_blob → streaming_from_web (now covers both).
main_web exports load_sidecar_from_url_c(url); shell.html reads a
?model=URL query param and ccalls it once the app is live (same-origin
needs no CORS; cross-origin hosts must send CORS + Accept-Ranges).
Test: serve.mjs now answers HEAD + Range (206) and falls back to the
ifcviewer-web source dir for sample.ifcview (embedded in the wasm, not in
build-web). New smoke case loads ?model=/sample.ifcview and asserts it
renders via the Range path. 6/6 web smoke + 107/107 unit pass; desktop
unaffected (web-guarded; only the shared field rename touches it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 blob sidecar/.test(m.text()),
|
||||
predicate: (m) => /loaded sidecar \(blob:/.test(m.text()),
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.locator('#file-input').setInputFiles(samplePath);
|
||||
@@ -194,6 +194,43 @@ test('loads a user-picked sidecar through the Blob.slice byte-range path', async
|
||||
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
|
||||
});
|
||||
|
||||
test('streams a remote sidecar over HTTP Range (?model= URL backend)', async ({ page }) => {
|
||||
// The remote backend: ?model=URL resolves total size (HEAD), then reads the
|
||||
// metadata + per-chunk byte ranges via HTTP Range (206) — same async-chunk
|
||||
// path as the local Blob load, different byte source. serve.mjs answers
|
||||
// Range requests, so this exercises it end-to-end (same-origin, no CORS).
|
||||
const gpuErrors = [];
|
||||
page.on('console', (msg) => {
|
||||
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(msg.text()))
|
||||
gpuErrors.push(msg.text());
|
||||
});
|
||||
page.on('pageerror', (e) => gpuErrors.push('pageerror: ' + e.message));
|
||||
|
||||
// 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()),
|
||||
timeout: 20_000,
|
||||
});
|
||||
await page.goto('/IfcViewerWeb.html?model=/sample.ifcview');
|
||||
await page.waitForFunction(
|
||||
() => !!(window.Module && window.Module._app_ptr), null, { timeout: 30_000 });
|
||||
await loaded;
|
||||
await page.waitForTimeout(800); // stream the chunk + a few frames
|
||||
|
||||
// The remote-streamed model must render: centre patch (cube) != corner.
|
||||
const box = await page.locator('#viewer-canvas').boundingBox();
|
||||
const patch = (cx, cy) => page.screenshot({
|
||||
clip: { x: Math.round(cx - 12), y: Math.round(cy - 12), width: 24, height: 24 },
|
||||
});
|
||||
const center = await patch(box.x + box.width / 2, box.y + box.height / 2);
|
||||
const corner = await patch(box.x + 16, box.y + 16);
|
||||
expect(
|
||||
Buffer.compare(center, corner),
|
||||
'remote-streamed model did not render — HTTP Range path failed',
|
||||
).not.toBe(0);
|
||||
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
|
||||
});
|
||||
|
||||
test('click selects an object and the highlight renders (async pick)', async ({ page }) => {
|
||||
// Exercises the async object-pick readback: a click maps the pick staging
|
||||
// buffer via a spontaneous callback (no blocking spin, which would hang the
|
||||
|
||||
Reference in New Issue
Block a user