@bitruvius/geo-core 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WGS84 geocentric (ECEF) ↔ geodetic, plus the ECEF→ENU rotation used to rebase
3
+ * 3D-Tiles content near a scene anchor before handing it to a web map. Pure math
4
+ * (no DOM/GL); composes in f64. Reuses {@link fromBasis}/{@link fromTranslation}/
5
+ * {@link multiply} from `./mat4`; pairs with `./mercator`'s `placementMatrix` to
6
+ * complete ECEF → ENU(meters) → mercator.
7
+ */
8
+ import type { Mat4 } from './mat4';
9
+ /** A 3-component vector `[x, y, z]` (ECEF or ENU meters, per context). */
10
+ export type Vec3 = readonly [number, number, number];
11
+ /** A geodetic position on the WGS84 ellipsoid. */
12
+ export interface Geodetic {
13
+ /** Longitude in degrees. */ lng: number;
14
+ /** Latitude in degrees. */ lat: number;
15
+ /** Height in meters above the WGS84 ellipsoid. */ height: number;
16
+ }
17
+ /**
18
+ * Geodetic → WGS84 geocentric (ECEF) meters.
19
+ * @param lngDeg - Longitude in degrees.
20
+ * @param latDeg - Latitude in degrees.
21
+ * @param h - Height in meters above the WGS84 ellipsoid.
22
+ * @returns ECEF `[x, y, z]` in meters.
23
+ */
24
+ export declare function geodeticToEcef(lngDeg: number, latDeg: number, h: number): Vec3;
25
+ /**
26
+ * WGS84 geocentric (ECEF) → geodetic, via the Bowring closed-form solution.
27
+ * @param x - ECEF X in meters.
28
+ * @param y - ECEF Y in meters.
29
+ * @param z - ECEF Z in meters.
30
+ * @returns {@link Geodetic} (lng/lat degrees, height meters above the ellipsoid).
31
+ */
32
+ export declare function ecefToGeodetic(x: number, y: number, z: number): Geodetic;
33
+ /**
34
+ * Build the matrix that maps ECEF (meters) → local East-North-Up (meters) at `anchor`:
35
+ * `enu = R · (p − anchor)`, where R's rows are the East / North / Up basis vectors
36
+ * expressed in ECEF.
37
+ * @param anchor - The ENU origin in ECEF meters.
38
+ * @param lngDeg - Anchor longitude in degrees (orients the East/North axes).
39
+ * @param latDeg - Anchor latitude in degrees.
40
+ * @returns A column-major f64 {@link Mat4} (ECEF → ENU meters).
41
+ */
42
+ export declare function enuFromEcefMatrix(anchor: Vec3, lngDeg: number, latDeg: number): Mat4;
43
+ /**
44
+ * glTF y-up → 3D-Tiles z-up (rotation +90° about X): (x,y,z) → (x,−z,y). 3D Tiles
45
+ * applies this to glTF content before the tile transform; Esri's glb carries an
46
+ * inverse node matrix that cancels it (content stored z-up).
47
+ * @internal
48
+ */
49
+ export declare const Y_UP_TO_Z_UP: Mat4;
50
+ /**
51
+ * Compose the matrix that takes glb-content vertices into the shared local ENU (meters)
52
+ * frame: `enuFromEcef · tileTransform · (yUpToZUp · nodeMatrix)`. A renderer then
53
+ * applies the mercator placement once to all content already in this frame.
54
+ * @param enuFromEcef - ECEF → scene ENU meters (from {@link enuFromEcefMatrix}).
55
+ * @param tileTransform - The cumulative tile transform (tile-local → ECEF).
56
+ * @param nodeMatrix - The glTF node's world matrix (glb root → primitive node).
57
+ * @param upAxis - Up-axis of the glb CONTENT, i.e. 3D Tiles' `asset.gltfUpAxis`. `'y'` — the
58
+ * spec default, and the default here — applies {@link Y_UP_TO_Z_UP}; `'z'` means the content
59
+ * is ALREADY z-up, so the flip is skipped.
60
+ *
61
+ * Applying the flip to z-up content rotates it 90° about X **inside its tile-local frame**,
62
+ * about that frame's origin rather than about the geometry. For a tile whose local
63
+ * coordinates sit tens of km from that origin — normal for a `box` volume with a per-tile
64
+ * `transform` — the content is thrown tens of km sideways and tens of km below the
65
+ * ellipsoid, so it draws for a frame and is then buried. Publishers declaring `Z` are a
66
+ * minority (Esri's `3DTilesServer`), which is why this went unnoticed: the default-`Y`
67
+ * datasets were all correct.
68
+ * @returns A column-major f64 {@link Mat4} (glb-local → scene ENU meters).
69
+ */
70
+ export declare function tileToEnu(enuFromEcef: Mat4, tileTransform: Mat4, nodeMatrix: Mat4, upAxis?: 'y' | 'z'): Mat4;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Extract a mesh's **hard edges** — the lines a person would draw if asked to sketch the model.
3
+ *
4
+ * @remarks
5
+ * No I3S or 3D Tiles service ships edge geometry; a building arrives as a triangle soup and the
6
+ * edges have to be derived from it. Two kinds count:
7
+ *
8
+ * - **Boundary** edges, used by exactly one triangle. These are the silhouette of an open surface.
9
+ * - **Crease** edges, where the two triangles sharing the edge meet at more than `creaseAngleDeg`.
10
+ * These are the corners of a box, the eaves of a roof, the sill of a window.
11
+ *
12
+ * Everything else is tessellation — the diagonal splitting a flat wall into two triangles — and
13
+ * drawing it is the single most obvious way edge rendering looks wrong: the model reads as a
14
+ * wireframe rather than as a building.
15
+ *
16
+ * Two details matter more than they look:
17
+ *
18
+ * **Welding.** Uncompressed I3S geometry (and de-indexed Draco) repeats each position per triangle,
19
+ * so nothing shares vertices by index. Without welding by position, *every* edge looks like a
20
+ * boundary edge and the entire mesh draws as a wireframe. Positions are quantized to 0.1 mm and
21
+ * matched on that.
22
+ *
23
+ * **Welding stops at the feature.** Terraced houses share a coincident wall. Welded across
24
+ * features, that wall pairs up, reads as an interior edge and vanishes, taking the line between
25
+ * two adjacent buildings with it. So the feature id is part of the weld identity: each feature is
26
+ * outlined as if it were alone.
27
+ *
28
+ * Pure and allocation-conscious: this runs in the decode worker, once per tile, never per frame.
29
+ */
30
+ /** What to extract edges from. */
31
+ export interface HardEdgeInput {
32
+ /** 3·N vertex positions. */
33
+ readonly positions: Float32Array;
34
+ /** Triangle indices; omit for a non-indexed soup (positions are read three at a time). */
35
+ readonly indices?: Uint16Array | Uint32Array;
36
+ /** Per-VERTEX feature ordinal. When present, welding never crosses features. */
37
+ readonly featureIds?: Uint32Array;
38
+ /** Dihedral angle above which a shared edge counts as a crease. Default 25°. */
39
+ readonly creaseAngleDeg?: number;
40
+ /** Cap on emitted edges. Default 400 000. */
41
+ readonly maxEdges?: number;
42
+ }
43
+ /** Extracted edges as a flat line list. */
44
+ export interface HardEdges {
45
+ /** 6·count: `[x0,y0,z0, x1,y1,z1]` per edge, in the same frame as the input positions. */
46
+ readonly positions: Float32Array;
47
+ /** Per-EDGE feature ordinal; present iff the input carried `featureIds`. Lets a renderer hide
48
+ * an edge along with the feature it belongs to. */
49
+ readonly featureIds?: Uint32Array;
50
+ /**
51
+ * 2·count original VERTEX indices, one per endpoint.
52
+ *
53
+ * @remarks
54
+ * So a caller can gather any parallel per-vertex attribute the extractor knows nothing about.
55
+ * The one that matters today is the baked web-mercator offset: a planet-scale tile is placed
56
+ * from `mercatorOffset` rather than from ENU positions, so edges built only from ENU would
57
+ * float away from the buildings they outline.
58
+ */
59
+ readonly endpoints: Uint32Array;
60
+ readonly count: number;
61
+ /** True when {@link HardEdgeInput.maxEdges} cut the result short. */
62
+ readonly truncated: boolean;
63
+ }
64
+ /**
65
+ * Derive the hard edges of a triangle mesh.
66
+ *
67
+ * @param input - The geometry and extraction thresholds ({@link HardEdgeInput}).
68
+ * @returns The line list, or `null` when there is nothing to draw (empty, degenerate, or under
69
+ * one triangle) — so a caller can skip allocating and uploading an empty buffer.
70
+ */
71
+ export declare function extractHardEdges(input: HardEdgeInput): HardEdges | null;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Minimal column-major 4x4 matrix math, following the standard WebGL /
3
+ * gl-matrix convention: storage is column-major (`m[col*4 + row]`),
4
+ * `multiply(a, b)` returns `a*b`, and matrices act on column vectors on
5
+ * the right (`gl_Position = P * V * M * v`).
6
+ *
7
+ * Backed by **Float64Array**: mercator world coordinates (~0.5) and
8
+ * meter-scale offsets (~1e-8) must coexist in the same matrix, which f32
9
+ * cannot resolve (R3). CPU-side composition (placement, view-matrix
10
+ * recovery, covariance transforms) stays f64; only the final GPU uniform
11
+ * is downcast via `toFloat32` at the WebGL boundary.
12
+ *
13
+ * Independently implemented from textbook linear algebra — no third-party
14
+ * renderer code.
15
+ */
16
+ /** A column-major 4×4 matrix: a length-16 `Float64Array` (`m[col*4 + row]`). */
17
+ export type Mat4 = Float64Array;
18
+ /** A column-major 3×3 matrix: a length-9 `Float64Array` (`m[col*3 + row]`). */
19
+ export type Mat3 = Float64Array;
20
+ /** A 4-component vector `[x, y, z, w]`. */
21
+ export type Vec4 = readonly [number, number, number, number];
22
+ /** Downcast a CPU f64 matrix to a new `Float32Array` for a WebGL uniform.
23
+ * @param m - The f64 matrix. @returns A new length-16 `Float32Array`. */
24
+ export declare function toFloat32(m: Mat4): Float32Array;
25
+ /** Downcast into a caller-owned `Float32Array` (no allocation) — for per-frame uniform writes.
26
+ * @param m - The f64 source. @param out - The length-16 destination (written in place).
27
+ * @returns `out`. */
28
+ export declare function toFloat32Into(m: Mat4, out: Float32Array): Float32Array;
29
+ /** Copy any array-like (e.g. a viewer's matrix) into a new f64 {@link Mat4}.
30
+ * @param a - A length-16 column-major array-like. @returns A new {@link Mat4}. */
31
+ export declare function fromArrayLike(a: ArrayLike<number>): Mat4;
32
+ /** @returns A new 4×4 identity {@link Mat4}. */
33
+ export declare function identity(): Mat4;
34
+ /** @param a - The matrix to copy. @returns A new {@link Mat4} with the same values. */
35
+ export declare function clone(a: Mat4): Mat4;
36
+ /** Matrix product `out = a · b` (column-major; column vectors on the right).
37
+ * @param a - Left matrix. @param b - Right matrix.
38
+ * @param out - Optional destination (defaults to a new {@link Mat4}); may not alias `a`/`b`.
39
+ * @returns `out`. */
40
+ export declare function multiply(a: Mat4, b: Mat4, out?: Mat4): Mat4;
41
+ /** Transpose: `out = aᵀ`.
42
+ * @param a - The matrix. @param out - Optional destination. @returns `out`. */
43
+ export declare function transpose(a: Mat4, out?: Mat4): Mat4;
44
+ /** Extract the upper-left 3×3 (the linear part used for covariance/normals).
45
+ * @param a - The 4×4 matrix. @param out - Optional length-9 {@link Mat3} destination.
46
+ * @returns `out`. */
47
+ export declare function mat3FromMat4(a: Mat4, out?: Mat3): Mat3;
48
+ /** Transform a 4-vector: `out = m · v` (column vector).
49
+ * @param m - The matrix. @param v - The {@link Vec4}. @returns The transformed `[x,y,z,w]`. */
50
+ export declare function multiplyVec4(m: Mat4, v: Vec4): [number, number, number, number];
51
+ /** Build a translation matrix.
52
+ * @param x - X translation. @param y - Y translation. @param z - Z translation.
53
+ * @returns A new {@link Mat4} translating by `(x, y, z)`. */
54
+ export declare function fromTranslation(x: number, y: number, z: number): Mat4;
55
+ /** Build a (possibly non-uniform) scaling matrix.
56
+ * @param x - X scale. @param y - Y scale. @param z - Z scale.
57
+ * @returns A new {@link Mat4} scaling by `(x, y, z)`. */
58
+ export declare function fromScaling(x: number, y: number, z: number): Mat4;
59
+ /** Build a rotation about the +X axis (right-handed: counter-clockwise when +X
60
+ * points at the viewer; gl-matrix convention, column-major).
61
+ * @param rad - The rotation angle in radians.
62
+ * @param out - Optional destination (defaults to a new {@link Mat4}).
63
+ * @returns `out`. */
64
+ export declare function fromRotationX(rad: number, out?: Mat4): Mat4;
65
+ /** Build a rotation about the +Y axis (right-handed: counter-clockwise when +Y
66
+ * points at the viewer; gl-matrix convention, column-major).
67
+ * @param rad - The rotation angle in radians.
68
+ * @param out - Optional destination (defaults to a new {@link Mat4}).
69
+ * @returns `out`. */
70
+ export declare function fromRotationY(rad: number, out?: Mat4): Mat4;
71
+ /** Build a rotation about the +Z axis (right-handed: counter-clockwise when +Z
72
+ * points at the viewer; gl-matrix convention, column-major).
73
+ * @param rad - The rotation angle in radians.
74
+ * @param out - Optional destination (defaults to a new {@link Mat4}).
75
+ * @returns `out`. */
76
+ export declare function fromRotationZ(rad: number, out?: Mat4): Mat4;
77
+ /**
78
+ * Full general 4×4 inverse via cofactors: `out = a⁻¹`.
79
+ * @param a - The matrix to invert.
80
+ * @param out - Optional destination (defaults to a new {@link Mat4}).
81
+ * @returns `out`.
82
+ * @throws Error `'mat4.invert: singular matrix'` when the determinant is `0` or
83
+ * non-finite (a uniformly tiny determinant — e.g. the mercator meter placement,
84
+ * det ~1e-23 — is well-conditioned and inverts fine; only true singularities throw).
85
+ */
86
+ export declare function invert(a: Mat4, out?: Mat4): Mat4;
87
+ /** Apply an affine 4×4 to a point (`w = 1`).
88
+ * @param m - The matrix. @param p - The point `[x, y, z]`.
89
+ * @returns The transformed point `[x, y, z]`. */
90
+ export declare function transformPoint(m: Mat4, p: readonly [number, number, number]): [number, number, number];
91
+ /** Apply only the 3×3 linear part of an affine 4×4 to a direction (no translation).
92
+ * @param m - The matrix. @param d - The direction `[x, y, z]`.
93
+ * @returns The rotated/scaled direction `[x, y, z]` (not renormalized). */
94
+ export declare function transformDir(m: Mat4, d: readonly [number, number, number]): [number, number, number];
95
+ /**
96
+ * Orthographic projection (column-major), mapping the view-space box
97
+ * x∈[left,right], y∈[bottom,top], z∈[-far,-near] (right-handed; the view looks down −Z, so
98
+ * `near`/`far` are positive distances) into NDC [-1,1]³. Used to fit a directional (sun) light's
99
+ * frustum to a scene. Matches the gl-matrix convention.
100
+ *
101
+ * @param left - View-space left bound.
102
+ * @param right - View-space right bound.
103
+ * @param bottom - View-space bottom bound.
104
+ * @param top - View-space top bound.
105
+ * @param near - Near plane distance (positive, in front of the camera).
106
+ * @param far - Far plane distance (positive).
107
+ * @param out - Optional destination (defaults to a new {@link Mat4}).
108
+ * @returns `out`.
109
+ */
110
+ export declare function orthoInto(left: number, right: number, bottom: number, top: number, near: number, far: number, out?: Mat4): Mat4;
111
+ /** Build a column-major 4×4 from three basis columns + a translation column (row 3 = `[0,0,0,1]`).
112
+ * @param col0 - First basis column. @param col1 - Second. @param col2 - Third.
113
+ * @param translation - The translation column.
114
+ * @param out - Optional destination. @returns `out`. */
115
+ export declare function fromBasis(col0: readonly [number, number, number], col1: readonly [number, number, number], col2: readonly [number, number, number], translation: readonly [number, number, number], out?: Mat4): Mat4;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Geodesic measurement math — distances, bearings, interpolation, and polygon
3
+ * area/perimeter over the WGS84 ellipsoid. Everything works in local ENU metres
4
+ * via the exact ECEF↔ENU primitives in `./ecef` (chord-vs-arc error is d³/24R² —
5
+ * ~2 mm at 10 km, ~3 m at 100 km — matching the SDK's city-scale precision
6
+ * philosophy; the bound is documented per function). Pure f64 math, no DOM/GL.
7
+ */
8
+ import { type Geodetic } from './ecef';
9
+ /** A measured position: lng/lat in degrees, `height` in metres (default 0 where optional). */
10
+ export interface MeasurePoint {
11
+ lng: number;
12
+ lat: number;
13
+ height?: number;
14
+ }
15
+ /**
16
+ * The local ENU offset (metres) of `b` relative to `a` — the workhorse the other
17
+ * measures build on: `[east, north, up]` in the tangent frame at `a`.
18
+ * @param a - The origin point.
19
+ * @param b - The target point.
20
+ * @returns `[east, north, up]` metres.
21
+ */
22
+ export declare function enuOffset(a: MeasurePoint, b: MeasurePoint): [number, number, number];
23
+ /**
24
+ * Direct (slope) 3D distance between two points in metres — the straight chord
25
+ * through space, including the height difference.
26
+ */
27
+ export declare function distance3d(a: MeasurePoint, b: MeasurePoint): number;
28
+ /**
29
+ * Horizontal (map-plane) distance in metres — the ground component, heights
30
+ * ignored. Chord error vs the true geodesic arc is `d³/24R²` (~2 mm at 10 km,
31
+ * ~3 m at 100 km).
32
+ */
33
+ export declare function horizontalDistance(a: MeasurePoint, b: MeasurePoint): number;
34
+ /** Signed height difference `b.height − a.height` in metres. */
35
+ export declare function verticalDelta(a: MeasurePoint, b: MeasurePoint): number;
36
+ /**
37
+ * Initial compass bearing from `a` to `b`, degrees clockwise from north in `[0, 360)`.
38
+ */
39
+ export declare function bearingDeg(a: MeasurePoint, b: MeasurePoint): number;
40
+ /**
41
+ * Great-circle distance on the mean sphere (metres) — the long-range cross-check
42
+ * and the axis measure for long elevation profiles. Haversine on {@link EARTH_RADIUS}.
43
+ */
44
+ export declare function haversineDistance(a: MeasurePoint, b: MeasurePoint): number;
45
+ /**
46
+ * Interpolate between two points at fraction `t` (0 = `a`, 1 = `b`): positions
47
+ * lerp in ECEF then renormalize to the surface direction (antimeridian- and
48
+ * pole-safe); heights lerp linearly and independently.
49
+ * @param a - Start point. @param b - End point. @param t - Fraction in `[0, 1]`.
50
+ * @returns The interpolated {@link Geodetic} (lng/lat degrees, height metres).
51
+ */
52
+ export declare function interpolatePosition(a: MeasurePoint, b: MeasurePoint, t: number): Geodetic;
53
+ /**
54
+ * Total polyline length in metres.
55
+ * @param points - The vertices (≥ 2 for a nonzero result).
56
+ * @param mode - `'3d'` sums {@link distance3d} per segment; `'horizontal'` sums
57
+ * {@link horizontalDistance}.
58
+ */
59
+ export declare function polylineLength(points: readonly MeasurePoint[], mode: '3d' | 'horizontal'): number;
60
+ /**
61
+ * Planar polygon area in square metres — the ring is projected into the ENU
62
+ * tangent plane at its centroid (heights ignored) and measured with the shoelace
63
+ * formula. Exact to city scale; the tangent-plane error grows with extent and is
64
+ * negligible below ~50 km. Vertex order (CW/CCW) doesn't matter (absolute value);
65
+ * self-intersecting rings are not supported.
66
+ * @param ring - The polygon vertices (unclosed; ≥ 3 for a nonzero result).
67
+ */
68
+ export declare function polygonAreaM2(ring: readonly MeasurePoint[]): number;
69
+ /**
70
+ * A raster grid of cell CENTRES (lng/lat) inside a polygon — the sampling stations for an
71
+ * area-integral measure (e.g. volume = Σ over cells of fill × cellArea). Cells are square in a
72
+ * local planar frame at the ring's mean latitude; the long bbox axis gets ≤128 cells (≥0.25 m),
73
+ * and the total is capped at `maxCells` (cell size grows to fit). Pure — the caller samples a
74
+ * surface/DEM at the returned centres and integrates. City-scale accuracy (same tangent-plane
75
+ * philosophy as {@link polygonAreaM2}).
76
+ *
77
+ * The `grid` descriptor lets a caller scatter the sampled values back into a full raster (for a
78
+ * heatmap / contour overlay): `centers[k]` lives in row-major cell `cellIndex[k]` (`= jy·nx + ix`)
79
+ * of an `nx × ny` grid whose cell `(ix, jy)` centre is `[minLng+(ix+0.5)·dLng, minLat+(jy+0.5)·dLat]`
80
+ * (`jy` counts NORTH from the bbox's south edge). Cells outside the ring have no `centers` entry.
81
+ * @param ring - Polygon vertices (unclosed; ≥ 3).
82
+ * @param maxCells - Hard cap on the returned cell count (default 8192).
83
+ * @returns `centers` (cell centres strictly inside the ring), the uniform per-cell area (m²), and the
84
+ * `grid` descriptor (`nx`, `ny`, SW origin `minLng`/`minLat`, per-cell steps `dLng`/`dLat`, and the
85
+ * `cellIndex` of each centre).
86
+ */
87
+ export declare function volumeGridCells(ring: readonly MeasurePoint[], maxCells?: number): {
88
+ centers: {
89
+ lng: number;
90
+ lat: number;
91
+ }[];
92
+ cellAreaM2: number;
93
+ grid: {
94
+ nx: number;
95
+ ny: number;
96
+ minLng: number;
97
+ minLat: number;
98
+ dLng: number;
99
+ dLat: number;
100
+ cellIndex: number[];
101
+ };
102
+ };
103
+ /**
104
+ * Marching-squares iso-lines of a scalar raster, as independent line SEGMENTS in fractional grid
105
+ * coordinates. Grid sample `field[j·nx + i]` sits at continuous coordinate `(i, j)`; a returned
106
+ * point `[fi, fj]` maps to lng/lat as `[minLng+(fi+0.5)·dLng, minLat+(fj+0.5)·dLat]` (matching
107
+ * {@link volumeGridCells}'s cell-centre convention). Cells touching a `NaN` sample are skipped, so
108
+ * contours never cross the polygon boundary. Pure & allocation-light (no stitching — abutting
109
+ * segments render as a continuous line).
110
+ * @param field - Row-major `nx × ny` samples (`NaN` = no data).
111
+ * @param nx - Columns.
112
+ * @param ny - Rows.
113
+ * @param level - Iso value.
114
+ * @returns Segments `[[fi0, fj0], [fi1, fj1]]` in fractional grid coordinates.
115
+ */
116
+ export declare function contourSegments(field: ReadonlyArray<number> | Float32Array, nx: number, ny: number, level: number): [[number, number], [number, number]][];
117
+ /**
118
+ * Choose a "nice" contour interval (a 1-2-5 multiple of `baseIntervalM`) and the iso levels spanning
119
+ * `(0, maxFillM]`, targeting at most `maxLines` lines. Base interval is the unit's step — 1 m
120
+ * (metric) or 0.3048 m (1 ft, imperial) — widened to 2/5/10/… for tall piles.
121
+ * @param maxFillM - Peak value of the field (metres).
122
+ * @param baseIntervalM - The unit's base contour step (metres).
123
+ * @param maxLines - Cap on the number of levels (default 12).
124
+ * @returns The chosen `intervalM` and the ascending `levels` (metres).
125
+ */
126
+ export declare function contourLevels(maxFillM: number, baseIntervalM: number, maxLines?: number): {
127
+ intervalM: number;
128
+ levels: number[];
129
+ };
130
+ /**
131
+ * Polygon perimeter in metres (horizontal, ring closed automatically).
132
+ * @param ring - The polygon vertices (unclosed).
133
+ */
134
+ export declare function polygonPerimeterM(ring: readonly MeasurePoint[]): number;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Standard Web Mercator conversions, matching MapLibre's
3
+ * `MercatorCoordinate` exactly so this core math is interchangeable
4
+ * with the viewer's own conversion. Implemented from the public Web
5
+ * Mercator definition (not from any renderer library).
6
+ *
7
+ * Mercator unit square: x,y in [0,1] over the world; z is altitude in
8
+ * the same units (meters / circumferenceAtLatitude). A scene authored
9
+ * in local meters is placed by `placementMatrix`.
10
+ */
11
+ import { type Mat4 } from './mat4';
12
+ /** Mean Earth radius (m) used for the mercator circumference. @internal */
13
+ export declare const EARTH_RADIUS = 6371008.8;
14
+ /** Earth circumference (m) at the equator (`2π·EARTH_RADIUS`). @internal */
15
+ export declare const EARTH_CIRCUMFERENCE: number;
16
+ /** A geographic anchor placing a local-meters scene in the world. */
17
+ export interface GeoAnchor {
18
+ /** Longitude in degrees. */
19
+ readonly lng: number;
20
+ /** Latitude in degrees. */
21
+ readonly lat: number;
22
+ /** Height in meters above the WGS84 ellipsoid. */
23
+ readonly altitude: number;
24
+ }
25
+ /** A point in MapLibre-compatible web-mercator: `x`,`y` in `[0,1]` over the world,
26
+ * `z` altitude in the same normalized units (meters / circumferenceAtLatitude). */
27
+ export interface MercatorXYZ {
28
+ /** Normalized mercator X in `[0,1]` (west→east). */
29
+ readonly x: number;
30
+ /** Normalized mercator Y in `[0,1]` (north→south). */
31
+ readonly y: number;
32
+ /** Normalized mercator Z (altitude in mercator units). */
33
+ readonly z: number;
34
+ }
35
+ /** World circumference (m) at a latitude (shrinks toward the poles by `cos lat`).
36
+ * @param lat - Latitude in degrees.
37
+ * @returns Circumference in meters. */
38
+ export declare function circumferenceAtLatitude(lat: number): number;
39
+ /** Longitude (degrees) → normalized mercator X in `[0,1]`. */
40
+ export declare function mercatorXfromLng(lng: number): number;
41
+ /** Latitude (degrees) → normalized mercator Y in `[0,1]`. */
42
+ export declare function mercatorYfromLat(lat: number): number;
43
+ /** Altitude (m above ellipsoid) → normalized mercator Z at a latitude.
44
+ * @param altitude - Height in meters. @param lat - Latitude in degrees. */
45
+ export declare function mercatorZfromAltitude(altitude: number, lat: number): number;
46
+ /** Normalized mercator X in `[0,1]` → longitude (degrees). Inverse of {@link mercatorXfromLng}. */
47
+ export declare function lngFromMercatorX(x: number): number;
48
+ /** Normalized mercator Y in `[0,1]` → latitude (degrees). Inverse of {@link mercatorYfromLat}
49
+ * (the Gudermannian). */
50
+ export declare function latFromMercatorY(y: number): number;
51
+ /** Normalized mercator Z → altitude in meters at a latitude. Inverse of {@link mercatorZfromAltitude}.
52
+ * @param z - Normalized mercator Z. @param lat - Latitude in degrees. */
53
+ export declare function altitudeFromMercatorZ(z: number, lat: number): number;
54
+ /** Mercator units per meter at a latitude (latitude-dependent — never a constant); the
55
+ * linear scale that places a local-meters scene into mercator.
56
+ * @param lat - Latitude in degrees. @returns Mercator units per meter. */
57
+ export declare function meterInMercatorCoordinateUnits(lat: number): number;
58
+ /** Convert lng/lat (degrees) + altitude (meters) to {@link MercatorXYZ}.
59
+ * @param lngLat - `{ lng, lat }` in degrees.
60
+ * @param altitude - Height in meters above the ellipsoid (default 0).
61
+ * @returns The normalized web-mercator coordinate. */
62
+ export declare function mercatorFromLngLat(lngLat: {
63
+ lng: number;
64
+ lat: number;
65
+ }, altitude?: number): MercatorXYZ;
66
+ /** Options for {@link placementMatrix}. */
67
+ export interface PlacementOptions {
68
+ /**
69
+ * Optional basis rotation (column-major {@link Mat4}) applied BEFORE the meter
70
+ * scaling, to reconcile the asset's axis convention (e.g. Y-up) with the mercator
71
+ * frame (x east, y south, z up). Defaults to identity — i.e. the local frame is
72
+ * already x-east / y-south / z-up in meters.
73
+ */
74
+ readonly basis?: Mat4;
75
+ }
76
+ /**
77
+ * Build the model matrix that maps a local-meters scene anchored at `anchor` into
78
+ * web-mercator world space: `T(merc) · S(meter) · [basis]`.
79
+ *
80
+ * @remarks
81
+ * The meter scaling is latitude-dependent (see {@link meterInMercatorCoordinateUnits});
82
+ * the same linear part is what the splat covariance transform must include so ellipsoids
83
+ * scale with the map.
84
+ *
85
+ * @param anchor - Where the scene is anchored ({@link GeoAnchor}: lng/lat degrees, altitude meters).
86
+ * @param opts - Optional {@link PlacementOptions} (axis `basis`).
87
+ * @returns A column-major f64 {@link Mat4} (local meters → mercator).
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const model = placementMatrix({ lng: -73.99, lat: 40.75, altitude: 0 });
92
+ * gl.uniformMatrix4fv(uModel, false, toFloat32(model));
93
+ * ```
94
+ */
95
+ export declare function placementMatrix(anchor: GeoAnchor, opts?: PlacementOptions): Mat4;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Friendly model placement — the user-facing vocabulary that replaces raw
3
+ * `Mat4` basis math for placing a single local-meters model (gaussian splat,
4
+ * point cloud, …) on the map.
5
+ *
6
+ * A capture's coordinate frame is a format/pipeline property (SPZ-style
7
+ * assets are typically Y-up, LAS/BVC are Z-up), its heading/scale are
8
+ * per-capture tuning, and MapLibre's mercator frame quirks (Y grows SOUTH,
9
+ * so the frame is left-handed) are an SDK implementation detail. This module
10
+ * keeps each concern in its place: `ModelPlacement` exposes only
11
+ * degrees/meters/presets, and `placementBasis` composes the correct
12
+ * column-major basis — including the internal mercator Y-flip the user must
13
+ * never see.
14
+ */
15
+ import { type Mat4 } from './mat4';
16
+ import { type GeoAnchor } from './mercator';
17
+ import type { LocalBounds } from '../dequant/tile-bounds';
18
+ export type { LocalBounds } from '../dequant/tile-bounds';
19
+ /**
20
+ * Which SOURCE axis points up in the asset's own frame — a format/pipeline
21
+ * property, never user matrix math. `'z'` = already Z-up (LAS/BVC native);
22
+ * `'y'` = glTF/SPZ-style Y-up; `'-y'` = COLMAP/3DGS Y-down; `'x' | '-x' |
23
+ * '-z'` cover exotic captures.
24
+ */
25
+ export type UpAxis = 'z' | 'y' | '-y' | 'x' | '-x' | '-z';
26
+ /**
27
+ * Which local point of the model sits exactly at the `GeoAnchor` — the
28
+ * vertical seat is an explicit top/middle/bottom choice:
29
+ * `'center'` (default) = the model's center at the anchor altitude;
30
+ * `'bottom-center'` = base at the anchor altitude ("sits on the ground");
31
+ * `'top-center'` = top at the anchor altitude (hang from a height);
32
+ * `'origin'` = the capture's raw local origin (legacy behavior); a tuple =
33
+ * an explicit source-space point in meters (deterministic — skips bounds
34
+ * entirely; useful for pinning production placements).
35
+ *
36
+ * @remarks
37
+ * When the bounds carry robust percentiles (`LocalBounds.p05/p50/p95` — the
38
+ * layers always compute them), anchoring is OUTLIER-PROOF: `'center'` is the
39
+ * per-axis median and bottom/top are the 5th/95th percentiles along the up
40
+ * axis. Gaussian-splat captures surround their subject with a huge sparse
41
+ * halo of background splats (±200 m around an 8 m subject is typical), so
42
+ * raw min/max anchoring would seat the halo, not the subject.
43
+ */
44
+ export type AnchorPoint = 'center' | 'bottom-center' | 'top-center' | 'origin' | readonly [number, number, number];
45
+ /**
46
+ * Friendly per-capture placement — everything a user tunes to seat a model
47
+ * on the map, with no matrices. Shared by every single-model layer
48
+ * (`BvcMapLibreLayer`, `SpzMapLibreLayer`, …).
49
+ */
50
+ export interface ModelPlacement {
51
+ /** Up axis of the source asset (`UpAxis`). Default `'z'` (layers may document a format-specific default). */
52
+ readonly upAxis?: UpAxis;
53
+ /** Heading in degrees clockwise from north viewed from above (0 = the model's local +y faces north).
54
+ * Same semantics as `GeodeticInstance.heading`. Default 0. */
55
+ readonly heading?: number;
56
+ /** Pitch in degrees about the model's local x axis (east at heading 0) — positive tips the
57
+ * +y (north-facing) side up. A leveling aid for off-level captures. Default 0. */
58
+ readonly pitch?: number;
59
+ /** Roll in degrees about the model's local y axis (north at heading 0) — positive lowers the
60
+ * +x (east-facing) side. A leveling aid for off-level captures. Default 0. */
61
+ readonly roll?: number;
62
+ /** Unitless scale: uniform number or per-axis `[x, y, z]` in the UP-FIXED frame (z = height,
63
+ * regardless of `ModelPlacement.upAxis`). Same shape as `GeodeticInstance.scale`. Default 1. */
64
+ readonly scale?: number | readonly [number, number, number];
65
+ /** Which local point sits at the anchor (`AnchorPoint`: top/middle/bottom,
66
+ * origin, or an explicit point). Default `'center'`. */
67
+ readonly anchorPoint?: AnchorPoint;
68
+ }
69
+ /**
70
+ * `diag(1, −1, 1)` — right-handed local ENU-meters → MapLibre's left-handed
71
+ * mercator frame (mercator Y grows SOUTH). The ONLY handedness flip in the
72
+ * placement composition (det −1); applied internally by
73
+ * `placementBasis`, never user-visible.
74
+ */
75
+ export declare const MERCATOR_Y_FLIP: Mat4;
76
+ /**
77
+ * Pure rotations (det +1) taking each source frame to x-east / y-north /
78
+ * z-up. Exact signed permutations (literals, not trig — no 1e-17 residue);
79
+ * `'y'` equals the glTF/SPZ `Y_UP_TO_Z_UP` rotation (+90° about X).
80
+ */
81
+ export declare const UP_AXIS_BASIS: Readonly<Record<UpAxis, Mat4>>;
82
+ /**
83
+ * The signed unit vector of an `UpAxis` in the model's SOURCE frame — the
84
+ * direction "height" runs along before the up-axis fix (e.g. `'y'` →
85
+ * `[0, 1, 0]`, `'-y'` → `[0, -1, 0]`).
86
+ *
87
+ * @param upAxis - The source up axis.
88
+ * @returns The signed unit source-frame up vector.
89
+ */
90
+ export declare function upAxisVector(upAxis: UpAxis): readonly [number, number, number];
91
+ /**
92
+ * Resolve the SOURCE-space point that `placementBasis` pins to the anchor.
93
+ * `'bottom-center'`/`'top-center'` are sign-aware along the source axis that
94
+ * maps to up (e.g. `'-y'` → bottom = the source MAX y).
95
+ *
96
+ * @remarks
97
+ * Robust percentiles win when the bounds carry them (`p50` for centers, the
98
+ * up-axis `p05`/`p95` for bottom/top) — see the `AnchorPoint` remarks on
99
+ * outlier halos. Plain AABBs fall back to midpoint/min/max.
100
+ *
101
+ * @param bounds - The model's bounds in source coordinates (`LocalBounds`).
102
+ * @param upAxis - The source up axis (decides which axis top/bottom runs along).
103
+ * @param anchorPoint - The requested `AnchorPoint`.
104
+ * @returns The source-space point (meters) to place at the anchor.
105
+ */
106
+ export declare function anchorPointSource(bounds: LocalBounds, upAxis: UpAxis, anchorPoint: AnchorPoint): [number, number, number];
107
+ /**
108
+ * Build the local→mercator basis from a friendly `ModelPlacement`:
109
+ *
110
+ * `B = MERCATOR_Y_FLIP · Rz(−heading) · Rx(pitch) · Ry(roll) · S(scale) · U(upAxis) · T(−anchorSource)`
111
+ *
112
+ * @remarks
113
+ * The innermost recenter translation is in SOURCE coordinates, so `B ·
114
+ * anchorSource = 0` exactly — heading/pitch/roll/scale pivot about the
115
+ * anchored point by construction. With every field defaulted and
116
+ * `anchorSource` at the origin, the result is byte-identical to the legacy
117
+ * default basis `diag(1, −1, 1)`.
118
+ *
119
+ * @param placement - The friendly placement fields (all optional; see `ModelPlacement`).
120
+ * @param anchorSource - The source-space point pinned to the anchor (from
121
+ * `anchorPointSource`); default `[0, 0, 0]`.
122
+ * @returns A column-major f64 `Mat4` basis for `placementMatrix`.
123
+ */
124
+ export declare function placementBasis(placement?: ModelPlacement, anchorSource?: readonly [number, number, number]): Mat4;
125
+ /**
126
+ * The one-call model matrix every single-model layer uses:
127
+ * `T(merc) · S(meterPerMercUnit) · placementBasis(placement, anchorSource)`.
128
+ *
129
+ * @remarks
130
+ * `bounds` are only needed for the `'center'` / `'bottom-center'` anchor
131
+ * modes; when they are not yet known (tiles still streaming) pass `null` and
132
+ * rebuild once they are — the fallback anchors the source origin, exactly the
133
+ * legacy behavior. `'origin'` and explicit-tuple anchor points never consult
134
+ * bounds.
135
+ *
136
+ * @param anchor - Where the model sits (`GeoAnchor`: lng/lat degrees, altitude meters).
137
+ * @param placement - The friendly placement (`ModelPlacement`); default all-defaults.
138
+ * @param bounds - The model's source-space AABB, or `null`/omitted before it is known.
139
+ * @returns A column-major f64 `Mat4` (source coordinates → mercator).
140
+ */
141
+ export declare function modelPlacementMatrix(anchor: GeoAnchor, placement?: ModelPlacement, bounds?: LocalBounds | null): Mat4;