@nika-js/onlymap 0.6.0 → 0.6.2

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 (80) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +39 -14
  3. package/THIRD-PARTY-LICENSES.md +27 -0
  4. package/dist/{LercDecode.es-fbo6h5rt.js → LercDecode.es-BOJJr6Gx.js} +1 -1
  5. package/dist/{basemap-CprSan9q.js → basemap-BofXgXxy.js} +13 -3
  6. package/dist/basemap.d.ts +10 -1
  7. package/dist/cityjson-D_V5GY8b.js +332 -0
  8. package/dist/clip-box-controller.d.ts +94 -0
  9. package/dist/clip-box.d.ts +109 -0
  10. package/dist/crs-Ciu7Xs7a.js +108 -0
  11. package/dist/crs.d.ts +50 -0
  12. package/dist/ctx.d.ts +8 -1
  13. package/dist/data-layer.d.ts +11 -3
  14. package/dist/draw-controller.d.ts +75 -1
  15. package/dist/draw.d.ts +22 -4
  16. package/dist/elements/om-map.d.ts +45 -2
  17. package/dist/elements/om-overlay.d.ts +13 -1
  18. package/dist/elements/om-widget.d.ts +4 -0
  19. package/dist/feature-colors.d.ts +41 -0
  20. package/dist/geodesy.d.ts +29 -3
  21. package/dist/{geoparquet-Bm8pfxyo.js → geoparquet-DapHATA0.js} +1 -1
  22. package/dist/html-data.d.ts +2 -2
  23. package/dist/ifc-Dc-TdSJr.js +877 -0
  24. package/dist/ifc.d.ts +406 -0
  25. package/dist/{index-Bz-rbmhj.js → index-7B-6Cbzu.js} +31619 -26565
  26. package/dist/{index-DSRIZR81.js → index-BF9iO8Tq.js} +1 -1
  27. package/dist/{index-GOwMjqm6.js → index-BmX6IId3.js} +1 -1
  28. package/dist/{index-UzmNhuS0.js → index-SPJVn_n_.js} +1 -1
  29. package/dist/{index-CCQttJzO.js → index-xmJjZxQJ.js} +2 -2
  30. package/dist/index.d.ts +13 -2
  31. package/dist/ir-snapshot.d.ts +3 -1
  32. package/dist/layer-registry.d.ts +14 -0
  33. package/dist/layers/bim-layer.d.ts +127 -0
  34. package/dist/layers/feature-mesh-layer.d.ts +288 -0
  35. package/dist/layers/gltf-scene-walk.d.ts +55 -0
  36. package/dist/layers/popup-layer.d.ts +13 -0
  37. package/dist/legend-spec.d.ts +1 -1
  38. package/dist/{lerc-CuifOkoA.js → lerc-C6k7EzSN.js} +2 -2
  39. package/dist/license.d.ts +12 -5
  40. package/dist/measure-controller.d.ts +458 -4
  41. package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
  42. package/dist/onlymap.standalone.js +57077 -50729
  43. package/dist/onlymapjs.js +72 -69
  44. package/dist/parse-manifest.d.ts +3 -0
  45. package/dist/programmatic.d.ts +39 -5
  46. package/dist/{raster-dysYIQ_k.js → raster-Cuzhibe2.js} +2 -2
  47. package/dist/{raster-pipeline-ZdCOGfqV.js → raster-pipeline-DLrnJK8y.js} +1 -1
  48. package/dist/react/om-layer.d.ts +6 -1
  49. package/dist/react/om-overlay.d.ts +9 -0
  50. package/dist/react.js +176 -169
  51. package/dist/region-export-controller.d.ts +37 -0
  52. package/dist/region-export.d.ts +56 -0
  53. package/dist/runtime-core.d.ts +260 -3
  54. package/dist/selection.d.ts +44 -1
  55. package/dist/site-placement.d.ts +107 -0
  56. package/dist/snapping.d.ts +88 -0
  57. package/dist/terrain-heightfield.d.ts +53 -0
  58. package/dist/terrain-sample.d.ts +30 -0
  59. package/dist/terrain.d.ts +6 -1
  60. package/dist/testing.d.ts +6 -2
  61. package/dist/tile3d-metadata.d.ts +185 -0
  62. package/dist/units.d.ts +27 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/volumetrics-run.d.ts +13 -0
  65. package/dist/volumetrics-worker.d.ts +1 -0
  66. package/dist/volumetrics.d.ts +201 -0
  67. package/dist/widget-registry.d.ts +11 -0
  68. package/dist/widgets/ifc-clash.d.ts +18 -0
  69. package/dist/widgets/ifc.d.ts +41 -0
  70. package/dist/{zarr-OewK7k2K.js → zarr-CUzEX8fB.js} +79 -66
  71. package/docs/3d-assets.md +48 -0
  72. package/docs/live-data.md +9 -1
  73. package/docs/testing.md +4 -2
  74. package/llms.txt +10 -5
  75. package/onlymapjs.html-data.json +313 -2
  76. package/package.json +6 -2
  77. package/skills/onlymapjs/SKILL.md +14 -2
  78. package/skills/onlymapjs/references/react.md +2 -2
  79. package/skills/onlymapjs/references/syntax.md +314 -15
  80. package/dist/cityjson-urQeujQv.js +0 -407
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Region 3D export (spec: issue #34 §"Technical Implementation Steps" —
3
+ * region export; ported from a sibling project's proven implementation
4
+ * rather than rebuilt from scratch, per direct instruction). Draw a 2D
5
+ * footprint over loaded 3D Tiles content (BIM/`BIMLayer`, `Tile3DLayer`)
6
+ * and export exactly the triangles inside it as a portable GLB — or a
7
+ * b3dm-wrapped GLB for pipelines (Cesium, 3D Tiles tooling) that expect
8
+ * that container.
9
+ *
10
+ * Deliberately simpler than the source implementation in one respect: tile
11
+ * selection is a plain bbox prefilter (`tileGeoBounds` vs. the ring's own
12
+ * bbox) rather than the source's mesh-sampling/loose-footprint heuristics —
13
+ * those exist there to avoid walking every triangle of city-scale national
14
+ * tilesets before an exact clip; at this library's current single-tileset
15
+ * (BIM model) scale, bbox-prefilter-then-exact-clip is both simpler and
16
+ * already correct (a tile outside the ring exact-clips to zero triangles
17
+ * regardless), and the same geometric pipeline this module already needs
18
+ * is reused straightforwardly if the heuristic layer becomes worth adding.
19
+ *
20
+ * No textures: BIM/IFC materials are flat colors, not textured meshes, so
21
+ * the UV/image plumbing the source implementation carries is dead weight
22
+ * here — dropped rather than kept for a case this library doesn't have.
23
+ * Each triangle's own source color IS carried through, though — baked as
24
+ * per-vertex `COLOR_0` (see `resolvePrimitiveColor`/`buildGlb`), not left
25
+ * as an undifferentiated flat gray.
26
+ *
27
+ * PURE geometry/binary-writing core (this file) + a thin DOM-touching
28
+ * driver (`region-export-run.ts`) that resolves the live tileset registry
29
+ * and triggers the browser download — same split as terrain-sample.ts /
30
+ * terrain-heightfield.ts.
31
+ */
32
+ export type LngLatRing = Array<[number, number]>;
33
+ /** Closed (first === last) + CCW — committed draw rings arrive closed, previews open; both normalize the same way. */
34
+ export declare function normalizeRing(ring: LngLatRing): LngLatRing;
35
+ export interface B3dmBatchTable {
36
+ /** batchLength: number of features encoded in the glb's `_BATCHID`; each property array below must have this length. */
37
+ batchLength: number;
38
+ /** Plain-JSON per-feature properties — entry i is feature i's value. Omit for a structurally-valid b3dm with no property data. */
39
+ properties?: Record<string, unknown[]>;
40
+ }
41
+ export interface RegionExportResult {
42
+ bytes: Uint8Array;
43
+ triangleCount: number;
44
+ format: "glb" | "b3dm";
45
+ }
46
+ /**
47
+ * Clip every loaded tile's triangles across ALL given tilesets against
48
+ * `ring`, re-frame the clipped result to a local ENU frame at the ring's
49
+ * own centroid (portable — most viewers can't display ECEF-scale
50
+ * coordinates), and pack it as GLB or b3dm. `null` when the ring or the
51
+ * clip produces nothing (an empty region, or a ring drawn where nothing is
52
+ * loaded) — the caller reports that distinctly from a thrown error.
53
+ */
54
+ export declare function computeRegionExport(ring: LngLatRing, tilesets: Map<string, unknown>, options?: {
55
+ format?: "glb" | "b3dm";
56
+ }): RegionExportResult | null;
@@ -1,10 +1,30 @@
1
+ import type { PickingInfo } from "@deck.gl/core";
1
2
  import { type LightingIR } from "./scene-lighting";
