OptionalbuildOptionalfederationWhen provided, ref is the federated globalId; otherwise the expressId.
OptionalmeshThe federation offset the HOST has ALREADY added to every mesh.expressId,
so this adapter can subtract it back out and address store in its own
(local) id space. Default 0 — meshes are local.
The viewer's loader shifts meshes into the global id space in place
(useIfcLoader: mesh.expressId = mesh.expressId + idOffset) while
IfcDataStore keeps LOCAL ids, so for every federated model past the first
mesh.expressId is NOT a key into store. Without this, every lookup in
the loop below misses: key degrades to the synthetic fallback, tag
reads Unknown, name/storey come back empty, and buildStepExclusions
finds no relationships at all — so void/host/assembly pairs silently stop
being excluded. ref was wrong in the other direction, with
federation.toGlobalId adding the offset a second time.
tag degrades with the rest, despite the node.type || mesh.ifcType
expression below reading as though the mesh could rescue it: every
production EntityTable returns the literal string 'Unknown' from
getTypeName on a miss, and 'Unknown' is truthy, so the mesh.ifcType
arm is unreachable on a store lookup that missed. Unknown next to a blank
name is the loudest on-screen symptom of this bug.
The viewer's own compare path (lib/compare/buildFingerprints.ts) does the
same subtraction for the same reason. The CLI / MCP / playground callers
hand over local meshes and leave this at its 0 default.
The obvious hardening is to make this REQUIRED, and the repo has the
precedent: buildFingerprints.ts declares idOffset: number and does this
exact subtraction, which is why compare never had this bug. It is
deliberately NOT copied here. buildFingerprints is app-internal
(apps/viewer), free to break at will; elementsFromStep is published API
— @ifc-lite/clash exposes it as the ./step subpath export — so
requiring the member is a breaking change for every external caller, all of
which legitimately pass local meshes and would have to add a 0 to keep
compiling. That is a major bump to harden one in-repo call site, on a
patch-level bugfix.
What covers the gap instead, since the optional signature is exactly what let the viewer wiring be deleted and stay green:
apps/viewer/src/hooks/useClash.federated-id-offset.test.tsx pins the
viewer's call at the level that actually broke (it fails if
meshIdOffset is dropped from gatherElements);console.warn at the end of elementsFromStep reports
a forgotten offset at RUNTIME, in any host, including new ones — for
any model whose store actually holds GlobalIds. It stays silent for a
store that holds none (a GLB import), where a total miss is the normal
state and warning would only teach the reader to ignore the message.A dev-only expressId < 0 assert was considered and rejected: it fires
only when the subtrahend is too LARGE, whereas the failure that shipped was
a forgotten offset, i.e. a subtrahend of 0 producing positive, plausible,
entirely wrong ids. It would not have caught this bug. The total-miss check
keys on the damage instead, so it catches both directions.
Model/file id (federation).
OptionalworldAligns this model into the common world frame (RTC + building rotation).
Precompute void/host/assembly exclusions. Default true.