mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 09:28:44 +00:00
ifcviewer-web: mint session model ids when a load is requested
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>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
// await viewer.addFile(file, { replace: true });
|
||||
// await viewer.addUrl('/model.ifcview'); // appends (federation)
|
||||
//
|
||||
// const objects = await viewer.getObjects(); // [{objectId, guid, name, type, model}]
|
||||
// const objects = await viewer.getObjects(); // [{objectId, guid, name, type, model, sourceId}]
|
||||
// viewer.setSelection(['3vB2YO$MX4xv5uCqZZG05x']);
|
||||
// viewer.setColor(objects.filter(o => o.type === 'IfcWall'), '#ff8800');
|
||||
// viewer.setCamera({ yaw: 45, pitch: 30 });
|
||||
@@ -21,6 +21,14 @@
|
||||
// The canvas element MUST have id="viewer-canvas" — the wasm side hard-codes
|
||||
// that selector for its WebGPU surface and input handlers.
|
||||
//
|
||||
// Model identity. addFile/addUrl return a source id: the handle for that model,
|
||||
// minted the moment it is registered and stable for the session. Objects come
|
||||
// back tagged with both their `sourceId` and a `model` index (the model's slot
|
||||
// in load order). Map an object to the file it came from through the source id
|
||||
// — the index is a POSITION, so it shifts down if an earlier model fails to
|
||||
// load, and a host keying its own list off it then attributes objects to the
|
||||
// wrong file.
|
||||
//
|
||||
// Object identity. Everything the scripting API takes or returns is keyed by
|
||||
// `objectId`: a u32 the renderer assigns, unique across the federation but only
|
||||
// meaningful for this session. IFC GlobalIds are the stable identity, and every
|
||||
@@ -197,15 +205,17 @@
|
||||
// a remote URL (HTTP Range). load_sidecar_from_source_c(sid) streams one.
|
||||
Module.__ifcvSources = Module.__ifcvSources || [];
|
||||
|
||||
// The wasm calls this on every single-object pick; (0, '', -1) means the
|
||||
// The wasm calls this on every single-object pick; (0, '', -1, -1) means the
|
||||
// selection was cleared. modelIndex is the picked object's model in load
|
||||
// order (matches the modelProgress index), or -1. A marquee box-select does
|
||||
// NOT fire this (it has no single object) — use onSelectionChange for that.
|
||||
Module.__ifcvOnSelect = function (objectId, guid, modelIndex) {
|
||||
// order (matches the modelProgress index) and sourceId the source it was
|
||||
// added from, either null when unknown. A marquee box-select does NOT fire
|
||||
// this (it has no single object) — use onSelectionChange for that.
|
||||
Module.__ifcvOnSelect = function (objectId, guid, modelIndex, sourceId) {
|
||||
const detail = {
|
||||
objectId: objectId >>> 0,
|
||||
guid: guid || null,
|
||||
modelIndex: (typeof modelIndex === 'number' && modelIndex >= 0) ? modelIndex : null,
|
||||
sourceId: (typeof sourceId === 'number' && sourceId >= 0) ? sourceId : null,
|
||||
};
|
||||
selectListeners.forEach(function (cb) {
|
||||
try { cb(detail); } catch (e) { console.error(e); }
|
||||
@@ -279,8 +289,8 @@
|
||||
|
||||
// ---- Events ----------------------------------------------------------
|
||||
|
||||
// Single-object picks (click). Fires with {objectId, guid, modelIndex}.
|
||||
// Returns an unsubscribe function.
|
||||
// Single-object picks (click). Fires with
|
||||
// {objectId, guid, modelIndex, sourceId}. Returns an unsubscribe function.
|
||||
onSelect: function (cb) {
|
||||
selectListeners.push(cb);
|
||||
return function () {
|
||||
@@ -421,11 +431,14 @@
|
||||
|
||||
// ---- Objects ---------------------------------------------------------
|
||||
|
||||
// Every object in the scene: [{objectId, guid, name, type, model}], where
|
||||
// `model` is the index into the load-ordered model list (same index as
|
||||
// modelProgress). Asynchronous — the element tables are fetched lazily per
|
||||
// model so first paint never waits on them. Resolving this is also what
|
||||
// lets every other call accept GlobalIds; the result is cached for that.
|
||||
// Every object in the scene:
|
||||
// [{objectId, guid, name, type, model, sourceId}], where `model` is the
|
||||
// index into the load-ordered model list (same index as modelProgress)
|
||||
// and `sourceId` the source the model was added from — see the model
|
||||
// identity note at the top of the file. Asynchronous — the element tables
|
||||
// are fetched lazily per model so first paint never waits on them.
|
||||
// Resolving this is also what lets every other call accept GlobalIds; the
|
||||
// result is cached for that.
|
||||
getObjects: function () {
|
||||
const token = ++objectsToken;
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
Reference in New Issue
Block a user