mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
2c1d445d5b
A federated pick could be attributed to the wrong file. The model slot a host sees — ElementRef::model_index, modelProgress's index — is a rank in session_model_id order, and on web that id was minted at the END of the sidecar read chain, after three network round trips. So the ranking was the order the models' reads happened to finish in, not the order the host added them. With ~40 similarly-sized models over HTTP, adjacent models swapped and a click reported its neighbour's file; the host page then asked for a GUID the file does not contain. Mint the id at the top of loadSidecarMetadataWeb instead, which runs synchronously from load_sidecar_from_source_c and therefore in the order the host asked for its models. A load that fails partway just abandons its id, and the ranks compact over the surviving models as before. Positions are still positions, though: if one model fails to load, every later index shifts down one and a host mapping index into its own list silently drifts again. So also carry the source id — the handle the host minted itself when it registered the file — through ElementRef into the pick payload and getObjects rows, and document it as the way to attribute an object to a file. ModelGpuData::web_source_id defaults to -1 now, since 0 is a real source id and cannot double as "none". The test server grows a ?delay=<ms> knob so a test can force the losing interleaving: georef-a is added first and served slowly, and its objects must still come back as model 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.7 KiB
JavaScript
88 lines
3.7 KiB
JavaScript
// Minimal static file server for the Emscripten build output. WebGPU
|
|
// needs a real http(s) origin (not file://), so the smoke test serves
|
|
// the build-web directory over localhost. No COOP/COEP headers yet —
|
|
// pthreads are off in the current web build (that's task #47).
|
|
//
|
|
// Serve dir resolution: $WEB_BUILD_DIR if set, else the repo's build-web.
|
|
import http from 'node:http';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import path from 'node:path';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = process.env.WEB_BUILD_DIR
|
|
? path.resolve(process.env.WEB_BUILD_DIR)
|
|
: path.resolve(__dirname, '../../../build-web');
|
|
const PORT = Number(process.env.PORT || 8124);
|
|
// Fallback root: the ifcviewer-web source dir holds sample.ifcview (which is
|
|
// embedded in the wasm, not copied into build-web). Lets the remote-backend
|
|
// test fetch http://localhost/sample.ifcview over real HTTP Range.
|
|
const SRC = path.resolve(__dirname, '..');
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.mjs': 'text/javascript; charset=utf-8',
|
|
'.wasm': 'application/wasm',
|
|
'.ifcview': 'application/octet-stream',
|
|
};
|
|
|
|
http.createServer(async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
let p = decodeURIComponent(url.pathname);
|
|
// ?delay=<ms> stalls every response for this URL, HEAD and Range alike.
|
|
// Load order across federated models is decided by whichever model's
|
|
// async read chain finishes first, so a test that wants a specific
|
|
// interleaving has to be able to make one source slower than another.
|
|
const delay = Number(url.searchParams.get('delay') || 0);
|
|
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
|
|
if (p === '/') p = '/IfcViewerWeb.html';
|
|
const inRoot = path.join(ROOT, p);
|
|
const inSrc = path.join(SRC, p);
|
|
// Contain to one of the two allowed roots.
|
|
if (!inRoot.startsWith(ROOT) && !inSrc.startsWith(SRC)) { res.writeHead(403).end(); return; }
|
|
let body;
|
|
try { body = await readFile(inRoot); }
|
|
catch { body = await readFile(inSrc); } // fall back to the source dir
|
|
const ctype = MIME[path.extname(p)] || 'application/octet-stream';
|
|
|
|
// HEAD: headers only — lets the remote backend resolve total size.
|
|
if (req.method === 'HEAD') {
|
|
res.writeHead(200, {
|
|
'Content-Type': ctype,
|
|
'Content-Length': body.length,
|
|
'Accept-Ranges': 'bytes',
|
|
});
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Range: serve 206 partial content so the HTTP-Range backend is exercised
|
|
// exactly as a real Accept-Ranges host would (handles bytes=a-b and a-).
|
|
const range = req.headers['range'];
|
|
const m = range && /^bytes=(\d*)-(\d*)$/.exec(range.trim());
|
|
if (m) {
|
|
let start = m[1] === '' ? undefined : parseInt(m[1], 10);
|
|
let end = m[2] === '' ? undefined : parseInt(m[2], 10);
|
|
if (start === undefined) { start = body.length - end; end = body.length - 1; } // bytes=-N
|
|
if (end === undefined || end > body.length - 1) end = body.length - 1; // bytes=a-
|
|
if (Number.isNaN(start) || start > end) { res.writeHead(416).end(); return; }
|
|
const slice = body.subarray(start, end + 1);
|
|
res.writeHead(206, {
|
|
'Content-Type': ctype,
|
|
'Content-Range': `bytes ${start}-${end}/${body.length}`,
|
|
'Accept-Ranges': 'bytes',
|
|
'Content-Length': slice.length,
|
|
});
|
|
res.end(slice);
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': ctype, 'Accept-Ranges': 'bytes' });
|
|
res.end(body);
|
|
} catch {
|
|
res.writeHead(404).end('not found');
|
|
}
|
|
}).listen(PORT, () => console.log(`serving ${ROOT} on http://localhost:${PORT}`));
|