2
3
  import { type TerrainIR } from "./terrain";
4
+ import { type ClipBoxIR } from "./clip-box";
5
+ import { type SnapAgent, type SnapConfig } from "./snapping";
3
6
  import type { ViewOrigin } from "./external-store";
4
7
  import type { LayerIR } from "./ir";
5
8
  import { type Selection } from "./selection";
6
9
  import type { ValidationEntry } from "./validation";
7
10
  import type { MapViewport } from "./basemap";
11
+ /**
12
+ * The tile-level EXT_structural_metadata property-table index, resolved
13
+ * independently of `FeatureMeshLayer`'s own copy of this same computation.
14
+ *
15
+ * `tile.content.featureIdPropertyTableIndex` (which `FeatureMeshLayer`
16
+ * eventually sets, in its own `renderLayers()`) is NOT readable here: deck.gl's
17
+ * `Tile3DLayer._onTileLoad` fires the `onTileLoad` prop callback (what feeds
18
+ * this hook) BEFORE it ever constructs `FeatureMeshLayer` for that tile —
19
+ * confirmed against `@deck.gl/geo-layers`'s own `_onTileLoad`/`_getSubLayer`
20
+ * ordering. Reading that field here always saw `undefined`, silently falling
21
+ * back to property table 0 regardless of which table a tile's feature IDs
22
+ * actually reference — invisible only because this repo's own IFC converter
23
+ * never emits more than one table. Recomputing from the tile's raw glTF with
24
+ * the same pure, already-tested helpers `FeatureMeshLayer` itself uses avoids
25
+ * depending on that ordering entirely.
26
+ */
27
+ export declare function resolveTilePropertyTableIndex(gltf: unknown, featureIdProperty: string): number;
8
28
  /** Mapbox presets/protocol URLs aren't implemented — kept maplibre-free so validation/runtime can check without loading the chunk. */
