/** * BIM feature-picking epic, Phase 1 (design pending — see * agent-map-library-architecture.md once the design section lands). * * Enriches a Tile3DLayer pick with whatever metadata is CHEAPLY available at * tile granularity — no shader work, no new manifest attribute, no per-tile * caching, no new deck.gl prop injection. Three independent sources merge, * most-content-specific winning when more than one is present: * * 1. glTF `EXT_mesh_features` + `EXT_structural_metadata`, already fully * decoded by loaders.gl's GLTFLoader by the time a tile is even * renderable (`Tile3D.content.gltf`, post `postProcessGLTF` — verified * against node_modules/@loaders.gl/{gltf,3d-tiles} source: primitive * `.extensions` and the top-level `EXT_structural_metadata` survive the * postprocess step via object spread, so the same decode this repo's * Phase-0 spike proved against a real fixture applies here unchanged). * Only used when the content resolves to exactly ONE feature — Phase 1 * has no per-vertex resolution (that's Phase 2's `pick-features` shader * work), so a multi-feature content would be misattributing properties * to whichever vertex the coarse tile-level pick happened to return. * 2. **Legacy 3D Tiles 1.0 batch tables** — b3dm's older per-feature * mechanism (a JSON `batchTableJson` of `{propertyName: [oneValuePerBatch]}` * arrays, keyed by a `_BATCHID` vertex attribute never surfaced by this * layer since Phase 1 doesn't do per-vertex work). Verified directly * against real production data (NYC DoITT building footprints and Japan's * PLATEAU CityGML export, both streamed live via Cesium ion): real-world * 3D Tiles datasets overwhelmingly still ship this older mechanism, not * 1.1's `EXT_structural_metadata` — this is the metadata source that * actually matters for content people can get their hands on today, not * just synthetic samples. loaders.gl's b3dm parser * (`parse-3d-tile-tables.js`) sets `tile.featureTableJson.BATCH_LENGTH` * and `tile.batchTableJson` directly on the SAME parsed-content object * `.gltf` lives on (confirmed by reading the parser itself — no * intermediate class wraps it). Same single-feature-only restriction as * the glTF path, same reasoning: `BATCH_LENGTH === 1` is unambiguous, * `BATCH_LENGTH > 1` needs per-vertex `_BATCHID` resolution this layer * doesn't attempt. * 3. 3D Tiles 1.1's OWN (non-glTF) metadata mechanism — tileset/group/tile/ * content-level entities declared directly in tileset.json as plain * inline JSON (no binary property-table decode needed at this * granularity, unlike the glTF one — verified against a real tileset.json, * CesiumGS/3d-tiles-samples 1.1/MetadataGranularities). loaders.gl's * `Tile3D` retains the raw tile node verbatim on `.header`, and * `Tileset3D` retains the raw parsed tileset.json on `.tileset`, so this * reads entirely off already-retained data. */ interface MetadataEntity { class?: string; properties?: Record; } interface GltfPropertyTable { class?: string; count: number; /** Decoded column data. NOT numeric-only — STRING properties decode to `string[]`, which is what most BIM classification fields are. */ properties?: Record; }>; } export interface GltfExtStructuralMetadata { schema?: { classes?: Record; }; propertyTables?: GltfPropertyTable[]; } interface GltfMeshFeaturesId { attribute?: number; /** Present instead of `attribute` for "feature ID by texture coordinates" sets — see resolvePrimitiveFeatureIdSet. */ texture?: { index?: number; texCoord?: number; channels?: number[]; }; propertyTable?: number; data?: ArrayLike; } export interface GltfPrimitiveLike { extensions?: { EXT_mesh_features?: { featureIds?: GltfMeshFeaturesId[]; }; }; } /** * Finds the `EXT_mesh_features` feature-ID set selected by a `_FEATURE_ID_N` * name (the manifest's `feature-id-property`, default `_FEATURE_ID_0`) on one * glTF primitive. Returns null when the primitive carries no usable feature * IDs (a plain, non-BIM mesh — the common case until real IFC-derived content * is loaded). * * `EXT_mesh_features` defines THREE ways to carry feature IDs, and N selects * across all of them: * * - **By vertex** (`attribute: N` -> the `_FEATURE_ID_N` vertex attribute). * Matched by attribute number first, so an explicitly numbered set always * wins over its position in the array. * - **By texture** (`texture: {...}`, no `attribute`) — how photogrammetry * classification datasets carry per-element IDs, since a photogrammetry * mesh is one triangle soup with the classification painted on. These sets * have no attribute number to match, so N falls back to selecting the Nth * entry of the `featureIds` array — the same positional convention the * extension's own `featureId_N` shader naming uses. * - **By index** (neither field; the vertex's own index is its feature ID). * loaders.gl leaves `data` empty for these, so they resolve to null here. * * CAVEAT for texture-backed sets: loaders.gl decodes them by sampling the * feature-ID texture once PER VERTEX at that vertex's UV * (`getPrimitiveTextureData`), not per fragment. The resulting IDs are * therefore only as precise as the mesh is dense — a classification boundary * that runs through the middle of a triangle is lost, and that triangle takes * whichever ID its vertices sampled. Good enough to pick an element, not * equivalent to a per-fragment lookup. * * Note both texture and index sets require the glTF to be loaded with * `loadBuffers` (and, for textures, `loadImages`); without them loaders.gl * leaves `data` undefined/empty and this returns null. */ export declare function resolvePrimitiveFeatureIdSet(primitive: GltfPrimitiveLike | undefined, featureIdProperty: string): { data: ArrayLike; propertyTable?: number; texture?: GltfMeshFeaturesId["texture"]; nullFeatureId?: number; } | null; /** * Reads ONE property of a decoded property table as a column — one entry per * feature ID, indexed by feature ID. * * This is the row-major counterpart to `resolveGltfFeatureRow`: the declarative * `hide-features`/`ghost-features`/`isolate-features` attributes need to test * a single field across EVERY feature at once, and building a full properties * object per row just to read one key would allocate the whole table to answer * a one-column question. Returns null when the table or property is absent. */ export declare function readPropertyColumn(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined, property: string): unknown[] | null; /** * The WHOLE property table as row objects, indexed by feature ID. * * Picking resolves one row at a time, which is right for a hover but useless * for anything that needs the set: a legend listing every IfcClass, a category * palette, a model tree. Those previously had to fetch a hand-built sidecar * next to the tileset. This reads the same table the GLB already carries. */ export declare function extractPropertyTableRows(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined): Record[] | null; /** * Resolves one feature's properties out of a specific decoded property * table, by row index — the general case (Phase 2: `rowIndex` is the * per-vertex feature ID; Phase 1's single-feature path below is just this * with `rowIndex = 0` on a `count === 1` table). Returns null for an * out-of-range table/row (defensive — a malformed or partially-loaded tile * should degrade to "no properties," not throw during picking). */ export declare function resolveGltfFeatureRow(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined, rowIndex: number): { properties: Record; class: string | null; } | null; export interface Tile3DPickMetadata { /** Merged properties, most-specific source wins: glTF content / legacy batch table > 3D-Tiles content > tile > group > tileset. Empty object if nothing resolved. */ properties: Record; /** The class name of the most specific contributing source, or null if nothing resolved. */ class: string | null; /** Best-effort identity (IFC GlobalId / gml:id / guid), or null — see findGuidLike. */ guid: string | null; /** Unmerged, per-source view — the escape hatch. Deck.gl/loaders.gl shapes leak here deliberately, unlike `properties`/`class`. */ rawMetadata: { tileset?: MetadataEntity; group?: MetadataEntity; tile?: MetadataEntity; content?: MetadataEntity; gltfProperties?: Record; gltfClass?: string; batchTableProperties?: Record; batchTableClass?: string; }; } /** * Resolves whatever tile-granularity BIM metadata is available for a * Tile3DLayer pick. `sourceTile` is `PickingInfo.sourceTile` (Tile3DLayer's * own `getPickingInfo` always sets it, undefined for a miss) — callers * should only invoke this once they know the pick landed on a * `carriesTileset` layer. Returns null when nothing resolved (a tileset * with no metadata at all — the common case today, since this is new). */ export declare function resolveTile3DPickMetadata(sourceTile: unknown): Tile3DPickMetadata | null; export {};