@nika-js/onlymap 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +18 -6
  3. package/THIRD-PARTY-LICENSES.md +27 -0
  4. package/dist/{LercDecode.es-fbo6h5rt.js → LercDecode.es-CJnypw8j.js} +1 -1
  5. package/dist/{basemap-CprSan9q.js → basemap-DrQ0-eyR.js} +1 -1
  6. package/dist/cityjson-D_V5GY8b.js +332 -0
  7. package/dist/crs-Ciu7Xs7a.js +108 -0
  8. package/dist/crs.d.ts +50 -0
  9. package/dist/ctx.d.ts +8 -1
  10. package/dist/elements/om-map.d.ts +26 -2
  11. package/dist/elements/om-overlay.d.ts +1 -1
  12. package/dist/elements/om-widget.d.ts +4 -0
  13. package/dist/feature-colors.d.ts +41 -0
  14. package/dist/{geoparquet-Bm8pfxyo.js → geoparquet-Dix4lTNy.js} +1 -1
  15. package/dist/html-data.d.ts +1 -1
  16. package/dist/ifc-Dc-TdSJr.js +877 -0
  17. package/dist/ifc.d.ts +406 -0
  18. package/dist/{index-Bz-rbmhj.js → index-1UgNlfGR.js} +20892 -18886
  19. package/dist/{index-GOwMjqm6.js → index-BQMjW5w0.js} +1 -1
  20. package/dist/{index-DSRIZR81.js → index-B_1PPJgC.js} +1 -1
  21. package/dist/{index-CCQttJzO.js → index-CAuT5j9Y.js} +2 -2
  22. package/dist/{index-UzmNhuS0.js → index-CXPaeisL.js} +1 -1
  23. package/dist/index.d.ts +8 -0
  24. package/dist/layer-registry.d.ts +14 -0
  25. package/dist/layers/bim-layer.d.ts +104 -0
  26. package/dist/layers/feature-mesh-layer.d.ts +288 -0
  27. package/dist/layers/gltf-scene-walk.d.ts +55 -0
  28. package/dist/{lerc-CuifOkoA.js → lerc-CbTjQ7uI.js} +2 -2
  29. package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
  30. package/dist/onlymap.standalone.js +57996 -54706
  31. package/dist/onlymapjs.js +69 -67
  32. package/dist/programmatic.d.ts +22 -2
  33. package/dist/{raster-dysYIQ_k.js → raster-0b0nSHUh.js} +2 -2
  34. package/dist/{raster-pipeline-ZdCOGfqV.js → raster-pipeline-D8siq7-4.js} +1 -1
  35. package/dist/react/om-overlay.d.ts +9 -0
  36. package/dist/react.js +144 -144
  37. package/dist/runtime-core.d.ts +110 -3
  38. package/dist/selection.d.ts +33 -0
  39. package/dist/site-placement.d.ts +85 -0
  40. package/dist/testing.d.ts +6 -2
  41. package/dist/tile3d-metadata.d.ts +185 -0
  42. package/dist/version.d.ts +1 -1
  43. package/dist/widget-registry.d.ts +11 -0
  44. package/dist/widgets/ifc-clash.d.ts +18 -0
  45. package/dist/widgets/ifc.d.ts +41 -0
  46. package/dist/{zarr-OewK7k2K.js → zarr-DCYro_Vs.js} +79 -66
  47. package/docs/testing.md +2 -2
  48. package/llms.txt +3 -3
  49. package/onlymapjs.html-data.json +152 -0
  50. package/package.json +3 -1
  51. package/skills/onlymapjs/SKILL.md +4 -2
  52. package/skills/onlymapjs/references/react.md +1 -1
  53. package/skills/onlymapjs/references/syntax.md +298 -11
  54. package/dist/cityjson-urQeujQv.js +0 -407
@@ -5,6 +5,23 @@ import type { LayerIR } from "./ir";
5
5
  import { type Selection } from "./selection";
6
6
  import type { ValidationEntry } from "./validation";
7
7
  import type { MapViewport } from "./basemap";
8
+ /**
9
+ * The tile-level EXT_structural_metadata property-table index, resolved
10
+ * independently of `FeatureMeshLayer`'s own copy of this same computation.
11
+ *
12
+ * `tile.content.featureIdPropertyTableIndex` (which `FeatureMeshLayer`
13
+ * eventually sets, in its own `renderLayers()`) is NOT readable here: deck.gl's
14
+ * `Tile3DLayer._onTileLoad` fires the `onTileLoad` prop callback (what feeds
15
+ * this hook) BEFORE it ever constructs `FeatureMeshLayer` for that tile —
16
+ * confirmed against `@deck.gl/geo-layers`'s own `_onTileLoad`/`_getSubLayer`
17
+ * ordering. Reading that field here always saw `undefined`, silently falling
18
+ * back to property table 0 regardless of which table a tile's feature IDs
19
+ * actually reference — invisible only because this repo's own IFC converter
20
+ * never emits more than one table. Recomputing from the tile's raw glTF with
21
+ * the same pure, already-tested helpers `FeatureMeshLayer` itself uses avoids
22
+ * depending on that ordering entirely.
23
+ */
24
+ export declare function resolveTilePropertyTableIndex(gltf: unknown, featureIdProperty: string): number;
8
25
  /** Mapbox presets/protocol URLs aren't implemented — kept maplibre-free so validation/runtime can check without loading the chunk. */