9
29
  export declare function isMapboxBasemap(raw: string): boolean;
10
30
  export interface InitialView {
@@ -25,6 +45,36 @@ export interface CameraOptions {
25
45
  /** Arc the move (zoom-out-and-in) instead of easing directly — MapLibre `flyTo` / deck `FlyToInterpolator`. */
26
46
  curve?: boolean;
27
47
  }
48
+ export { resolveFeatureColor } from "./feature-colors";
49
+ /** One layer's accumulated (per-tile) property tables — see RuntimeCore.featureTables. */
50
+ export interface FeatureTableEntry {
51
+ source: string;
52
+ tiles: Map<string, Record<string, unknown>[]>;
53
+ rows: Record<string, unknown>[];
54
+ rowsVersion: number;
55
+ mappingKey?: string;
56
+ mapping?: import("./feature-colors").FeatureColorMapping | null;
57
+ }
58
+ /**
59
+ * The accumulator, extracted for direct testing: resets on a SOURCE change
60
+ * (the loader widget swapping models under one layer id), REPLACES rows for a
61
+ * re-seen tile key (deck evicts + refetches tiles as new objects — identity
62
+ * dedup accumulated duplicates), and keeps per-tile row arrays because
63
+ * feature IDs are tile-local. Returns the (possibly new) entry.
64
+ */
65
+ export declare function accumulateFeatureRows(tables: Map<string, FeatureTableEntry>, layerId: string, sourceKey: string, tileKey: string, rows: Record<string, unknown>[]): FeatureTableEntry;
66
+ /**
67
+ * The subset of deck.gl's MjolnirGestureEvent a gizmo drag-claim needs (spec:
68
+ * "Cut/fill volume measurement"). `stopPropagation()` sets the underlying
69
+ * event's `handled` flag, which `Controller.isPointInBounds` checks before
70
+ * starting a camera pan — calling it from `onGizmoDragStart` when the gizmo
71
+ * is the picked layer is what lets a LEFT-button drag on the gizmo win over
72
+ * the map's own drag-to-pan (deck dispatches its root onDragStart before the
73
+ * controller's own pan handling for the same gesture, so this is in time).
74
+ */
75
+ export interface GizmoDragEvent {
76
+ stopPropagation(): void;
77
+ }
28
78
  export interface RuntimeCoreCallbacks {
29
79
  /**
30
80
  * Fired whenever the view state changes (pan/zoom/pitch/bearing) — a
@@ -34,7 +84,14 @@ export interface RuntimeCoreCallbacks {
34
84
  * convention, the echo-loop half of two-way store binding.
35
85
  */
36
86
  onViewportChange?: (origin: ViewOrigin) => void;
37
- onSelectionChange?: (selection: Selection | null) => void;
87
+ /**
88
+ * `pickType` is the pointer event that produced this pick — REQUIRED even
89
+ * when `selection` is null (an empty pick), because toSelection discards
90
+ * empty picks and with them their type. Overlays scoped with
91
+ * `selection-type` need it: a click on empty space dismisses a
92
+ * click-anchored popup, a hover over empty space must not.
93
+ */
94
+ onSelectionChange?: (selection: Selection | null, pickType: "hover" | "click") => void;
38
95
  /** Fired once the map (deck.gl standalone, or the basemap) has finished its async init (spec: Behavior Engine "load" event). */
39
96
  onLoad?: () => void;
40
97
  /**
@@ -44,6 +101,17 @@ export interface RuntimeCoreCallbacks {
44
101
  * re-render on every drag-move.
45
102
  */
46
103
  onDragPick?: (selection: Selection) => void;
104
+ /**
105
+ * Raw deck.gl drag lifecycle (spec: "Cut/fill volume measurement"),
106
+ * forwarded verbatim alongside `onDragPick` — for a controller that owns a
107
+ * pickable internal layer (the volume tool's height gizmo) and needs the
108
+ * real `x`/`y`/`viewport`, not a resolved `Selection`. `onDragPick` stays
109
+ * the mechanism for declarative `on="drag"` behaviors; this is the
110
+ * mechanism for a controller driving its own layer via patchAnimatedProps.
111
+ */
112
+ onGizmoDragStart?: (info: PickingInfo, event: GizmoDragEvent) => void;
113
+ onGizmoDrag?: (info: PickingInfo) => void;
114
+ onGizmoDragEnd?: (info: PickingInfo) => void;
47
115
  /**
48
116
  * Every click/hover's map coordinate (spec: "Manual Drawing"), fired
49
117
  * ALONGSIDE onSelectionChange but INCLUDING empty-map events — which
@@ -52,6 +120,31 @@ export interface RuntimeCoreCallbacks {
52
120
  * deck reports none (e.g. off-globe).
53
121
  */
54
122
  onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void;
123
+ /**
124
+ * XY snapping (spec: issue #34 Part A) — fires ALONGSIDE onMapPoint on
125
+ * every click/hover, `null` whenever that point ISN'T a snap (no config,
126
+ * no candidate within tolerance, an opted-out/unsupported layer) — the
127
+ * snap-tip UI's entire "show only while actually snapped" contract reads
128
+ * off this being null vs. set, not off onMapPoint's own coordinate.
129
+ * Carries `position` itself (not just agent/layer) so a consumer never
130
+ * has to correlate this against a SEPARATE onMapPoint firing in the same
131
+ * tick to know where to anchor a tip — both already come from the one
132
+ * `resolveMapPoint` call.
133
+ */
134
+ onSnapPoint?: (result: {
135
+ position: [number, number];
136
+ agent: SnapAgent;
137
+ layerId: string;
138
+ elevation?: number;
139
+ } | null) => void;
140
+ /**
141
+ * A `pick-features` layer decoded its EXT_structural_metadata property
142
+ * table — the whole table, indexed by feature ID. Fires once per layer, on
143
+ * the first tile that carries one, because every tile in a tileset shares
144
+ * the same schema. This is what lets a legend enumerate categories without
145
+ * a hand-built sidecar next to the tileset.
146
+ */
147
+ onFeatureTable?: (layerId: string, rows: Record<string, unknown>[]) => void;
55
148
  /**
56
149
  * A tileset-bearing layer (Tile3DLayer) finished loading its root tileset
57
150
  * — the consumer hook for tools that must reach the LIVE deck tileset
@@ -60,6 +153,30 @@ export interface RuntimeCoreCallbacks {
60
153
  * @loaders.gl/tiles type dependency in the core).
61
154
  */
62
155
  onTilesetLoad?: (layerId: string, tileset: unknown) => void;
156
+ /**
157
+ * A `carriesGeoreference` layer (BIMLayer) finished loading its source file
158
+ * and read whatever georeferencing it declares — `hasFullMapConversion`
159
+ * is true only when the file both resolved a real position
160
+ * (`IfcMapConversion` into a recognized CRS) AND declared
161
+ * `OrthogonalHeight`, the two facts absolute elevation needs to mean
162
+ * anything. Informational for consumers: the library never auto-applies
163
+ * terrain from it (what-you-write-is-what-you-see) — validation instead
164
+ * requires the map to author `terrain` explicitly for these layers.
165
+ *
166
+ * `approximatePlacement` is a narrower, separate fact: true whenever the
167
+ * file's position did NOT come from a real `IfcMapConversion` (IfcSite
168
+ * lat/lon, or no georeference at all) — that source has no rotation data
169
+ * at all, and is very often an authoring tool's default location rather
170
+ * than a survey. A plain boolean (not the raw `originSource` string) to
171
+ * keep this file decoupled from IFC-specific types, same reasoning as
172
+ * `hasFullMapConversion` already being precomputed rather than passed as
173
+ * raw parts. The front-end layer surfaces this as a validation warning.
174
+ */
175
+ onGeoreference?: (layerId: string, info: {
176
+ orthogonalHeight?: number;
177
+ hasFullMapConversion: boolean;
178
+ approximatePlacement: boolean;
179
+ }) => void;
63
180
  /**
64
181
  * Runtime error boundary (spec: "Runtime error boundary") — deck.gl-level
65
182
  * failures (a crashing accessor, an incompatible prop) formatted into the
@@ -103,6 +220,10 @@ export declare class RuntimeCore {
103
220
  private mode;
104
221
  /** Layers reconciled while the basemap chunk is still loading — applied on adapter arrival. */
105
222
  private pendingLayers?;
223
+ /** setDrawCapture's last value — composed with dragPanSuppressed by standaloneControllerOverrides. */
224
+ private drawCaptureActive;
225
+ /** setDragPan's last value, inverted — see standaloneControllerOverrides. */
226
+ private dragPanSuppressed;
106
227
  /** Retained descriptors — what the per-frame channel re-applies against. */
107
228
  private lastIRs;
108
229
  /** layerId → effect-driven plain-prop patches (the per-frame channel). */
@@ -136,8 +257,65 @@ export declare class RuntimeCore {
136
257
  */
137
258
  private terrain;
138
259
  private terrainGeneration;
260
+ /**
261
+ * Clip box (spec: issue #34 §"Cutting / Clipping") — the retained box IR.
262
+ * Non-null patches every opted-in layer with `ClipBoxExtension` in
263
+ * buildLayers (see `applyClipBox`'s own doc comment for the default-
264
+ * applied, `clip="off"`-to-exclude posture). `clipBoxGeneration` bumps on
265
+ * every active-state flip, same reason and same fix as terrain's own
266
+ * generation counter: extension sets must be BIRTH-stable (verified
267
+ * empirically — a box present from the first render works, toggling one
268
+ * onto an already-mounted DataFilterExtension-carrying layer silently
269
+ * blanks it), so patched layers get `#c<generation>`-based deck ids.
270
+ */
271
+ private clipBox;
272
+ private clipBoxGeneration;
273
+ /** XY snapping (spec: issue #34 Part A) — `null` is the free/common case (no pickingRadius change, resolveMapPoint short-circuits to the plain pick). */
274
+ private snapConfig;
275
+ /** See resolveMapPoint's memo comment — the last picked object's derived snap geometry. */
276
+ private snapGeometryMemo;
277
+ /** See pickForSnap's memo comment — the non-terrain layer-id list, keyed by the deck layer array's own identity. */
278
+ private snapLayerIdsMemo;
279
+ /** Spacebar-held suppression (spec: issue #34 Part A) — deliberately leaves `pickingRadius`/`snapConfig` untouched, only gates `resolveMapPoint`'s refinement step, since the underlying pick tolerance isn't what a user means by "hold to disable snapping momentarily." */
280
+ private snapSuppressed;
281
+ /** Per-BIMLayer lonLat/heading/scale, captured off `onGeoreference` (see `BimGeoreferenceInfo`) — `resolveMapPoint`'s only way to convert a picked EdgeRow's raw local vertices back to real `[lng, lat]` for snapping, since that georeference otherwise lives entirely inside BIMLayer's own async load state. */
282
+ private bimGeoreference;
139
283
  /** Rendered deck layer id → authored manifest/controller id (terrain uses fresh renderer ids). */
140
284
  private renderedLayerIds;
285
+ /** The decoded property table for a pick-features layer, or undefined before its first tile. */
286
+ getFeatureTable(layerId: string): Record<string, unknown>[] | undefined;
287
+ /**
288
+ * Decoded property tables per `pick-features` layer, keyed by layer id.
289
+ * Held here rather than in the element because LAYER props derive from them
290
+ * too (`feature-color-by`), not only widgets. `rows` accumulates across
291
+ * every tile that has loaded so far (see `featureTableHook`) — for a
292
+ * grid-tiled model this means the table is only as complete as whatever
293
+ * has actually streamed in, same as any real tiled 3D-Tiles viewer.
294
+ */
295
+ /**
296
+ * Per-layer accumulated property tables (spec: BIM feature picking). Rows
297
+ * are kept PER TILE — feature IDs are tile-local, so a flat concatenation
298
+ * can never be indexed by feature ID (the multi-tile styling bug). `rows`
299
+ * is the derived aggregate for legends/`ctx.features()`/global domains;
300
+ * `tiles` is keyed by a STABLE content key so an evicted-and-refetched tile
301
+ * REPLACES its rows instead of duplicating them, and `source` is the layer's
302
+ * source URL string (not the per-parse rows-array identity, which reset the
303
+ * whole accumulator on every reconcile mid-stream). `mapping` memoizes the
304
+ * derived colour mapping so animation frames reuse one identity — the
305
+ * style-texture-rebuild-per-rAF fix.
306
+ */
307
+ private featureTables;
308
+ /** Fallback identity for tiles that expose no content URL — a stable per-object key. */
309
+ private tileKeyFallback;
310
+ private tileKeyCounter;
311
+ /**
312
+ * The memoized global colour mapping for a layer — rebuilt only when the
313
+ * accumulated table grows or a colour prop changes, so `applyLayers` calls
314
+ * from the per-frame channel (patchAnimatedProps runs one per rAF during
315
+ * story animations) hand every tile the SAME mapping identity and no GPU
316
+ * style texture is ever rebuilt for an unrelated animation.
317
+ */
318
+ private featureColorMappingFor;
141
319
  /**
142
320
  * The basemap attribute suppressed while terrain is active — a flat
143
321
  * MapLibre canvas at sea level visibly desyncs from a raised surface
@@ -192,6 +370,72 @@ export declare class RuntimeCore {
192
370
  */
193
371
  setTerrain(ir: TerrainIR | null): void;
194
372
  getTerrainInternal(): TerrainIR | null;
373
+ /** Clip box on/off/extent change (spec: issue #34) — no basemap-replacement dance like terrain's (a clip box never changes render mode), just a straight re-apply. Bumps the generation on an active-state flip (fresh deck ids — see the `clipBoxGeneration` field doc). */
374
+ setClipBox(ir: ClipBoxIR | null): void;
375
+ getClipBoxInternal(): ClipBoxIR | null;
376
+ /**
377
+ * XY snapping on/off + tolerance change (spec: issue #34 Part A). Unlike
378
+ * clip box/terrain, this never touches the layer list — only deck's
379
+ * `pickingRadius` (0 by default and never otherwise set in this
380
+ * codebase; without it a cursor has to land EXACTLY on rendered pixels
381
+ * to pick anything, which defeats snapping's own point of "near enough
382
+ * counts") and `resolveMapPoint`'s own read of `this.snapConfig` on the
383
+ * next pick — no `applyLayers()` needed.
384
+ */
385
+ /** Spacebar held/released (spec: issue #34 Part A) — no-op when snapping isn't configured at all. */
386
+ setSnapSuppressed(suppressed: boolean): void;
387
+ /**
388
+ * Whether a snap resolver is actually configured — the ONLY condition under
389
+ * which om-map's window-level keydown listener may `preventDefault()` the
390
+ * spacebar. Without this gate, every page embedding an `<om-map>` lost
391
+ * space-to-scroll and space-to-activate-a-focused-button, snapping
392
+ * configured or not (`setSnapSuppressed` no-ops in that case, but
393
+ * `preventDefault` does not).
394
+ */
395
+ hasSnapConfig(): boolean;
396
+ setSnapConfig(config: SnapConfig | null): void;
397
+ /**
398
+ * `coordOf` plus snap refinement — the two-stage resolver (see
399
+ * src/snapping.ts's own header comment): a pick (stage 1) tells us which
400
+ * feature + viewport are under the cursor; `resolveSnap` (stage 2)
401
+ * refines that to the nearest vertex/edge/midpoint. Falls back to the
402
+ * plain pick on no config, no layer/viewport (an empty-map pick), a
403
+ * `snap="off"` layer, or no candidate within tolerance — `resolveSnap`'s
404
+ * own `null` covers the last case AND "geometry shape this module
405
+ * doesn't understand" (e.g. a raw meter-offsets PathLayer row)
406
+ * identically, which is the right behavior either way: nothing to add,
407
+ * use the raw pick.
408
+ *
409
+ * Stage 1 does NOT simply reuse `info` (deck's own top-level pick that
410
+ * already drove `onMapPoint`/`onSelectionChange`) whenever terrain is
411
+ * active — confirmed live, terrain wins deck's pick-buffer resolution
412
+ * against ANY co-located pickable content, vector or BIM mesh alike,
413
+ * even where that content visibly, correctly renders on top of it (the
414
+ * SAME finding already documented on this session's z-tooltip work, one
415
+ * layer up: there it was worked around with an unscoped hover behavior;
416
+ * here, where snapping specifically needs to know WHICH non-terrain
417
+ * feature is under the cursor, that workaround doesn't apply). Instead,
418
+ * re-picks explicitly via `deck.pickObject`, `layerIds`-restricted to
419
+ * everything EXCEPT the terrain layer — deliberately narrow (standalone
420
+ * mode only, terrain's own required mode) rather than a general-purpose
421
+ * picking API this codebase otherwise avoids.
422
+ */
423
+ private resolveMapPoint;
424
+ /**
425
+ * See `resolveMapPoint`'s own doc comment for why this re-picks rather than
426
+ * reusing deck's own top-level `info`. `radius` mirrors the configured
427
+ * tolerance — a vertex sitting outside the tolerance couldn't win anyway,
428
+ * and this keeps the explicit re-pick's own search window consistent with
429
+ * the resolver's.
430
+ *
431
+ * The `layerIds` list is memoized against the layer ARRAY's identity: this
432
+ * runs on every pointer move while snapping is on, and deck only hands out
433
+ * a new array when the layer set actually changes, so the filter+map runs
434
+ * per layer-set change instead of per mousemove. (The `pickObject` call
435
+ * itself is a synchronous GPU readback and remains the real cost here —
436
+ * this just stops adding avoidable per-move allocation on top of it.)
437
+ */
438
+ private pickForSnap;
195
439
  private drainSnapshotCaptures;
196
440
  /** Settle queued snapshot promises when the renderer they were waiting on goes away (mode flip / destroy) — never leave a caller hanging. */
197
441
  private cancelSnapshotCaptures;
@@ -259,7 +503,7 @@ export declare class RuntimeCore {
259
503
  * behavior dispatch, and the overlay flush are same-path by construction.
260
504
  * Available in every mode (a browser-level test may use it too).
261
505
  */
262
- injectPick(selection: Selection | null): void;
506
+ injectPick(selection: Selection | null, pickType?: "hover" | "click"): void;
263
507
  getViewState(): Readonly<ViewState>;
264
508
  /**
265
509
  * Sketch capture (spec: "Manual Drawing", D4) — while a draw tool is active,
@@ -269,6 +513,20 @@ export declare class RuntimeCore {
269
513
  * fight drag-pan, so pan stays live.
270
514
  */
271
515
  setDrawCapture(active: boolean): void;
516
+ /**
517
+ * Suspend/restore drag-to-pan while a gizmo is grabbed — otherwise a drag
518
+ * on the gizmo also pans the camera underneath it. Originally standalone-
519
+ * only (the volume tool's own height gizmo requires terrain, which always
520
+ * forces standalone mode — see setTerrain — so it never hit basemap mode
521
+ * in practice); the clip-box face-handle gizmo (issue #34) has no such
522
+ * precondition and runs fine with a plain basemap, which surfaced the gap
523
+ * for real (reported: dragging a handle panned the map underneath it and
524
+ * the two fighting over every mousemove read as the whole page hanging).
525
+ * `basemap.setInteractive` is the SAME mechanism setDrawCapture already
526
+ * uses for doubleClickZoom, just a different MapLibre gesture handler.
527
+ */
528
+ setDragPan(active: boolean): void;
529
+ private standaloneControllerOverrides;
272
530
  /**
273
531
  * Used by the built-in `zoom-controls` widget's emitted zoom-in/zoom-out
274
532
  * intents. Programmatic, so — unlike a user drag/scroll — standalone mode
@@ -305,4 +563,3 @@ export declare class RuntimeCore {
305
563
  flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void;
306
564
  destroy(): void;
307
565
  }
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;
@@ -13,9 +14,44 @@ export interface Selection {
13
14
  * onSelectionChange), so downstream consumers still always see an object.
14
15
  */
15
16
  index: number;
16
- coordinate: [number, number] | null;
17
+ /**
18
+ * `[lng, lat]`, or `[lng, lat, z]` when deck actually ran the depth-pick
19
+ * pass for this pick (some pickable layer in the scene has `pickable:
20
+ * '3d'` — see terrain.ts and BIMLayer's own hover picking). Without that
21
+ * pass deck's `info.coordinate` is the ray∩z=0-plane intersection, not a
22
+ * real surface point, so a 2-length coordinate here is not "z=0 at this
23
+ * point" — it's "no z was ever computed," and callers (the hover-tooltip
24
+ * z field, this session's own concrete use) must treat it as absent, not
25
+ * zero.
26
+ */
27
+ coordinate: [number, number] | [number, number, number] | null;
17
28
  pixel: [number, number];
18
29
  type: "hover" | "click";
30
+ /**
31
+ * BIM feature-picking epic. Undefined for every pick except a
32
+ * Tile3DLayer pick that resolved SOME metadata; `class`/`properties`
33
+ * follow the same "ctx never leaks deck.gl internals" normalization as
34
+ * the rest of this shape, `rawMetadata` is the deliberate escape hatch.
35
+ * Two sources, most-specific-wins:
36
+ * - Phase 1 (tile granularity, always active): 3D-Tiles-native
37
+ * tileset/group/tile/content entities, merged with a single-feature
38
+ * glTF EXT_structural_metadata property table when the content
39
+ * resolves to exactly one feature. See src/tile3d-metadata.ts.
40
+ * - Phase 2 (per-feature, only when `pick-features` resolved a specific
41
+ * vertex-level feature): overrides Phase 1's properties/class/guid
42
+ * with the picked feature's own row from the same property table,
43
+ * still layered on top of Phase 1's broader tile-level context. See
44
+ * src/layers/feature-mesh-layer.ts.
45
+ */
46
+ properties?: Record<string, unknown>;
47
+ class?: string | null;
48
+ /** 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. */
49
+ guid?: string | null;
50
+ /** 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. */
51
+ featureId?: number;
52
+ /** 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. */
53
+ featureKey?: string;
54
+ rawMetadata?: Tile3DPickMetadata["rawMetadata"];
19
55
  }
20
56
  interface PickingInfoLike {
21
57
  layer?: {
@@ -26,6 +62,13 @@ interface PickingInfoLike {
26
62
  coordinate?: number[];
27
63
  x: number;
28
64
  y: number;
65
+ /** Tile3DLayer's own getPickingInfo always sets this (undefined on a miss) — see tile3d-metadata.ts. */
66
+ sourceTile?: unknown;
67
+ /** 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. */
68
+ featureId?: number;
69
+ properties?: Record<string, unknown>;
70
+ class?: string | null;
71
+ guid?: string | null;
29
72
  }
30
73
  export declare function toSelection(info: PickingInfoLike, type: "hover" | "click", resolveLayerId?: (renderedId: string) => string): Selection | null;
31
74
  export {};
@@ -0,0 +1,107 @@
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
+ * Companion to `localPlacementMatrix` + `coordinateOrigin` (the outline
73
+ * overlay's own placement, see this file's header comment) — converts ONE
74
+ * RAW local vertex (an `EdgeRow.path` point, in the model's own pre-
75
+ * transform local frame — the exact same value `getPath` hands the
76
+ * PathLayer) to real `[lng, lat]`, the SAME two steps deck.gl itself
77
+ * applies to render that vertex on screen: `localPlacementMatrix`'s
78
+ * `scale * Rz(-heading)` first, THEN interpret the result as an east/north
79
+ * metre offset from `coordinateOrigin`. Needed for XY snapping (spec:
80
+ * issue #34 Part A) against the BIM edges overlay specifically: its picked
81
+ * rows carry this raw local form, not GeoJSON, so `snapping.ts`'s
82
+ * resolver — which only understands `[lng, lat]` — cannot search them
83
+ * directly without this.
84
+ *
85
+ * The metres-to-degrees step is the standard small-area linear
86
+ * approximation (matches `getDistanceScales` inside
87
+ * `@math.gl/web-mercator`, which is what ACTUALLY renders this exact
88
+ * vertex on screen today — deliberately consistent with that real
89
+ * projection rather than a more "correct" ellipsoidal model that would
90
+ * silently disagree with it).
91
+ */
92
+ export declare function localOffsetToLngLat(local: readonly [number, number], origin: readonly [number, number], heading: number, scale: number): [number, number];
93
+ /**
94
+ * The `Tileset3D.modelMatrix` that moves a tileset anchored at `baked` onto `p`.
95
+ *
96
+ * `target · Rz · S · anchor⁻¹`: the inverse undoes the frame the converter
97
+ * wrote, then the model is re-planted. With no `site-origin` the target IS the
98
+ * anchor, so the inverse cancels it exactly and only the rotation and scale
99
+ * survive — `site-heading` on its own spins the model where it stands.
100
+ *
101
+ * Both the pivot and the outline overlay's `coordinateOrigin` are the same
102
+ * ground-level site point, so mesh and outlines rotate together.
103
+ */
104
+ export declare function tilesetPlacementMatrix(baked: Matrix4, p: SitePlacement): Matrix4;
105
+ export declare function tilesetBakedFrame(url: string, onReady: () => void): Matrix4 | null | undefined;
106
+ /** Test seam — the cache is process-wide and would leak between cases. */
107
+ export declare function resetSitePlacementCache(): void;