@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.
- package/CHANGELOG.md +63 -1
- package/README.md +39 -14
- package/THIRD-PARTY-LICENSES.md +27 -0
- package/dist/{LercDecode.es-fbo6h5rt.js → LercDecode.es-BOJJr6Gx.js} +1 -1
- package/dist/{basemap-CprSan9q.js → basemap-BofXgXxy.js} +13 -3
- package/dist/basemap.d.ts +10 -1
- package/dist/cityjson-D_V5GY8b.js +332 -0
- package/dist/clip-box-controller.d.ts +94 -0
- package/dist/clip-box.d.ts +109 -0
- package/dist/crs-Ciu7Xs7a.js +108 -0
- package/dist/crs.d.ts +50 -0
- package/dist/ctx.d.ts +8 -1
- package/dist/data-layer.d.ts +11 -3
- package/dist/draw-controller.d.ts +75 -1
- package/dist/draw.d.ts +22 -4
- package/dist/elements/om-map.d.ts +45 -2
- package/dist/elements/om-overlay.d.ts +13 -1
- package/dist/elements/om-widget.d.ts +4 -0
- package/dist/feature-colors.d.ts +41 -0
- package/dist/geodesy.d.ts +29 -3
- package/dist/{geoparquet-Bm8pfxyo.js → geoparquet-DapHATA0.js} +1 -1
- package/dist/html-data.d.ts +2 -2
- package/dist/ifc-Dc-TdSJr.js +877 -0
- package/dist/ifc.d.ts +406 -0
- package/dist/{index-Bz-rbmhj.js → index-7B-6Cbzu.js} +31619 -26565
- package/dist/{index-DSRIZR81.js → index-BF9iO8Tq.js} +1 -1
- package/dist/{index-GOwMjqm6.js → index-BmX6IId3.js} +1 -1
- package/dist/{index-UzmNhuS0.js → index-SPJVn_n_.js} +1 -1
- package/dist/{index-CCQttJzO.js → index-xmJjZxQJ.js} +2 -2
- package/dist/index.d.ts +13 -2
- package/dist/ir-snapshot.d.ts +3 -1
- package/dist/layer-registry.d.ts +14 -0
- package/dist/layers/bim-layer.d.ts +127 -0
- package/dist/layers/feature-mesh-layer.d.ts +288 -0
- package/dist/layers/gltf-scene-walk.d.ts +55 -0
- package/dist/layers/popup-layer.d.ts +13 -0
- package/dist/legend-spec.d.ts +1 -1
- package/dist/{lerc-CuifOkoA.js → lerc-C6k7EzSN.js} +2 -2
- package/dist/license.d.ts +12 -5
- package/dist/measure-controller.d.ts +458 -4
- package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
- package/dist/onlymap.standalone.js +57077 -50729
- package/dist/onlymapjs.js +72 -69
- package/dist/parse-manifest.d.ts +3 -0
- package/dist/programmatic.d.ts +39 -5
- package/dist/{raster-dysYIQ_k.js → raster-Cuzhibe2.js} +2 -2
- package/dist/{raster-pipeline-ZdCOGfqV.js → raster-pipeline-DLrnJK8y.js} +1 -1
- package/dist/react/om-layer.d.ts +6 -1
- package/dist/react/om-overlay.d.ts +9 -0
- package/dist/react.js +176 -169
- package/dist/region-export-controller.d.ts +37 -0
- package/dist/region-export.d.ts +56 -0
- package/dist/runtime-core.d.ts +260 -3
- package/dist/selection.d.ts +44 -1
- package/dist/site-placement.d.ts +107 -0
- package/dist/snapping.d.ts +88 -0
- package/dist/terrain-heightfield.d.ts +53 -0
- package/dist/terrain-sample.d.ts +30 -0
- package/dist/terrain.d.ts +6 -1
- package/dist/testing.d.ts +6 -2
- package/dist/tile3d-metadata.d.ts +185 -0
- package/dist/units.d.ts +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/volumetrics-run.d.ts +13 -0
- package/dist/volumetrics-worker.d.ts +1 -0
- package/dist/volumetrics.d.ts +201 -0
- package/dist/widget-registry.d.ts +11 -0
- package/dist/widgets/ifc-clash.d.ts +18 -0
- package/dist/widgets/ifc.d.ts +41 -0
- package/dist/{zarr-OewK7k2K.js → zarr-CUzEX8fB.js} +79 -66
- package/docs/3d-assets.md +48 -0
- package/docs/live-data.md +9 -1
- package/docs/testing.md +4 -2
- package/llms.txt +10 -5
- package/onlymapjs.html-data.json +313 -2
- package/package.json +6 -2
- package/skills/onlymapjs/SKILL.md +14 -2
- package/skills/onlymapjs/references/react.md +2 -2
- package/skills/onlymapjs/references/syntax.md +314 -15
- 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)
|
|
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
|
-
/**
|
|
53
|
-
|
|
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
|
@@ -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
|
+
};
|