9
26
  export declare function isMapboxBasemap(raw: string): boolean;
10
27
  export interface InitialView {
@@ -25,6 +42,24 @@ export interface CameraOptions {
25
42
  /** Arc the move (zoom-out-and-in) instead of easing directly — MapLibre `flyTo` / deck `FlyToInterpolator`. */
26
43
  curve?: boolean;
27
44
  }
45
+ export { resolveFeatureColor } from "./feature-colors";
46
+ /** One layer's accumulated (per-tile) property tables — see RuntimeCore.featureTables. */
47
+ export interface FeatureTableEntry {
48
+ source: string;
49
+ tiles: Map<string, Record<string, unknown>[]>;
50
+ rows: Record<string, unknown>[];
51
+ rowsVersion: number;
52
+ mappingKey?: string;
53
+ mapping?: import("./feature-colors").FeatureColorMapping | null;
54
+ }
55
+ /**
56
+ * The accumulator, extracted for direct testing: resets on a SOURCE change
57
+ * (the loader widget swapping models under one layer id), REPLACES rows for a
58
+ * re-seen tile key (deck evicts + refetches tiles as new objects — identity
59
+ * dedup accumulated duplicates), and keeps per-tile row arrays because
60
+ * feature IDs are tile-local. Returns the (possibly new) entry.
61
+ */
62
+ export declare function accumulateFeatureRows(tables: Map<string, FeatureTableEntry>, layerId: string, sourceKey: string, tileKey: string, rows: Record<string, unknown>[]): FeatureTableEntry;
28
63
  export interface RuntimeCoreCallbacks {
29
64
  /**
30
65
  * Fired whenever the view state changes (pan/zoom/pitch/bearing) — a
@@ -34,7 +69,14 @@ export interface RuntimeCoreCallbacks {
34
69
  * convention, the echo-loop half of two-way store binding.
35
70
  */
36
71
  onViewportChange?: (origin: ViewOrigin) => void;
37
- onSelectionChange?: (selection: Selection | null) => void;
72
+ /**
73
+ * `pickType` is the pointer event that produced this pick — REQUIRED even
74
+ * when `selection` is null (an empty pick), because toSelection discards
75
+ * empty picks and with them their type. Overlays scoped with
76
+ * `selection-type` need it: a click on empty space dismisses a
77
+ * click-anchored popup, a hover over empty space must not.
78
+ */
79
+ onSelectionChange?: (selection: Selection | null, pickType: "hover" | "click") => void;
38
80
  /** Fired once the map (deck.gl standalone, or the basemap) has finished its async init (spec: Behavior Engine "load" event). */
39
81
  onLoad?: () => void;
40
82
  /**
@@ -52,6 +94,14 @@ export interface RuntimeCoreCallbacks {
52
94
  * deck reports none (e.g. off-globe).
53
95
  */
54
96
  onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void;
97
+ /**
98
+ * A `pick-features` layer decoded its EXT_structural_metadata property
99
+ * table — the whole table, indexed by feature ID. Fires once per layer, on
100
+ * the first tile that carries one, because every tile in a tileset shares
101
+ * the same schema. This is what lets a legend enumerate categories without
102
+ * a hand-built sidecar next to the tileset.
103
+ */
104
+ onFeatureTable?: (layerId: string, rows: Record<string, unknown>[]) => void;
55
105
  /**
56
106
  * A tileset-bearing layer (Tile3DLayer) finished loading its root tileset
57
107
  * — the consumer hook for tools that must reach the LIVE deck tileset
@@ -60,6 +110,30 @@ export interface RuntimeCoreCallbacks {
60
110
  * @loaders.gl/tiles type dependency in the core).
61
111
  */
62
112
  onTilesetLoad?: (layerId: string, tileset: unknown) => void;
113
+ /**
114
+ * A `carriesGeoreference` layer (BIMLayer) finished loading its source file
115
+ * and read whatever georeferencing it declares — `hasFullMapConversion`
116
+ * is true only when the file both resolved a real position
117
+ * (`IfcMapConversion` into a recognized CRS) AND declared
118
+ * `OrthogonalHeight`, the two facts absolute elevation needs to mean
119
+ * anything. Informational for consumers: the library never auto-applies
120
+ * terrain from it (what-you-write-is-what-you-see) — validation instead
121
+ * requires the map to author `terrain` explicitly for these layers.
122
+ *
123
+ * `approximatePlacement` is a narrower, separate fact: true whenever the
124
+ * file's position did NOT come from a real `IfcMapConversion` (IfcSite
125
+ * lat/lon, or no georeference at all) — that source has no rotation data
126
+ * at all, and is very often an authoring tool's default location rather
127
+ * than a survey. A plain boolean (not the raw `originSource` string) to
128
+ * keep this file decoupled from IFC-specific types, same reasoning as
129
+ * `hasFullMapConversion` already being precomputed rather than passed as
130
+ * raw parts. The front-end layer surfaces this as a validation warning.
131
+ */
132
+ onGeoreference?: (layerId: string, info: {
133
+ orthogonalHeight?: number;
134
+ hasFullMapConversion: boolean;
135
+ approximatePlacement: boolean;
136
+ }) => void;
63
137
  /**
64
138
  * Runtime error boundary (spec: "Runtime error boundary") — deck.gl-level
65
139
  * failures (a crashing accessor, an incompatible prop) formatted into the
@@ -138,6 +212,40 @@ export declare class RuntimeCore {
138
212
  private terrainGeneration;
139
213
  /** Rendered deck layer id → authored manifest/controller id (terrain uses fresh renderer ids). */
140
214
  private renderedLayerIds;
215
+ /** The decoded property table for a pick-features layer, or undefined before its first tile. */
216
+ getFeatureTable(layerId: string): Record<string, unknown>[] | undefined;
217
+ /**
218
+ * Decoded property tables per `pick-features` layer, keyed by layer id.
219
+ * Held here rather than in the element because LAYER props derive from them
220
+ * too (`feature-color-by`), not only widgets. `rows` accumulates across
221
+ * every tile that has loaded so far (see `featureTableHook`) — for a
222
+ * grid-tiled model this means the table is only as complete as whatever
223
+ * has actually streamed in, same as any real tiled 3D-Tiles viewer.
224
+ */
225
+ /**
226
+ * Per-layer accumulated property tables (spec: BIM feature picking). Rows
227
+ * are kept PER TILE — feature IDs are tile-local, so a flat concatenation
228
+ * can never be indexed by feature ID (the multi-tile styling bug). `rows`
229
+ * is the derived aggregate for legends/`ctx.features()`/global domains;
230
+ * `tiles` is keyed by a STABLE content key so an evicted-and-refetched tile
231
+ * REPLACES its rows instead of duplicating them, and `source` is the layer's
232
+ * source URL string (not the per-parse rows-array identity, which reset the
233
+ * whole accumulator on every reconcile mid-stream). `mapping` memoizes the
234
+ * derived colour mapping so animation frames reuse one identity — the
235
+ * style-texture-rebuild-per-rAF fix.
236
+ */
237
+ private featureTables;
238
+ /** Fallback identity for tiles that expose no content URL — a stable per-object key. */
239
+ private tileKeyFallback;
240
+ private tileKeyCounter;
241
+ /**
242
+ * The memoized global colour mapping for a layer — rebuilt only when the
243
+ * accumulated table grows or a colour prop changes, so `applyLayers` calls
244
+ * from the per-frame channel (patchAnimatedProps runs one per rAF during
245
+ * story animations) hand every tile the SAME mapping identity and no GPU
246
+ * style texture is ever rebuilt for an unrelated animation.
247
+ */
248
+ private featureColorMappingFor;
141
249
  /**
142
250
  * The basemap attribute suppressed while terrain is active — a flat
143
251
  * MapLibre canvas at sea level visibly desyncs from a raised surface
@@ -259,7 +367,7 @@ export declare class RuntimeCore {
259
367
  * behavior dispatch, and the overlay flush are same-path by construction.
260
368
  * Available in every mode (a browser-level test may use it too).
261
369
  */
262
- injectPick(selection: Selection | null): void;
370
+ injectPick(selection: Selection | null, pickType?: "hover" | "click"): void;
263
371
  getViewState(): Readonly<ViewState>;
264
372
  /**
265
373
  * Sketch capture (spec: "Manual Drawing", D4) — while a draw tool is active,
@@ -305,4 +413,3 @@ export declare class RuntimeCore {
305
413
  flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void;
306
414
  destroy(): void;
307
415
  }
308
- export {};
@@ -3,6 +3,7 @@
3
3
  * Converts a deck.gl PickingInfo into the documented, deck.gl-independent
4
4
  * ctx.selection shape (HU2: ctx never leaks deck.gl internals).
5
5
  */
6
+ import { type Tile3DPickMetadata } from "./tile3d-metadata";
6
7
  export interface Selection {
7
8
  layerId: string;
8
9
  object: unknown;
@@ -16,6 +17,31 @@ export interface Selection {
16
17
  coordinate: [number, number] | null;
17
18
  pixel: [number, number];
18
19
  type: "hover" | "click";
20
+ /**
21
+ * BIM feature-picking epic. Undefined for every pick except a
22
+ * Tile3DLayer pick that resolved SOME metadata; `class`/`properties`
23
+ * follow the same "ctx never leaks deck.gl internals" normalization as
24
+ * the rest of this shape, `rawMetadata` is the deliberate escape hatch.
25
+ * Two sources, most-specific-wins:
26
+ * - Phase 1 (tile granularity, always active): 3D-Tiles-native
27
+ * tileset/group/tile/content entities, merged with a single-feature
28
+ * glTF EXT_structural_metadata property table when the content
29
+ * resolves to exactly one feature. See src/tile3d-metadata.ts.
30
+ * - Phase 2 (per-feature, only when `pick-features` resolved a specific
31
+ * vertex-level feature): overrides Phase 1's properties/class/guid
32
+ * with the picked feature's own row from the same property table,
33
+ * still layered on top of Phase 1's broader tile-level context. See
34
+ * src/layers/feature-mesh-layer.ts.
35
+ */
36
+ properties?: Record<string, unknown>;
37
+ class?: string | null;
38
+ /** Best-effort identity (IFC GlobalId / gml:id / guid), from either Phase 1's tile-granularity merge or Phase 2's per-feature row — see tile3d-metadata.ts's findGuidLike. Undefined when nothing matched. */
39
+ guid?: string | null;
40
+ /** The picked vertex-level feature ID — only set by Phase 2's per-vertex picking; undefined for a Phase 1 tile-granularity pick. TILE-LOCAL: each tile's property table indexes 0..n-1, so the same number exists in every tile of a grid-tiled model — use `featureKey` to identify an element across tiles/models. */
41
+ featureId?: number;
42
+ /** Stable cross-tile identity for a Phase 2 pick: the element's own guid when the table carries one (IFC GlobalId / gml:id), else `<tile content key>#<featureId>`. Undefined when no per-feature pick resolved. */
43
+ featureKey?: string;
44
+ rawMetadata?: Tile3DPickMetadata["rawMetadata"];
19
45
  }
20
46
  interface PickingInfoLike {
21
47
  layer?: {
@@ -26,6 +52,13 @@ interface PickingInfoLike {
26
52
  coordinate?: number[];
27
53
  x: number;
28
54
  y: number;
55
+ /** Tile3DLayer's own getPickingInfo always sets this (undefined on a miss) — see tile3d-metadata.ts. */
56
+ sourceTile?: unknown;
57
+ /** Set by src/layers/feature-mesh-layer.ts's SinglePrimitiveFeatureLayer.getPickingInfo when pick-features resolved a specific vertex-level feature — takes precedence over the tile-granularity fallback below. */
58
+ featureId?: number;
59
+ properties?: Record<string, unknown>;
60
+ class?: string | null;
61
+ guid?: string | null;
29
62
  }
30
63
  export declare function toSelection(info: PickingInfoLike, type: "hover" | "click", resolveLayerId?: (renderedId: string) => string): Selection | null;
31
64
  export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Declarative georeferencing — `site-origin` / `site-heading` / `site-scale`.
3
+ *
4
+ * An IFC export tells you where it thinks it is, and it is very often wrong:
5
+ * authoring tools ship a default project location (the Medical-Dental Clinic
6
+ * sample carries Revit's Boston default; the Duplex carries a Chicago city
7
+ * centre point), `IfcMapConversion` is absent from most IFC2x3 files, and
8
+ * `TrueNorth` is routinely left unset. Baking that into the tileset at
9
+ * conversion time means every correction is a reconversion.
10
+ *
11
+ * So placement moves to the layer, where it is an attribute like any other —
12
+ * inspectable in the markup, settable by the IFC loader from whatever it
13
+ * managed to extract, and undoable/story-steppable for free.
14
+ *
15
+ * Two transports, because the two layers that make up an IFC model are
16
+ * anchored differently:
17
+ *
18
+ * - The MESH is a 3D Tiles tileset whose root already carries an
19
+ * east/north/up frame. deck's `Tile3DLayer` forwards `loadOptions.tileset`
20
+ * into the `Tileset3D` constructor, and `Tileset3D` PRE-multiplies its
21
+ * `modelMatrix` onto the root transform, so relocating means cancelling
22
+ * the baked frame first: `target · Rz · S · baked⁻¹`. That needs the baked
23
+ * frame, hence `tilesetBakedFrame` below.
24
+ * - The OUTLINES are a `PathLayer` of local east/north/up metres, which
25
+ * `METER_OFFSETS` places directly from `coordinateOrigin` — no baked frame
26
+ * to cancel, and rotation/scale ride a plain `modelMatrix` uniform, so
27
+ * they are free to change per frame.
28
+ */
29
+ import { Matrix4 } from "@math.gl/core";
30
+ export interface SitePlacement {
31
+ /** Absent when the author gave only a heading/scale — rotate in place. */
32
+ origin: [number, number, number] | null;
33
+ /** Degrees CLOCKWISE from true north, matching how a survey bearing reads. */
34
+ heading: number;
35
+ scale: number;
36
+ }
37
+ /**
38
+ * Reads the three placement props off a resolved layer IR.
39
+ *
40
+ * Returns null when none is authored, which is the common case and must stay
41
+ * free: no prefetch, no matrix, no change to how the layer loads.
42
+ */
43
+ export declare function parseSitePlacement(props: Record<string, unknown>): SitePlacement | null;
44
+ /**
45
+ * Short, DOT-FREE token for "has the placement changed?" — appended to a
46
+ * tileset URL to force deck to reload (see the cache-bust in runtime-core).
47
+ *
48
+ * Hashed rather than spelled out because loaders.gl types a tile by
49
+ * `url.split('?')[0].split('.').pop()`, and a coordinate spelled into the
50
+ * fragment (`#site=-71.059776,…`) hands that sniffer `059776` instead of
51
+ * `json`. Tilesets that force `isTileset` would survive it; ones relying on
52
+ * auto-detection would not, and the failure is the silent kind where nothing
53
+ * renders. Base-36 of a 32-bit FNV-1a has no dots by construction.
54
+ */
55
+ export declare function sitePlacementToken(p: SitePlacement): string;
56
+ /**
57
+ * Local east/north/up frame at a geodetic point, as the column-major matrix
58
+ * 3D Tiles calls `root.transform`.
59
+ */
60
+ export declare function enuMatrix(lonDeg: number, latDeg: number, height?: number): Matrix4;
61
+ /**
62
+ * Rotation + uniform scale about the model's own origin, in its local
63
+ * east/north/up frame.
64
+ *
65
+ * Heading is clockwise from north; ENU is right-handed with +Z up, where a
66
+ * POSITIVE rotation about Z turns north toward west. So a clockwise bearing is
67
+ * a negative rotation — the sign here is the whole reason this is a named
68
+ * function rather than an inline `rotateZ`.
69
+ */
70
+ export declare function localPlacementMatrix(p: SitePlacement): Matrix4;
71
+ /**
72
+ * The `Tileset3D.modelMatrix` that moves a tileset anchored at `baked` onto `p`.
73
+ *
74
+ * `target · Rz · S · anchor⁻¹`: the inverse undoes the frame the converter
75
+ * wrote, then the model is re-planted. With no `site-origin` the target IS the
76
+ * anchor, so the inverse cancels it exactly and only the rotation and scale
77
+ * survive — `site-heading` on its own spins the model where it stands.
78
+ *
79
+ * Both the pivot and the outline overlay's `coordinateOrigin` are the same
80
+ * ground-level site point, so mesh and outlines rotate together.
81
+ */
82
+ export declare function tilesetPlacementMatrix(baked: Matrix4, p: SitePlacement): Matrix4;
83
+ export declare function tilesetBakedFrame(url: string, onReady: () => void): Matrix4 | null | undefined;
84
+ /** Test seam — the cache is process-wide and would leak between cases. */
85
+ export declare function resetSitePlacementCache(): void;
package/dist/testing.d.ts CHANGED
@@ -49,8 +49,12 @@ export interface TestHarness {
49
49
  * selection path a real pick takes. Settles before resolving.
50
50
  */
51
51
  pick(opts: PickOptions): Promise<void>;
52
- /** Ends the current selection (a hover-off) — the path that auto-hides tooltips. */
53
- clearSelection(): Promise<void>;
52
+ /**
53
+ * Ends the current selection with an empty pick. Default kind "hover" — a
54
+ * hover-off, the path that auto-hides tooltips. Pass "click" for a click on
55
+ * empty space, which is what dismisses a `selection-type="click"` popup.
56
+ */
57
+ clearSelection(kind?: "hover" | "click"): Promise<void>;
54
58
  /**
55
59
  * Feeds a map coordinate through the same path a real deck click/hover
56
60
  * takes — drives the draw controller and fires the `om-map-point` event.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * BIM feature-picking epic, Phase 1 (design pending — see
3
+ * agent-map-library-architecture.md once the design section lands).
4
+ *
5
+ * Enriches a Tile3DLayer pick with whatever metadata is CHEAPLY available at
6
+ * tile granularity — no shader work, no new manifest attribute, no per-tile
7
+ * caching, no new deck.gl prop injection. Three independent sources merge,
8
+ * most-content-specific winning when more than one is present:
9
+ *
10
+ * 1. glTF `EXT_mesh_features` + `EXT_structural_metadata`, already fully
11
+ * decoded by loaders.gl's GLTFLoader by the time a tile is even
12
+ * renderable (`Tile3D.content.gltf`, post `postProcessGLTF` — verified
13
+ * against node_modules/@loaders.gl/{gltf,3d-tiles} source: primitive
14
+ * `.extensions` and the top-level `EXT_structural_metadata` survive the
15
+ * postprocess step via object spread, so the same decode this repo's
16
+ * Phase-0 spike proved against a real fixture applies here unchanged).
17
+ * Only used when the content resolves to exactly ONE feature — Phase 1
18
+ * has no per-vertex resolution (that's Phase 2's `pick-features` shader
19
+ * work), so a multi-feature content would be misattributing properties
20
+ * to whichever vertex the coarse tile-level pick happened to return.
21
+ * 2. **Legacy 3D Tiles 1.0 batch tables** — b3dm's older per-feature
22
+ * mechanism (a JSON `batchTableJson` of `{propertyName: [oneValuePerBatch]}`
23
+ * arrays, keyed by a `_BATCHID` vertex attribute never surfaced by this
24
+ * layer since Phase 1 doesn't do per-vertex work). Verified directly
25
+ * against real production data (NYC DoITT building footprints and Japan's
26
+ * PLATEAU CityGML export, both streamed live via Cesium ion): real-world
27
+ * 3D Tiles datasets overwhelmingly still ship this older mechanism, not
28
+ * 1.1's `EXT_structural_metadata` — this is the metadata source that
29
+ * actually matters for content people can get their hands on today, not
30
+ * just synthetic samples. loaders.gl's b3dm parser
31
+ * (`parse-3d-tile-tables.js`) sets `tile.featureTableJson.BATCH_LENGTH`
32
+ * and `tile.batchTableJson` directly on the SAME parsed-content object
33
+ * `.gltf` lives on (confirmed by reading the parser itself — no
34
+ * intermediate class wraps it). Same single-feature-only restriction as
35
+ * the glTF path, same reasoning: `BATCH_LENGTH === 1` is unambiguous,
36
+ * `BATCH_LENGTH > 1` needs per-vertex `_BATCHID` resolution this layer
37
+ * doesn't attempt.
38
+ * 3. 3D Tiles 1.1's OWN (non-glTF) metadata mechanism — tileset/group/tile/
39
+ * content-level entities declared directly in tileset.json as plain
40
+ * inline JSON (no binary property-table decode needed at this
41
+ * granularity, unlike the glTF one — verified against a real tileset.json,
42
+ * CesiumGS/3d-tiles-samples 1.1/MetadataGranularities). loaders.gl's
43
+ * `Tile3D` retains the raw tile node verbatim on `.header`, and
44
+ * `Tileset3D` retains the raw parsed tileset.json on `.tileset`, so this
45
+ * reads entirely off already-retained data.
46
+ */
47
+ interface MetadataEntity {
48
+ class?: string;
49
+ properties?: Record<string, unknown>;
50
+ }
51
+ interface GltfPropertyTable {
52
+ class?: string;
53
+ count: number;
54
+ /** Decoded column data. NOT numeric-only — STRING properties decode to `string[]`, which is what most BIM classification fields are. */
55
+ properties?: Record<string, {
56
+ data?: ArrayLike<unknown>;
57
+ }>;
58
+ }
59
+ export interface GltfExtStructuralMetadata {
60
+ schema?: {
61
+ classes?: Record<string, {
62
+ name?: string;
63
+ }>;
64
+ };
65
+ propertyTables?: GltfPropertyTable[];
66
+ }
67
+ interface GltfMeshFeaturesId {
68
+ attribute?: number;
69
+ /** Present instead of `attribute` for "feature ID by texture coordinates" sets — see resolvePrimitiveFeatureIdSet. */
70
+ texture?: {
71
+ index?: number;
72
+ texCoord?: number;
73
+ channels?: number[];
74
+ };
75
+ propertyTable?: number;
76
+ data?: ArrayLike<number>;
77
+ }
78
+ export interface GltfPrimitiveLike {
79
+ extensions?: {
80
+ EXT_mesh_features?: {
81
+ featureIds?: GltfMeshFeaturesId[];
82
+ };
83
+ };
84
+ }
85
+ /**
86
+ * Finds the `EXT_mesh_features` feature-ID set selected by a `_FEATURE_ID_N`
87
+ * name (the manifest's `feature-id-property`, default `_FEATURE_ID_0`) on one
88
+ * glTF primitive. Returns null when the primitive carries no usable feature
89
+ * IDs (a plain, non-BIM mesh — the common case until real IFC-derived content
90
+ * is loaded).
91
+ *
92
+ * `EXT_mesh_features` defines THREE ways to carry feature IDs, and N selects
93
+ * across all of them:
94
+ *
95
+ * - **By vertex** (`attribute: N` -> the `_FEATURE_ID_N` vertex attribute).
96
+ * Matched by attribute number first, so an explicitly numbered set always
97
+ * wins over its position in the array.
98
+ * - **By texture** (`texture: {...}`, no `attribute`) — how photogrammetry
99
+ * classification datasets carry per-element IDs, since a photogrammetry
100
+ * mesh is one triangle soup with the classification painted on. These sets
101
+ * have no attribute number to match, so N falls back to selecting the Nth
102
+ * entry of the `featureIds` array — the same positional convention the
103
+ * extension's own `featureId_N` shader naming uses.
104
+ * - **By index** (neither field; the vertex's own index is its feature ID).
105
+ * loaders.gl leaves `data` empty for these, so they resolve to null here.
106
+ *
107
+ * CAVEAT for texture-backed sets: loaders.gl decodes them by sampling the
108
+ * feature-ID texture once PER VERTEX at that vertex's UV
109
+ * (`getPrimitiveTextureData`), not per fragment. The resulting IDs are
110
+ * therefore only as precise as the mesh is dense — a classification boundary
111
+ * that runs through the middle of a triangle is lost, and that triangle takes
112
+ * whichever ID its vertices sampled. Good enough to pick an element, not
113
+ * equivalent to a per-fragment lookup.
114
+ *
115
+ * Note both texture and index sets require the glTF to be loaded with
116
+ * `loadBuffers` (and, for textures, `loadImages`); without them loaders.gl
117
+ * leaves `data` undefined/empty and this returns null.
118
+ */
119
+ export declare function resolvePrimitiveFeatureIdSet(primitive: GltfPrimitiveLike | undefined, featureIdProperty: string): {
120
+ data: ArrayLike<number>;
121
+ propertyTable?: number;
122
+ texture?: GltfMeshFeaturesId["texture"];
123
+ nullFeatureId?: number;
124
+ } | null;
125
+ /**
126
+ * Reads ONE property of a decoded property table as a column — one entry per
127
+ * feature ID, indexed by feature ID.
128
+ *
129
+ * This is the row-major counterpart to `resolveGltfFeatureRow`: the declarative
130
+ * `hide-features`/`ghost-features`/`isolate-features` attributes need to test
131
+ * a single field across EVERY feature at once, and building a full properties
132
+ * object per row just to read one key would allocate the whole table to answer
133
+ * a one-column question. Returns null when the table or property is absent.
134
+ */
135
+ export declare function readPropertyColumn(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined, property: string): unknown[] | null;
136
+ /**
137
+ * The WHOLE property table as row objects, indexed by feature ID.
138
+ *
139
+ * Picking resolves one row at a time, which is right for a hover but useless
140
+ * for anything that needs the set: a legend listing every IfcClass, a category
141
+ * palette, a model tree. Those previously had to fetch a hand-built sidecar
142
+ * next to the tileset. This reads the same table the GLB already carries.
143
+ */
144
+ export declare function extractPropertyTableRows(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined): Record<string, unknown>[] | null;
145
+ /**
146
+ * Resolves one feature's properties out of a specific decoded property
147
+ * table, by row index — the general case (Phase 2: `rowIndex` is the
148
+ * per-vertex feature ID; Phase 1's single-feature path below is just this
149
+ * with `rowIndex = 0` on a `count === 1` table). Returns null for an
150
+ * out-of-range table/row (defensive — a malformed or partially-loaded tile
151
+ * should degrade to "no properties," not throw during picking).
152
+ */
153
+ export declare function resolveGltfFeatureRow(structuralMetadata: GltfExtStructuralMetadata | undefined, propertyTableIndex: number | undefined, rowIndex: number): {
154
+ properties: Record<string, unknown>;
155
+ class: string | null;
156
+ } | null;
157
+ export interface Tile3DPickMetadata {
158
+ /** Merged properties, most-specific source wins: glTF content / legacy batch table > 3D-Tiles content > tile > group > tileset. Empty object if nothing resolved. */
159
+ properties: Record<string, unknown>;
160
+ /** The class name of the most specific contributing source, or null if nothing resolved. */
161
+ class: string | null;
162
+ /** Best-effort identity (IFC GlobalId / gml:id / guid), or null — see findGuidLike. */
163
+ guid: string | null;
164
+ /** Unmerged, per-source view — the escape hatch. Deck.gl/loaders.gl shapes leak here deliberately, unlike `properties`/`class`. */
165
+ rawMetadata: {
166
+ tileset?: MetadataEntity;
167
+ group?: MetadataEntity;
168
+ tile?: MetadataEntity;
169
+ content?: MetadataEntity;
170
+ gltfProperties?: Record<string, unknown>;
171
+ gltfClass?: string;
172
+ batchTableProperties?: Record<string, unknown>;
173
+ batchTableClass?: string;
174
+ };
175
+ }
176
+ /**
177
+ * Resolves whatever tile-granularity BIM metadata is available for a
178
+ * Tile3DLayer pick. `sourceTile` is `PickingInfo.sourceTile` (Tile3DLayer's
179
+ * own `getPickingInfo` always sets it, undefined for a miss) — callers
180
+ * should only invoke this once they know the pick landed on a
181
+ * `carriesTileset` layer. Returns null when nothing resolved (a tileset
182
+ * with no metadata at all — the common case today, since this is new).
183
+ */
184
+ export declare function resolveTile3DPickMetadata(sourceTile: unknown): Tile3DPickMetadata | null;
185
+ export {};
package/dist/version.d.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * the build rootDir, and a `define` would need repeating across vite/vitest/
6
6
  * vite-node configs.
7
7
  */
8
- export declare const LIBRARY_VERSION = "0.6.0";
8
+ export declare const LIBRARY_VERSION = "0.6.1";
@@ -36,6 +36,17 @@ export interface WidgetImpl {
36
36
  */
37
37
  neverHides?: boolean;
38
38
  render(ctx: RuntimeContext, host: WidgetHost): void | Promise<void>;
39
+ /**
40
+ * Optional teardown, called by <om-widget> when the element is REMOVED for
41
+ * real — an author delete, or an SPA unmounting the whole <om-map> — never
42
+ * on the transient disconnect of a slot/fold/cluster reparent (om-widget
43
+ * distinguishes the two; see its disconnectedCallback). For releasing
44
+ * resources the document would otherwise pin past the widget's life: blob
45
+ * URLs, external listeners, large cached tables held in `host.state`.
46
+ * May fire again after a re-connect if the widget is removed a second
47
+ * time, so implementations must tolerate repeat calls.
48
+ */
49
+ destroy?(host: WidgetHost): void;
39
50
  }
40
51
  /** Clusterable (spec: "Widget Layout Manager") — the type is compact AND the author didn't opt out. One predicate for the cluster pass AND the validation lint. */
41
52
  export declare function isClusterableWidget(el: Element): boolean;
@@ -0,0 +1,18 @@
1
+ interface ClashItem {
2
+ id: number;
3
+ ifcClass: string;
4
+ name: string;
5
+ box: number[];
6
+ }
7
+ /** Rows to clash candidates, dropping ignored classes and anything with no real box. */
8
+ export declare function clashCandidates(rows: Record<string, unknown>[]): ClashItem[];
9
+ /**
10
+ * Axis-aligned overlap, with a tolerance that SHRINKS both boxes.
11
+ *
12
+ * The strict comparison matters: two slabs meeting exactly face to face share a
13
+ * plane, and reporting that would mean every floor clashes with every wall
14
+ * standing on it. `tolerance` then demands a real interpenetration of that
15
+ * depth before a pair counts, which is how modelling noise gets tuned out.
16
+ */
17
+ export declare function boxesOverlap(a: number[], b: number[], tolerance: number): boolean;
18
+ export {};
@@ -0,0 +1,41 @@
1
+ export declare function ensureStyles(root: ShadowRoot): void;
2
+ /** Every `<om-layer pick-features>` on a map — the BIM widgets' shared universe of pickable models. */
3
+ export declare function pickFeatureLayers(map: Element | null | undefined): Element[];
4
+ interface TreeNode {
5
+ /** Full prefix identifying this node — also its key in the visibility state. */
6
+ key: string;
7
+ label: string;
8
+ count: number;
9
+ /** Every concrete `spatialPath` value at or below this node. */
10
+ values: string[];
11
+ children: Map<string, TreeNode>;
12
+ }
13
+ /**
14
+ * Distinct spatial paths -> a nested tree, with counts aggregated upward.
15
+ *
16
+ * Depth is NOT uniform in real models: the bridge sample mixes 1-level and
17
+ * 5-level paths in one file, and a model can leave elements with no spatial
18
+ * container at all (700 of the clinic's 2,626). Both are represented rather
19
+ * than dropped — an element that vanishes from the tree is an element nobody
20
+ * can find again.
21
+ */
22
+ export declare function buildSpatialTree(counts: Map<string, number>): TreeNode;
23
+ /**
24
+ * Writes an attribute only when it would actually change.
25
+ *
26
+ * These widgets re-render on the `layers`/`features` tokens and their writes
27
+ * mutate layers, so an unconditional write schedules the MutationObserver →
28
+ * reconcile → full parseManifest pass that produced it — on EVERY render,
29
+ * even when nothing changed. `setAttribute` notifies observers even when the
30
+ * value is identical, so equality has to be checked here rather than relied
31
+ * on.
32
+ *
33
+ * It also matters for size: an isolate list or a serialized feature-styles
34
+ * table spans every element and runs to tens of kilobytes, and re-parsing
35
+ * that on a loop would be felt.
36
+ *
37
+ * Shared with ifc-clash (imported from here — the dependency already flows
38
+ * that way for ensureStyles/pickFeatureLayers).
39
+ */
40
+ export declare function setIfChanged(element: Element | null, name: string, value: string | null): void;
41
+ export {};