@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,88 @@
1
+ /**
2
+ * XY snapping (spec: issue #34 Part A). Two-stage, deliberately NOT a
3
+ * spatial index: deck.gl's own hover/click pick already tells us WHICH
4
+ * feature is under the cursor (stage 1 — free, runs every frame regardless
5
+ * of snapping) — this module refines that ONE feature's own geometry to
6
+ * the nearest vertex/edge/midpoint within a pixel tolerance (stage 2, CPU,
7
+ * only when snapping is on). OnlyMapJS never calls `pickObject` itself for
8
+ * this; the resolver runs on the SAME `PickingInfo` runtime-core.ts's
9
+ * existing `onHover`/`onClick` callbacks already receive every frame, at
10
+ * the exact point they currently discard everything but a bare
11
+ * `[lng, lat]` (see `coordOf`'s call sites).
12
+ *
13
+ * Agent vocabulary follows ArcGIS (the lingua franca every surveyor
14
+ * already knows): Vertex, Edge, Midpoint here; Endpoint/Intersection/
15
+ * Point-cloud/Snap-to-sketch are documented follow-ups (see the module's
16
+ * own README section on issue #34), Tangent skipped outright (CAD
17
+ * heritage, no geospatial use). "Closest wins" is NOT the tie-break when
18
+ * two agents both have a candidate within tolerance — Vertex beats
19
+ * Midpoint beats Edge (ArcGIS's own conflict order: a midpoint sitting
20
+ * near an edge should snap to the more specific point, not the general
21
+ * one), ties WITHIN one agent broken by distance.
22
+ */
23
+ export type SnapAgent = "vertex" | "edge" | "midpoint";
24
+ export declare const SNAP_AGENTS: readonly SnapAgent[];
25
+ export declare const DEFAULT_SNAP_TOLERANCE_PX = 12;
26
+ export interface SnapConfig {
27
+ agents: ReadonlySet<SnapAgent>;
28
+ tolerancePx: number;
29
+ }
30
+ export interface SnapResult {
31
+ /** The snapped [lng, lat] — interpolated for edge/midpoint, exact for vertex. */
32
+ position: [number, number];
33
+ agent: SnapAgent;
34
+ /** The snapped point's elevation in meters, when the matched geometry carried one — for ANCHORING UI at the visible point (the snap tip); the position contract itself stays XY. */
35
+ elevation?: number;
36
+ }
37
+ /** Minimal viewport contract this module needs — matches deck.gl's real `Viewport.project`. */
38
+ export interface SnapViewport {
39
+ project(coordinate: number[]): number[];
40
+ }
41
+ interface GeoJsonGeometryLike {
42
+ type: string;
43
+ coordinates: unknown;
44
+ }
45
+ /**
46
+ * `snap="vertex edge midpoint"` / `snap-tolerance="12"` on `<om-map>` →
47
+ * `SnapConfig`, or `null` when `snap` is absent/empty (the common case,
48
+ * must stay free — no pickingRadius change, no per-pick resolver call).
49
+ * Unrecognized tokens (a typo, or a future agent not implemented yet) are
50
+ * silently dropped rather than rejecting the whole attribute — validation.ts's
51
+ * `validateSnap` carries the loud version of that same check.
52
+ */
53
+ export declare function parseSnapAttrs(getAttr: (name: string) => string | null): SnapConfig | null;
54
+ /**
55
+ * The geometry a picked object can be snapped against. GeoJSON-shaped
56
+ * objects (GeoJsonLayer, MVT, draw features) carry it directly — but MOST
57
+ * OnlyMapJS data is flat rows (CSV/JSON + `get-position` accessors), whose
58
+ * picked object is just the row, with no `.geometry` at all: snapping
59
+ * silently found nothing on them (reported: "doesn't detect the vertices in
60
+ * most of my data"). For those, the layer's own resolved accessors ARE the
61
+ * geometry — evaluate them against the picked row exactly the way the
62
+ * layer's shader binding did: `get-polygon` → Polygon, `get-path` →
63
+ * LineString, `get-source-position`+`get-target-position` → a two-point
64
+ * LineString, `get-position` → Point.
65
+ *
66
+ * Guarded to geographic layers: a layer in an offset/cartesian coordinate
67
+ * system (a BIM mesh sublayer, a local-frame point cloud) yields raw local
68
+ * meters from these accessors, not lng/lat — callers handle those through
69
+ * their own conversion branches (see resolveMapPoint's BIM-edges path).
70
+ */
71
+ export declare function geometryFromPick(layerProps: Record<string, unknown>, object: unknown): GeoJsonGeometryLike | null;
72
+ /**
73
+ * Resolves a hover/click's cursor against ONE already-picked feature's
74
+ * geometry. `cursorPx`/`viewport` project every candidate to screen for
75
+ * pixel-accurate proximity; the winning MAP-space position is derived by
76
+ * interpolating the geometry's own [lng, lat] vertices with the SAME
77
+ * fraction found in screen space (an approximation for edge/midpoint on a
78
+ * long, steeply-projected segment, but exact for the vertex agent, and
79
+ * consistent with this codebase's own documented sphere-vs-ellipsoid,
80
+ * screen-space-drag-math tradeoffs elsewhere — e.g. geodesy.ts, the
81
+ * clip-box gizmo). Returns `null` when nothing is within tolerance, or the
82
+ * geometry type isn't one `extractRings` understands (e.g. a raw
83
+ * meter-offsets row, which this module does not attempt to project itself
84
+ * — see this file's own header comment on BIM/PathLayer content being a
85
+ * documented follow-up).
86
+ */
87
+ export declare function resolveSnap(geometry: GeoJsonGeometryLike | null | undefined, cursorPx: readonly number[], viewport: SnapViewport, config: SnapConfig): SnapResult | null;
88
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Bulk terrain heightfield (spec item C2 / "Elevation profile" — the shared
3
+ * infrastructure both reference, "sampling shares C2's bulk heightfield, not
4
+ * per-point elevationAt()"). `terrain-sample.ts`'s `sampleTerrainElevation`
5
+ * is one fetch per point — fine for volume mode's handful of footprint
6
+ * vertices, but the wrong shape for a profile's tens-to-hundreds of samples
7
+ * along a line or the volumetrics grid's 10⁵–10⁶ cells: nearby points share
8
+ * DEM tiles, so fetching (and decoding) each tile once and reading every
9
+ * point out of it in memory is both fewer round-trips and what the per-cell
10
+ * volume integrator (volumetrics.ts) builds on.
11
+ *
12
+ * This module is the DOM-TOUCHING half (fetch + canvas decode); the pixel
13
+ * math — bilinear sampling with tile-seam handling — lives in
14
+ * volumetrics.ts's `sampleBilinear`, which is pure and shared with the
15
+ * volumetrics worker (decoded tiles postMessage across; canvases don't).
16
+ */
17
+ import type { LngLat } from "./geodesy";
18
+ import type { TerrainIR } from "./terrain";
19
+ import { type HeightfieldTiles } from "./volumetrics";
20
+ /** A resolved, in-memory set of DEM tiles — `elevationAt` never touches the network. */
21
+ export interface TerrainHeightfield {
22
+ /** Ground elevation (meters) at `lngLat` — bilinear-interpolated, seam-correct — or `null` if no covering tile loaded/decoded. Ignores `terrain.exaggeration`, same as `sampleTerrainElevation`. */
23
+ elevationAt(lngLat: LngLat): number | null;
24
+ /** The raw decoded tile set — the transferable form volumetrics.ts (and its worker) consume directly. */
25
+ tiles: HeightfieldTiles;
26
+ }
27
+ /**
28
+ * Resolves every DEM tile `points` touches — INCLUDING the neighbor tiles a
29
+ * bilinear read at a tile-edge point spills into (the seam case: a point
30
+ * within half a pixel of a tile border reads up to 3 pixels from adjacent
31
+ * tiles; without pre-loading those, edge points would silently degrade to
32
+ * fewer-corner interpolation) — fetches + decodes each ONE TIME
33
+ * (deduplicated, in parallel), and returns a synchronous bilinear lookup. A
34
+ * tile that fails to load makes points inside it resolve to `null` (same
35
+ * "treat as no terrain" contract as `sampleTerrainElevation`) without
36
+ * affecting other tiles.
37
+ */
38
+ export declare function loadHeightfield(points: LngLat[], terrain: TerrainIR): Promise<TerrainHeightfield>;
39
+ /**
40
+ * Resolves the DEM tile set COVERING a lng/lat bounding box — the
41
+ * volumetrics grid's shape of demand ("every cell inside the polygon's
42
+ * bbox"), which a point list can't express without enumerating 10⁶ cells.
43
+ * Starts at the provider's `maxZoom` and steps down until the cover fits
44
+ * `MAX_BOUNDS_TILES` — the returned `tiles.zoom` records what was actually
45
+ * used, and the integrator derives its GSD (and error model) from that, so
46
+ * a coarsened cover is REPORTED coarser, never silently pretended finer.
47
+ */
48
+ export declare function loadHeightfieldForBounds(bounds: {
49
+ west: number;
50
+ south: number;
51
+ east: number;
52
+ north: number;
53
+ }, terrain: TerrainIR): Promise<TerrainHeightfield>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One-shot terrain elevation sampling (spec: "Cut/fill volume measurement").
3
+ * Kept separate from terrain.ts, whose header documents it as PURE and
4
+ * unit-testable without a GPU — this module is deliberately the DOM-touching
5
+ * half (fetch + canvas), for the one thing nothing else in this codebase
6
+ * needed before: "what is the real ground height at this lng/lat".
7
+ *
8
+ * Fetches the DEM tile as a blob (never an `<img src>`) so the canvas read
9
+ * below is never tainted regardless of the provider's CORS posture — and
10
+ * since deck.gl's own TerrainLayer already fetches this exact URL template
11
+ * cross-origin to RENDER the surface, a caller only ever reaches this with a
12
+ * URL that's already proven fetchable.
13
+ */
14
+ import type { LngLat } from "./geodesy";
15
+ import type { TerrainIR } from "./terrain";
16
+ /** Slippy-map tile + fractional pixel covering `lngLat` at `zoom` (256px tiles). Exported for `terrain-heightfield.ts`'s bulk loader — same tile math, no reason to duplicate it. */
17
+ export declare function tilePixel(lngLat: LngLat, zoom: number): {
18
+ x: number;
19
+ y: number;
20
+ px: number;
21
+ py: number;
22
+ };
23
+ /**
24
+ * Ground elevation (meters) at `lngLat`, decoded from `terrain`'s DEM at its
25
+ * configured `maxZoom`. `null` on any fetch/decode failure — the caller
26
+ * treats that the same as "no terrain": flat ground at 0m, not a thrown error.
27
+ * Ignores `terrain.exaggeration` deliberately: that scales the RENDERED
28
+ * surface for legibility, volume math wants the true decoded height.
29
+ */
30
+ export declare function sampleTerrainElevation(lngLat: LngLat, terrain: TerrainIR): Promise<number | null>;
package/dist/terrain.d.ts CHANGED
@@ -99,7 +99,12 @@ export interface TerrainLayerPatch {
99
99
  /**
100
100
  * Per-layer terrain patches, PURE. `terrainActive` false → identity patches
101
101
  * (stable ids, no props). Runtime-internal layers (trace temps, draw
102
- * preview, tooltip plumbing) never drape — they're screen-space/transient.
102
+ * preview, tooltip plumbing) default OFF — they're screen-space/transient
103
+ * but an internal layer that explicitly sets `terrain` (spec: "Cut/fill
104
+ * volume measurement": the shared draw-preview and measure's committed-
105
+ * geometry/label layers opt into "drape" so their 2D lng/lat vertices sit ON
106
+ * the terrain surface instead of at literal sea level, invisible below/
107
+ * behind a raised mesh) wins over that default, same as an authored layer.
103
108
  */
104
109
  export declare function applyTerrain(irs: readonly LayerIR[], terrainActive: boolean, generation: number): Map<string, TerrainLayerPatch>;
105
110
  /**
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/units.d.ts CHANGED
@@ -22,6 +22,33 @@ export declare function formatLength(meters: number, system?: UnitSystem): strin
22
22
  * question 1 — nmi² is not a denomination users read).
23
23
  */
24
24
  export declare function formatArea(m2: number, system?: UnitSystem): string;
25
+ /**
26
+ * Format a volume (m³, spec: "Cut/fill volume measurement"), with denomination
27
+ * crossover. Metric: m³, crossing to "×10³ m³" past 1000 m³ (the km² crossover
28
+ * shape). Imperial: cubic yards — the real-world earthworks unit, not ft³ — with
29
+ * the same ×10³ crossover. Nautical falls back to metric (formatArea's own
30
+ * documented fallback: nmi³ is not a denomination anyone reads).
31
+ */
32
+ export declare function formatVolume(m3: number, system?: UnitSystem): string;
33
+ /**
34
+ * Format a mass (kg, spec item D — "tonnage"), with denomination crossover.
35
+ * Metric: tonnes ("t"), crossing to "×10³ t" past 1000 t (matching
36
+ * {@link formatVolume}'s own crossover shape). Imperial: US short tons
37
+ * ("ton", 2000 lb) — the earthworks-industry convention, not the UK long
38
+ * ton. Nautical falls back to metric (formatArea/formatVolume's own
39
+ * documented fallback).
40
+ */
41
+ export declare function formatMass(kg: number, system?: UnitSystem): string;
42
+ /**
43
+ * Convert an author-provided density (spec item D — `density` attribute) to
44
+ * canonical kg/m³, interpreted per the ACTIVE unit system at the moment it's
45
+ * set: metric authors give t/m³ (tonnes per cubic meter — the earthworks
46
+ * convention, e.g. 1.6-1.8 for common soil), imperial authors give lb/yd³.
47
+ * Nautical falls back to the metric (t/m³) reading. Canonicalizing once here
48
+ * means a later unit-system TOGGLE (the widget's own units buttons) doesn't
49
+ * need to re-interpret an already-set density — kg/m³ is unit-system-agnostic.
50
+ */
51
+ export declare function densityToKgM3(raw: number, system: UnitSystem): number;
25
52
  /**
26
53
  * The largest 1/2/3/5 ×10ⁿ value ≤ `value` (GL's log10 "nice number"). Used by
27
54
  * the scale bar to snap its bar to a round distance. log10-based, so it is
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.2";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Volumetrics runner — dispatches a `VolumetricsRequest` to the inline-blob
3
+ * Web Worker (volumetrics-worker.ts) and falls back to a synchronous
4
+ * main-thread `computeVolumetrics` call when worker construction isn't
5
+ * possible (no `Worker` global — headless test environments, exotic
6
+ * embeddings), exactly raster.ts's COG-decode posture: "slower, but never
7
+ * blank." One fresh worker per run, terminated on completion — integrations
8
+ * fire once per footprint commit (not per frame; drag frames re-sum the
9
+ * CACHED grid via `reintegrateCustomBase` without ever coming back here),
10
+ * so a persistent pool would be state without a workload.
11
+ */
12
+ import { type VolumetricsRequest, type VolumetricsResult } from "./volumetrics";
13
+ export declare function runVolumetrics(req: VolumetricsRequest): Promise<VolumetricsResult>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Volumetric integration core (spec: issue #35 "Volumetric measurement —
3
+ * cut/fill against a base surface, with a published error model"; grid
4
+ * design per §C of the acceptance spec: C1 tangent-plane grid, C2 bulk
5
+ * heightfield sampling, C3 scanline point-in-polygon).
6
+ *
7
+ * PURE by design — no DOM, no fetch, no globals. This module runs both on
8
+ * the main thread and inside `volumetrics-worker.ts` (the issue's "all of it
9
+ * in a worker"), so the heightfield arrives as already-decoded pixel buffers
10
+ * (`HeightfieldTiles`, produced by terrain-heightfield.ts's DOM half) and
11
+ * everything here is arithmetic over them.
12
+ *
13
+ * The three load-bearing choices, per the spec:
14
+ *
15
+ * C1 — the grid lives in a LOCAL TANGENT-PLANE metric frame at the ring
16
+ * centroid (x = R·cos(φ₀)·Δλ, y = R·Δφ, on the same measure sphere as
17
+ * geodesy.ts), NOT in lng/lat ("cell size 0.25m" is meaningless in degrees)
18
+ * and NOT in uncorrected Web Mercator (linear scale sec(φ) → area/volume
19
+ * error sec²(φ), +100% at 45° latitude). Equirectangular error over a
20
+ * stockpile-sized extent (≤ a few km) is ~10⁻⁵ — far below the DEM's own
21
+ * z noise.
22
+ *
23
+ * C2 — the heightfield is bulk-decoded ONCE upstream and bilinear-sampled
24
+ * in memory here; never an async per-cell fetch (a fine grid is 10⁵–10⁶
25
+ * cells — one network call per cell is dead on arrival).
26
+ *
27
+ * C3 — point-in-polygon by SCANLINE, not per-cell ray-cast: per grid row,
28
+ * intersect ring edges with the row's y, sort crossings, fill spans —
29
+ * O(rows × edges + inside cells) instead of O(cells × edges), and it emits
30
+ * exactly the inside-cell runs the integrator iterates. Fractional
31
+ * boundary-cell coverage is deliberately skipped (documented; the market
32
+ * leader doesn't bother either).
33
+ */
34
+ import type { LngLat } from "./geodesy";
35
+ export interface ElevationDecoderSpec {
36
+ rScaler: number;
37
+ gScaler: number;
38
+ bScaler: number;
39
+ offset: number;
40
+ }
41
+ /** One decoded DEM tile — plain buffers so the whole set can postMessage to the worker (Uint8ClampedArray is structured-cloneable, and its ArrayBuffer transferable). */
42
+ export interface HeightfieldTile {
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ height: number;
47
+ data: Uint8ClampedArray;
48
+ }
49
+ /** The serializable heightfield: every decoded tile at one zoom + the decoder. Produced by terrain-heightfield.ts, consumed here (main thread or worker). */
50
+ export interface HeightfieldTiles {
51
+ zoom: number;
52
+ tiles: HeightfieldTile[];
53
+ decoder: ElevationDecoderSpec;
54
+ }
55
+ /**
56
+ * Bilinear heightfield sample at `lngLat`, correct across TILE SEAMS.
57
+ *
58
+ * Works in global fractional pixel space at the heightfield's zoom, in each
59
+ * tile's NATIVE resolution scaled to a common 256-per-tile grid: the four
60
+ * pixels around the sample point may live in up to four different tiles, and
61
+ * each is looked up in whichever tile owns it. Corners whose tile is missing
62
+ * or failed drop out and the remaining weights renormalize — a point right
63
+ * at the edge of available data degrades gracefully toward nearest-available
64
+ * rather than snapping to `null`; only a point with NO available corner
65
+ * returns `null` ("no data", the caller's skip-and-count contract).
66
+ *
67
+ * Native-resolution note: tiles can ship at 512px. Sampling positions are
68
+ * computed in 256-grid units (matching `tilePixel`'s convention) and each
69
+ * corner reads the native pixel COVERING that 256-grid cell — bilinear over
70
+ * the 256 grid is slightly coarser than the 512 tile could support, but is
71
+ * consistent across mixed-resolution tile sets and matches the GSD the error
72
+ * model reports.
73
+ */
74
+ export declare function sampleBilinear(hf: HeightfieldTiles, lngLat: LngLat, tileIndex?: Map<string, HeightfieldTile>): number | null;
75
+ /** Key the tile list for O(1) lookup — build once per batch of samples, pass into `sampleBilinear` (it rebuilds per call otherwise). */
76
+ export declare function buildTileIndex(hf: HeightfieldTiles): Map<string, HeightfieldTile>;
77
+ /**
78
+ * Base-surface strategies (spec: "Multiple Base Surfaces"):
79
+ * - `custom`: a constant elevation — the interactive gizmo's target plane
80
+ * (drag sets `customZ`), and the direct successor of the v1 flat math.
81
+ * - `plane`: least-squares plane fit to the ring's own boundary elevations —
82
+ * the right base for a stockpile on visibly sloped ground.
83
+ * - `lowest` / `highest` / `average`: constant at the boundary's min/max/mean
84
+ * elevation.
85
+ * - `triangulated`: boundary TIN (Delaunay over densely-resampled boundary
86
+ * points, interpolated per cell) — the industry default for stockpiles,
87
+ * since it follows the toe of the pile all the way around.
88
+ */
89
+ export type BaseSurfaceKind = "custom" | "plane" | "lowest" | "highest" | "average" | "triangulated";
90
+ export declare const BASE_SURFACE_KINDS: readonly BaseSurfaceKind[];
91
+ export declare function parseBaseSurface(raw: string | null | undefined): BaseSurfaceKind | null;
92
+ export interface BaseSurfaceSpec {
93
+ kind: BaseSurfaceKind;
94
+ /** Required for `kind: "custom"` — the target plane's elevation in meters. */
95
+ customZ?: number;
96
+ }
97
+ export interface VolumetricsRequest {
98
+ /** Footprint ring, lng/lat, open or closed (normalized internally). */
99
+ ring: LngLat[];
100
+ heightfield: HeightfieldTiles;
101
+ base: BaseSurfaceSpec;
102
+ /** The measure sphere radius (pass `getMeasureRadiusMeters()` — a parameter so this module stays free of mutable global state, worker included). */
103
+ radiusM: number;
104
+ /**
105
+ * Boundary elevation samples for the non-custom base surfaces — lng/lat
106
+ * plus the elevation the caller sampled there (they ride the same
107
+ * heightfield; the caller already has them for the profile). Converted to
108
+ * the internal tangent frame here, so the caller never has to reproduce
109
+ * this module's frame math. Required for `plane`/`lowest`/`highest`/
110
+ * `average`/`triangulated`; ignored for `custom`.
111
+ */
112
+ boundary?: Array<{
113
+ position: LngLat;
114
+ z: number;
115
+ }>;
116
+ /** Cell-count budget — cell size grows from the DEM's GSD until the grid fits. Default 1.5M. */
117
+ maxCells?: number;
118
+ /** Return the per-cell grids (`zTerrain`, `inside`) for cheap re-integration on gizmo drag + the heat map. */
119
+ includeGrid?: boolean;
120
+ }
121
+ export interface VolumetricsGrid {
122
+ /** Tangent-frame origin (the ring centroid), lng/lat — for projecting the grid back to the map (heat map bounds). */
123
+ originLngLat: LngLat;
124
+ /** Grid extent: cell (i, j) center is at x = x0 + (i + 0.5)·cellSize, y = y0 + (j + 0.5)·cellSize in tangent meters. */
125
+ x0: number;
126
+ y0: number;
127
+ nx: number;
128
+ ny: number;
129
+ cellSizeM: number;
130
+ /** Terrain elevation per cell, row-major (j·nx + i). NaN = no data. */
131
+ zTerrain: Float32Array;
132
+ /** Base elevation per cell (same layout). NaN outside the ring. */
133
+ zBase: Float32Array;
134
+ /** 1 = inside the ring, 0 = outside. */
135
+ inside: Uint8Array;
136
+ }
137
+ export interface VolumetricsResult {
138
+ cutM3: number;
139
+ fillM3: number;
140
+ netM3: number;
141
+ totalM3: number;
142
+ /** The actual cell size used (≥ the DEM's GSD when the budget forced coarsening). */
143
+ cellSizeM: number;
144
+ /** The elevation source's native ground-sample distance at the ring's latitude — what the error model is quoted against. */
145
+ gsdM: number;
146
+ insideCells: number;
147
+ /** Inside cells skipped for lack of DEM data (missing/failed tiles). */
148
+ nodataCells: number;
149
+ /** ± one-sided error bounds (spec: per-cell `cellArea × 1.5 × GSD_data`, summed separately per side). */
150
+ cutErrorM3: number;
151
+ fillErrorM3: number;
152
+ /** How the base surface resolved (constant z, plane params) — provenance for the readout. */
153
+ base: {
154
+ kind: BaseSurfaceKind;
155
+ z?: number;
156
+ plane?: {
157
+ a: number;
158
+ b: number;
159
+ c: number;
160
+ };
161
+ };
162
+ grid?: VolumetricsGrid;
163
+ }
164
+ /**
165
+ * The grid integrator. Everything in the tangent frame at the ring centroid;
166
+ * per inside cell: z_terrain from the bilinear heightfield, z_base from the
167
+ * strategy, signed delta accumulated into cut (terrain ABOVE base — material
168
+ * to remove) or fill (terrain BELOW base — material to add). Mixed cut+fill
169
+ * within one polygon is the entire point of the per-cell design — the
170
+ * v1 flat math this replaces could only ever be one or the other.
171
+ */
172
+ export declare function computeVolumetrics(req: VolumetricsRequest): VolumetricsResult;
173
+ /**
174
+ * Re-integration for the DEFAULT (non-flat-target) `custom` semantics: the
175
+ * target surface is the terrain itself offset by one constant depth/height —
176
+ * "grade this footprint down/up by N meters from wherever the ground is."
177
+ * Every valid cell moves by exactly `offsetM`, so the result is pure cut OR
178
+ * pure fill (a parallel offset can never produce both), each cell
179
+ * contributing |offset| × cell area — closed-form over the SAME per-cell
180
+ * counts and GSD error model `reintegrateCustomBase` accumulates, so the two
181
+ * stay directly comparable when the flat-target toggle switches between
182
+ * them. `base.z` is deliberately absent: no single plane elevation exists.
183
+ */
184
+ export declare function reintegrateParallelOffset(grid: VolumetricsGrid, offsetM: number, gsdM: number): Omit<VolumetricsResult, "grid" | "base"> & {
185
+ base: {
186
+ kind: "custom";
187
+ };
188
+ };
189
+ /**
190
+ * Cheap re-integration for the interactive flat-target case: the gizmo drag
191
+ * changes ONLY the `custom` base plane's z, so a cached grid re-sums in one
192
+ * arithmetic pass over the terrain samples — no re-sampling, no scanline, no
193
+ * worker round-trip needed per drag frame. Everything except the base offset
194
+ * (cell size, GSD, error basis) is inherited from the original result.
195
+ */
196
+ export declare function reintegrateCustomBase(grid: VolumetricsGrid, customZ: number, gsdM: number): Omit<VolumetricsResult, "grid" | "base"> & {
197
+ base: {
198
+ kind: "custom";
199
+ z: number;
200
+ };
201
+ };