@navaramap/three-api 0.0.7 → 0.1.0
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/LICENSE +1 -1
- package/README.md +30 -1
- package/dist/ellipsoidGeodesic.d.ts +12 -10
- package/dist/frames.d.ts +69 -0
- package/dist/frames.test.d.ts +1 -0
- package/dist/index.d.ts +39 -3
- package/dist/index.js +106 -82
- package/dist/index.test.d.ts +1 -0
- package/dist/index.umd.cjs +1 -1
- package/package.json +3 -3
- package/src/ellipsoidGeodesic.ts +32 -20
- package/src/frames.test.ts +154 -0
- package/src/frames.ts +98 -0
- package/src/index.test.ts +137 -0
- package/src/index.ts +68 -6
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
# @navaramap/three-api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Standalone geodetic and GIS math for Three.js, powered by Navara's WASM core. It covers geodetic ⇔ ECEF conversion, WGS84 ellipsoid constants and surface normals, local tangent frames (ENU/NED/NUE/NWU/WUN and heading/pitch/roll placement), screen ⇔ world projection and pick rays, ray/plane intersection, ellipsoid geodesics, and RTE (relative-to-eye) encoding for jitter-free rendering far from the origin. Everything is integrated with the Three.js type system (`Vector3`, `Matrix4`, cameras).
|
|
4
|
+
|
|
5
|
+
Unlike the rest of the Navara stack, this package is usable without the map engine: it is the standalone form of Navara's API tier, for pure geometry computation in any Three.js app.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Initialize the WASM module once before calling anything else (when using `@navaramap/three`, `view.init()` does this for you):
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { initNavaraApi, geodeticToVector3 } from "@navaramap/three-api";
|
|
13
|
+
|
|
14
|
+
await initNavaraApi();
|
|
15
|
+
|
|
16
|
+
// Geodetic coordinates (degrees, meters) to ECEF Cartesian.
|
|
17
|
+
const position = geodeticToVector3({
|
|
18
|
+
lng: 139.77,
|
|
19
|
+
lat: 35.68,
|
|
20
|
+
height: 0,
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
All of these utilities are also re-exported by `@navaramap/three`, so applications using the map engine don't need a direct dependency on this package.
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
|
|
28
|
+
Every function is documented at https://navara-docs.reearth.workers.dev/.
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT OR Apache-2.0
|
|
@@ -3,11 +3,13 @@ import { LatLngHeight } from '@navaramap/core';
|
|
|
3
3
|
* Wrapper class for geodesic calculations on an ellipsoid surface.
|
|
4
4
|
* Precomputes common variables for optimization when instantiated.
|
|
5
5
|
*
|
|
6
|
+
* All latitudes/longitudes and headings are in **degrees**.
|
|
7
|
+
*
|
|
6
8
|
* @example
|
|
7
9
|
* ```typescript
|
|
8
10
|
* const geodesic = new EllipsoidGeodesic(
|
|
9
|
-
* { lat:
|
|
10
|
-
* { lat:
|
|
11
|
+
* { lat: 35.6812, lng: 139.7671, height: 0 },
|
|
12
|
+
* { lat: 34.7025, lng: 135.4959, height: 0 }
|
|
11
13
|
* );
|
|
12
14
|
*
|
|
13
15
|
* console.log(geodesic.distance);
|
|
@@ -21,8 +23,8 @@ export declare class EllipsoidGeodesic {
|
|
|
21
23
|
private _raw;
|
|
22
24
|
/**
|
|
23
25
|
* Create a new geodesic between two points on the ellipsoid.
|
|
24
|
-
* @param start - Start point in geodetic coordinates (lat/lng in
|
|
25
|
-
* @param end - End point in geodetic coordinates (lat/lng in
|
|
26
|
+
* @param start - Start point in geodetic coordinates (lat/lng in degrees)
|
|
27
|
+
* @param end - End point in geodetic coordinates (lat/lng in degrees)
|
|
26
28
|
*/
|
|
27
29
|
constructor(start: LatLngHeight, end: LatLngHeight);
|
|
28
30
|
/** Distance in meters between start and end points */
|
|
@@ -34,24 +36,24 @@ export declare class EllipsoidGeodesic {
|
|
|
34
36
|
* unreliable solution.
|
|
35
37
|
*/
|
|
36
38
|
get converged(): boolean;
|
|
37
|
-
/** Heading at the start point in
|
|
39
|
+
/** Heading at the start point in degrees */
|
|
38
40
|
get startHeading(): number;
|
|
39
|
-
/** Heading at the end point in
|
|
41
|
+
/** Heading at the end point in degrees */
|
|
40
42
|
get endHeading(): number;
|
|
41
|
-
/** Start point in geodetic coordinates */
|
|
43
|
+
/** Start point in geodetic coordinates (lat/lng in degrees) */
|
|
42
44
|
get start(): LatLngHeight;
|
|
43
|
-
/** End point in geodetic coordinates */
|
|
45
|
+
/** End point in geodetic coordinates (lat/lng in degrees) */
|
|
44
46
|
get end(): LatLngHeight;
|
|
45
47
|
/**
|
|
46
48
|
* Interpolate points along the geodesic path.
|
|
47
49
|
* @param granularity - Distance between interpolated points in meters (optional)
|
|
48
|
-
* @returns Array of interpolated points in geodetic coordinates
|
|
50
|
+
* @returns Array of interpolated points in geodetic coordinates (lat/lng in degrees)
|
|
49
51
|
*/
|
|
50
52
|
interpolatePoints(granularity?: number): LatLngHeight[];
|
|
51
53
|
/**
|
|
52
54
|
* Interpolate a point at a specific distance along the geodesic path.
|
|
53
55
|
* @param distance - Distance from start point in meters
|
|
54
|
-
* @returns Interpolated point in geodetic coordinates
|
|
56
|
+
* @returns Interpolated point in geodetic coordinates (lat/lng in degrees)
|
|
55
57
|
*/
|
|
56
58
|
interpolateDistance(distance: number): LatLngHeight;
|
|
57
59
|
/**
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { XYZ } from '@navaramap/core';
|
|
2
|
+
import { Matrix4 } from 'three';
|
|
3
|
+
/**
|
|
4
|
+
* High-level geographic placement of a mesh object.
|
|
5
|
+
*
|
|
6
|
+
* All angles are in **degrees** and `height` is in metres, matching
|
|
7
|
+
* `ThreeView.setCamera`. The asset is oriented in a West-Up-North tangent
|
|
8
|
+
* frame, which matches glTF's own convention (front `+Z`, up `+Y`,
|
|
9
|
+
* right `-X`) on all three axes — so an unmodified glTF asset needs no
|
|
10
|
+
* up-axis correction.
|
|
11
|
+
*/
|
|
12
|
+
export type GeodeticPlacement = {
|
|
13
|
+
/** Longitude in degrees. */
|
|
14
|
+
lng: number;
|
|
15
|
+
/** Latitude in degrees. */
|
|
16
|
+
lat: number;
|
|
17
|
+
/**
|
|
18
|
+
* Metres above the ellipsoid, or above terrain when `heightReference` is
|
|
19
|
+
* `"terrain"`. Defaults to 0.
|
|
20
|
+
*/
|
|
21
|
+
height?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Degrees clockwise from north that the asset's front (glTF `+Z`) faces.
|
|
24
|
+
* Same convention as `setCamera`'s heading. Defaults to 0.
|
|
25
|
+
*/
|
|
26
|
+
heading?: number;
|
|
27
|
+
/** Degrees, nose up positive. Defaults to 0. */
|
|
28
|
+
pitch?: number;
|
|
29
|
+
/** Degrees, right wing down positive. Defaults to 0. */
|
|
30
|
+
roll?: number;
|
|
31
|
+
/** Uniform or per-axis scale of the frame. Defaults to 1. */
|
|
32
|
+
scale?: number | XYZ;
|
|
33
|
+
/** How `height` is interpreted. Defaults to `"ellipsoid"`. */
|
|
34
|
+
heightReference?: "ellipsoid" | "terrain";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Basis change from the NUE frame `(north, up, east)` to the WUN frame
|
|
38
|
+
* `(west, up, north)`.
|
|
39
|
+
*
|
|
40
|
+
* With `Ry(+90°)` columns `(0,0,-1), (0,1,0), (1,0,0)`, the product
|
|
41
|
+
* `NUE · Ry(+90°)` has columns `(-east, up, north)`, which is WUN. WUN is the
|
|
42
|
+
* unique right-handed, Y-up tangent frame whose `+Z` is north, which is why it
|
|
43
|
+
* agrees with glTF on every axis.
|
|
44
|
+
*
|
|
45
|
+
* Returns a fresh matrix on every call, so callers cannot corrupt a shared
|
|
46
|
+
* instance.
|
|
47
|
+
*
|
|
48
|
+
* @returns A new `Matrix4` performing the NUE-to-WUN basis change.
|
|
49
|
+
*/
|
|
50
|
+
export declare function nueToWunBasis(): Matrix4;
|
|
51
|
+
/**
|
|
52
|
+
* Composes a tangent frame with heading, pitch, roll and scale.
|
|
53
|
+
*
|
|
54
|
+
* Pure: takes the frame as a parameter and uses three.js `MathUtils.degToRad`,
|
|
55
|
+
* so it touches no WASM and is directly unit-testable.
|
|
56
|
+
*
|
|
57
|
+
* `Ry(-heading) · Rx(-pitch) · Rz(+roll)` is exactly three.js Euler order
|
|
58
|
+
* `"YXZ"`, the standard intrinsic yaw -> pitch -> roll order. Signs follow from
|
|
59
|
+
* the glTF axes in a WUN frame: positive heading swings `+Z` toward `-X`
|
|
60
|
+
* (east), positive pitch lifts `+Z` toward `+Y`, and positive roll about `+Z`
|
|
61
|
+
* drives the `-X` wing toward `-Y`.
|
|
62
|
+
*
|
|
63
|
+
* @param frame - Tangent frame to compose onto, normally from
|
|
64
|
+
* `westUpNorthToFixedFrame`
|
|
65
|
+
* @param p - Heading, pitch, roll (degrees) and scale; each defaults to
|
|
66
|
+
* no-op
|
|
67
|
+
* @returns `frame · R(heading, pitch, roll) · S(scale)`
|
|
68
|
+
*/
|
|
69
|
+
export declare function composeHeadingPitchRoll(frame: Matrix4, p: Pick<GeodeticPlacement, "heading" | "pitch" | "roll" | "scale">): Matrix4;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LatLngHeight, Window as WindowObject } from '@navaramap/core';
|
|
2
2
|
import { Vector3, Vector2, Matrix4, PerspectiveCamera } from 'three';
|
|
3
|
+
import { GeodeticPlacement } from './frames';
|
|
3
4
|
export type { LatLngHeight, LatLng } from '@navaramap/core';
|
|
4
5
|
export type { Window, Transform, CameraFrustum, Plane, Ray, Vec2, Vec3, } from '@navaramap/core';
|
|
6
|
+
export type { GeodeticPlacement } from './frames';
|
|
5
7
|
export * from './intersection';
|
|
6
8
|
export * from './rte';
|
|
7
9
|
export * from './ellipsoidGeodesic';
|
|
@@ -11,14 +13,14 @@ export * from './ellipsoidGeodesic';
|
|
|
11
13
|
export declare function initNavaraApi(): Promise<void>;
|
|
12
14
|
/**
|
|
13
15
|
* Converts geodetic coordinates (longitude, latitude, height) to a Cartesian Vector3 in ECEF coordinates.
|
|
14
|
-
* @param lle - Geodetic coordinates (lng in
|
|
16
|
+
* @param lle - Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
15
17
|
* @returns Cartesian Vector3 in Earth-Centered Earth-Fixed (ECEF) coordinates
|
|
16
18
|
*/
|
|
17
19
|
export declare function geodeticToVector3(lle: LatLngHeight): Vector3;
|
|
18
20
|
/**
|
|
19
21
|
* Converts a Cartesian Vector3 in ECEF coordinates to geodetic coordinates.
|
|
20
22
|
* @param xyz - Cartesian Vector3 in Earth-Centered Earth-Fixed (ECEF) coordinates
|
|
21
|
-
* @returns Geodetic coordinates (lng in
|
|
23
|
+
* @returns Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
22
24
|
*/
|
|
23
25
|
export declare function vector3ToGeodetic(xyz: Vector3): LatLngHeight;
|
|
24
26
|
/**
|
|
@@ -43,7 +45,7 @@ export declare function radianToDegree(radian: number): number;
|
|
|
43
45
|
export declare function convertScreenToWorld(windowObject: WindowObject, camera: PerspectiveCamera, vec2: Vector2): Vector3 | undefined;
|
|
44
46
|
/**
|
|
45
47
|
* Computes the surface normal vector at a geodetic position on the WGS84 ellipsoid.
|
|
46
|
-
* @param lle - Geodetic coordinates (lng in
|
|
48
|
+
* @param lle - Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
47
49
|
* @returns Unit normal vector pointing outward from the ellipsoid surface
|
|
48
50
|
*/
|
|
49
51
|
export declare function geodeticSurfaceNormal(lle: LatLngHeight): Vector3;
|
|
@@ -71,6 +73,40 @@ export declare function northUpEastToFixedFrame(origin: Vector3): Matrix4;
|
|
|
71
73
|
* @returns 4x4 transformation matrix from NWU to ECEF
|
|
72
74
|
*/
|
|
73
75
|
export declare function northWestUpToFixedFrame(origin: Vector3): Matrix4;
|
|
76
|
+
/**
|
|
77
|
+
* Creates a local West-Up-North (WUN) reference frame transformation matrix at
|
|
78
|
+
* a position.
|
|
79
|
+
*
|
|
80
|
+
* WUN is the only right-handed, Y-up tangent frame whose `+Z` axis is north, so
|
|
81
|
+
* it agrees with glTF's own convention (front `+Z`, up `+Y`, right `-X`) on all
|
|
82
|
+
* three axes. An unmodified glTF asset placed in this frame needs **no**
|
|
83
|
+
* `Rx(+90°)` up-axis correction — the correction is absorbed here, once, as the
|
|
84
|
+
* `Ry(+90°)` basis change from NUE.
|
|
85
|
+
*
|
|
86
|
+
* @param origin - Origin position in ECEF coordinates
|
|
87
|
+
* @returns 4x4 transformation matrix from WUN to ECEF
|
|
88
|
+
*/
|
|
89
|
+
export declare function westUpNorthToFixedFrame(origin: Vector3): Matrix4;
|
|
90
|
+
/**
|
|
91
|
+
* Builds a WUN tangent frame at a geodetic position and composes heading,
|
|
92
|
+
* pitch, roll and scale onto it — the single call that turns a geographic
|
|
93
|
+
* placement into a world matrix.
|
|
94
|
+
*
|
|
95
|
+
* `heading` is the compass bearing the asset's front faces, where the front is
|
|
96
|
+
* glTF's own `+Z` — the same convention as `ThreeView.setCamera`. Other globe
|
|
97
|
+
* engines differ in which axis they treat as an asset's front, so a model
|
|
98
|
+
* ported from one may need a multiple of 90° added to its heading.
|
|
99
|
+
*
|
|
100
|
+
* `lng`/`lat` and all angles here are in **degrees**, matching
|
|
101
|
+
* `setCamera` and the `geodetic` mesh Descriptor field.
|
|
102
|
+
*
|
|
103
|
+
* `heightReference` is intentionally not accepted: resolving terrain-relative
|
|
104
|
+
* height needs a view to sample from, so it is the Descriptor's job.
|
|
105
|
+
*
|
|
106
|
+
* @param placement - Geographic placement in degrees and metres
|
|
107
|
+
* @returns 4x4 transformation matrix from the placed local frame to ECEF
|
|
108
|
+
*/
|
|
109
|
+
export declare function headingPitchRollToFixedFrame(placement: Omit<GeodeticPlacement, "heightReference">): Matrix4;
|
|
74
110
|
/**
|
|
75
111
|
* Converts world coordinates to screen coordinates.
|
|
76
112
|
* @param window - Window configuration with width, height, and pixelRatio
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,56 @@
|
|
|
1
|
-
import e, { CameraFrustum as t, EllipsoidGeodesic as n, LLE as r, Ray as i, Transform as a, Vec2 as o, Vec3 as s, Window as c, angleToDegree as l, angleToRadian as u, eastNorthUpToFixedFrame as
|
|
2
|
-
import {
|
|
1
|
+
import e, { CameraFrustum as t, EllipsoidGeodesic as n, LLE as r, Ray as i, Transform as a, Vec2 as o, Vec3 as s, Window as c, angleToDegree as l, angleToRadian as u, eastNorthUpToFixedFrame as d, encodePosition as f, geodeticSurfaceNormal as p, geodeticToXyz as m, getHeightFromEllipsoid as h, getPickRay as g, getPlaneFromPointNormal as _, getRayPlaneIntersection as v, getWGS84Eccentricity as y, getWGS84EccentricitySquared as b, getWGS84Flattening as x, getWGS84SemiMajorAxis as S, getWGS84SemiMinorAxis as C, northEastDownToFixedFrame as ee, northUpEastToFixedFrame as te, northWestUpToFixedFrame as w, screenToWorld as T, worldToScreen as E, xyzToGeodetic as D } from "@navaramap/engine-api";
|
|
2
|
+
import { Euler as O, MathUtils as k, Matrix4 as A, Plane as j, Ray as M, Vector2 as N, Vector3 as P } from "three";
|
|
3
|
+
//#region src/frames.ts
|
|
4
|
+
function F() {
|
|
5
|
+
return new A().makeRotationY(Math.PI / 2);
|
|
6
|
+
}
|
|
7
|
+
function I(e, t) {
|
|
8
|
+
let n = new A().multiplyMatrices(e, new A().makeRotationFromEuler(new O(-k.degToRad(t.pitch ?? 0), -k.degToRad(t.heading ?? 0), k.degToRad(t.roll ?? 0), "YXZ")));
|
|
9
|
+
if (t.scale !== void 0) {
|
|
10
|
+
let e = t.scale;
|
|
11
|
+
n.scale(typeof e == "number" ? new P(e, e, e) : new P(e.x, e.y, e.z));
|
|
12
|
+
}
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
3
16
|
//#region src/intersection.ts
|
|
4
|
-
function
|
|
5
|
-
let n =
|
|
6
|
-
return
|
|
17
|
+
function L(e, t) {
|
|
18
|
+
let n = new s(e.x, e.y, e.z), r = new s(t.x, t.y, t.z), i = _(n, r), a = new P(i.normal.x, i.normal.y, i.normal.z), o = new j(a, -i.distance);
|
|
19
|
+
return i.free(), o;
|
|
7
20
|
}
|
|
8
|
-
function
|
|
21
|
+
function R(e, n, r) {
|
|
9
22
|
let i = new c(e.width, e.height, e.pixelRatio);
|
|
10
23
|
i.width *= i.pixelRatio, i.height *= i.pixelRatio;
|
|
11
|
-
let s =
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
function
|
|
15
|
-
let n =
|
|
16
|
-
if (
|
|
17
|
-
let e = new
|
|
18
|
-
return
|
|
24
|
+
let s = new a(n.position.x, n.position.y, n.position.z, n.quaternion.x, n.quaternion.y, n.quaternion.z, n.quaternion.w, n.scale.x, n.scale.y, n.scale.z), l = new t(n.near, n.far, u(n.fov), n.aspect), d = g(i, s, l, new o(r.x, r.y)), f = new P(d.origin.x, d.origin.y, d.origin.z), p = new P(d.direction.x, d.direction.y, d.direction.z), m = new M(f, p);
|
|
25
|
+
return d.free(), m;
|
|
26
|
+
}
|
|
27
|
+
function z(e, t) {
|
|
28
|
+
let n = new i(new s(e.origin.x, e.origin.y, e.origin.z), new s(e.direction.x, e.direction.y, e.direction.z)), r = _(new s(t.normal.x * -t.constant, t.normal.y * -t.constant, t.normal.z * -t.constant), new s(t.normal.x, t.normal.y, t.normal.z)), a = v(n, r);
|
|
29
|
+
if (a) {
|
|
30
|
+
let e = new P(a.x, a.y, a.z);
|
|
31
|
+
return a.free(), e;
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
34
|
+
function B(e) {
|
|
35
|
+
let t = new s(e.x, e.y, e.z);
|
|
36
|
+
return h(t);
|
|
23
37
|
}
|
|
24
38
|
//#endregion
|
|
25
39
|
//#region src/rte.ts
|
|
26
|
-
var
|
|
27
|
-
function
|
|
28
|
-
let r =
|
|
40
|
+
var V = Object.freeze({ value: 1 });
|
|
41
|
+
function H(e, t = new P(), n = new P()) {
|
|
42
|
+
let r = f(e.x, e.y, e.z);
|
|
29
43
|
return t.set(r.high.x, r.high.y, r.high.z), n.set(r.low.x, r.low.y, r.low.z), r.free(), {
|
|
30
44
|
high: t,
|
|
31
45
|
low: n
|
|
32
46
|
};
|
|
33
47
|
}
|
|
34
|
-
var
|
|
48
|
+
var ne = (e, t, n = new A()) => {
|
|
35
49
|
n.multiplyMatrices(t, e);
|
|
36
50
|
let r = n.elements;
|
|
37
51
|
return r[12] = 0, r[13] = 0, r[14] = 0, n;
|
|
38
52
|
};
|
|
39
|
-
function
|
|
53
|
+
function U(e, t, n = new P(), r = new A()) {
|
|
40
54
|
r.multiplyMatrices(e, t), n.setFromMatrixPosition(r);
|
|
41
55
|
let i = r.elements;
|
|
42
56
|
return i[12] = 0, i[13] = 0, i[14] = 0, {
|
|
@@ -44,17 +58,17 @@ function B(e, t, n = new N(), r = new k()) {
|
|
|
44
58
|
rotationScale: r
|
|
45
59
|
};
|
|
46
60
|
}
|
|
47
|
-
var
|
|
48
|
-
|
|
49
|
-
let n =
|
|
50
|
-
high: new
|
|
51
|
-
low: new
|
|
61
|
+
var W = new A(), G = new P(), K = (e, t) => {
|
|
62
|
+
W.copy(t).invert(), G.copy(e).applyMatrix4(W);
|
|
63
|
+
let n = f(G.x, G.y, G.z), r = n.high, i = n.low, a = {
|
|
64
|
+
high: new P(r.x, r.y, r.z),
|
|
65
|
+
low: new P(i.x, i.y, i.z)
|
|
52
66
|
};
|
|
53
67
|
return n.free(), a;
|
|
54
|
-
},
|
|
68
|
+
}, q = class {
|
|
55
69
|
_raw;
|
|
56
70
|
constructor(e, t) {
|
|
57
|
-
this._raw = new n(new r(e.lat, e.lng, e.height), new r(t.lat, t.lng, t.height));
|
|
71
|
+
this._raw = new n(new r(u(e.lat), u(e.lng), e.height), new r(u(t.lat), u(t.lng), t.height));
|
|
58
72
|
}
|
|
59
73
|
get distance() {
|
|
60
74
|
return this._raw.distance;
|
|
@@ -63,40 +77,40 @@ var V = new k(), H = new N(), U = (e, t) => {
|
|
|
63
77
|
return this._raw.converged;
|
|
64
78
|
}
|
|
65
79
|
get startHeading() {
|
|
66
|
-
return this._raw.start_heading;
|
|
80
|
+
return l(this._raw.start_heading);
|
|
67
81
|
}
|
|
68
82
|
get endHeading() {
|
|
69
|
-
return this._raw.end_heading;
|
|
83
|
+
return l(this._raw.end_heading);
|
|
70
84
|
}
|
|
71
85
|
get start() {
|
|
72
86
|
let e = this._raw.start;
|
|
73
87
|
return {
|
|
74
|
-
lat: e.lat,
|
|
75
|
-
lng: e.lng,
|
|
88
|
+
lat: l(e.lat),
|
|
89
|
+
lng: l(e.lng),
|
|
76
90
|
height: e.height
|
|
77
91
|
};
|
|
78
92
|
}
|
|
79
93
|
get end() {
|
|
80
94
|
let e = this._raw.end;
|
|
81
95
|
return {
|
|
82
|
-
lat: e.lat,
|
|
83
|
-
lng: e.lng,
|
|
96
|
+
lat: l(e.lat),
|
|
97
|
+
lng: l(e.lng),
|
|
84
98
|
height: e.height
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
interpolatePoints(e) {
|
|
88
102
|
let t = this._raw.interpolateGeodeticPoints(e ?? null), n = [];
|
|
89
103
|
for (let e of t) n.push({
|
|
90
|
-
lat: e.lat,
|
|
91
|
-
lng: e.lng,
|
|
104
|
+
lat: l(e.lat),
|
|
105
|
+
lng: l(e.lng),
|
|
92
106
|
height: e.height
|
|
93
107
|
}), e.free();
|
|
94
108
|
return n;
|
|
95
109
|
}
|
|
96
110
|
interpolateDistance(e) {
|
|
97
111
|
let t = this._raw.interpolateDistance(e), n = {
|
|
98
|
-
lat: t.lat,
|
|
99
|
-
lng: t.lng,
|
|
112
|
+
lat: l(t.lat),
|
|
113
|
+
lng: l(t.lng),
|
|
100
114
|
height: t.height
|
|
101
115
|
};
|
|
102
116
|
return t.free(), n;
|
|
@@ -107,77 +121,87 @@ var V = new k(), H = new N(), U = (e, t) => {
|
|
|
107
121
|
};
|
|
108
122
|
//#endregion
|
|
109
123
|
//#region src/index.ts
|
|
110
|
-
async function
|
|
124
|
+
async function J() {
|
|
111
125
|
await e();
|
|
112
126
|
}
|
|
113
|
-
function
|
|
114
|
-
let t =
|
|
127
|
+
function Y(e) {
|
|
128
|
+
let t = m(new r(u(e.lat), u(e.lng), e.height)), n = new P(t.x, t.y, t.z);
|
|
115
129
|
return t.free(), n;
|
|
116
130
|
}
|
|
117
|
-
function
|
|
118
|
-
let t =
|
|
119
|
-
lat:
|
|
120
|
-
lng:
|
|
121
|
-
height:
|
|
131
|
+
function X(e) {
|
|
132
|
+
let t = new s(e.x, e.y, e.z), n = D(t), r = {
|
|
133
|
+
lat: l(n.lat),
|
|
134
|
+
lng: l(n.lng),
|
|
135
|
+
height: n.height
|
|
122
136
|
};
|
|
123
|
-
return
|
|
137
|
+
return n.free(), r;
|
|
124
138
|
}
|
|
125
|
-
function
|
|
139
|
+
function Z(e) {
|
|
126
140
|
return u(e);
|
|
127
141
|
}
|
|
128
|
-
function
|
|
142
|
+
function re(e) {
|
|
129
143
|
return l(e);
|
|
130
144
|
}
|
|
131
|
-
function
|
|
145
|
+
function ie(e, n, r) {
|
|
132
146
|
let i = new c(e.width, e.height, e.pixelRatio);
|
|
133
147
|
i.width *= i.pixelRatio, i.height *= i.pixelRatio;
|
|
134
|
-
let s =
|
|
135
|
-
if (!
|
|
136
|
-
let
|
|
137
|
-
return
|
|
148
|
+
let s = new a(n.position.x, n.position.y, n.position.z, n.quaternion.x, n.quaternion.y, n.quaternion.z, n.quaternion.w, n.scale.x, n.scale.y, n.scale.z), l = new t(n.near, n.far, u(n.fov), n.aspect), d = T(i, s, l, new o(r.x, r.y));
|
|
149
|
+
if (!d) return;
|
|
150
|
+
let f = new P(d.x, d.y, d.z);
|
|
151
|
+
return d.free(), f;
|
|
138
152
|
}
|
|
139
|
-
function
|
|
140
|
-
let t =
|
|
153
|
+
function ae(e) {
|
|
154
|
+
let t = p(new r(u(e.lat), u(e.lng), e.height)), n = new P(t.x, t.y, t.z);
|
|
141
155
|
return t.free(), n;
|
|
142
156
|
}
|
|
157
|
+
function oe(e) {
|
|
158
|
+
let t = new s(e.x, e.y, e.z), n = d(t);
|
|
159
|
+
return new A().fromArray(n);
|
|
160
|
+
}
|
|
161
|
+
function se(e) {
|
|
162
|
+
let t = new s(e.x, e.y, e.z), n = ee(t);
|
|
163
|
+
return new A().fromArray(n);
|
|
164
|
+
}
|
|
143
165
|
function Q(e) {
|
|
144
|
-
let t =
|
|
145
|
-
return new
|
|
166
|
+
let t = new s(e.x, e.y, e.z), n = te(t);
|
|
167
|
+
return new A().fromArray(n);
|
|
146
168
|
}
|
|
147
|
-
function
|
|
148
|
-
let t =
|
|
149
|
-
return new
|
|
169
|
+
function ce(e) {
|
|
170
|
+
let t = new s(e.x, e.y, e.z), n = w(t);
|
|
171
|
+
return new A().fromArray(n);
|
|
150
172
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
return new k().fromArray(t);
|
|
173
|
+
function $(e) {
|
|
174
|
+
return Q(e).multiply(F());
|
|
154
175
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
function le(e) {
|
|
177
|
+
return I($(Y({
|
|
178
|
+
lng: e.lng,
|
|
179
|
+
lat: e.lat,
|
|
180
|
+
height: e.height ?? 0
|
|
181
|
+
})), e);
|
|
158
182
|
}
|
|
159
|
-
function
|
|
183
|
+
function ue(e, n, r) {
|
|
160
184
|
let i = new c(e.width, e.height, e.pixelRatio);
|
|
161
185
|
i.width *= i.pixelRatio, i.height *= i.pixelRatio;
|
|
162
|
-
let o =
|
|
163
|
-
if (!
|
|
164
|
-
let
|
|
165
|
-
return
|
|
186
|
+
let o = new a(n.position.x, n.position.y, n.position.z, n.quaternion.x, n.quaternion.y, n.quaternion.z, n.quaternion.w, n.scale.x, n.scale.y, n.scale.z), l = new t(n.near, n.far, u(n.fov), n.aspect), d = E(i, o, l, new s(r.x, r.y, r.z));
|
|
187
|
+
if (!d) return;
|
|
188
|
+
let f = new N(d.x, d.y);
|
|
189
|
+
return d.free(), f;
|
|
166
190
|
}
|
|
167
|
-
function
|
|
168
|
-
return x();
|
|
169
|
-
}
|
|
170
|
-
function $() {
|
|
191
|
+
function de() {
|
|
171
192
|
return S();
|
|
172
193
|
}
|
|
173
|
-
function
|
|
174
|
-
return
|
|
194
|
+
function fe() {
|
|
195
|
+
return C();
|
|
175
196
|
}
|
|
176
|
-
function
|
|
197
|
+
function pe() {
|
|
177
198
|
return b();
|
|
178
199
|
}
|
|
179
|
-
function
|
|
180
|
-
return
|
|
200
|
+
function me() {
|
|
201
|
+
return x();
|
|
202
|
+
}
|
|
203
|
+
function he() {
|
|
204
|
+
return y();
|
|
181
205
|
}
|
|
182
206
|
//#endregion
|
|
183
|
-
export {
|
|
207
|
+
export { q as EllipsoidGeodesic, V as RTE_ONE_UNIFORM, K as calcCameraPosition, ne as calcModelMatrixRTE, U as composeWorldMatrixForRTE, ie as convertScreenToWorld, ue as convertWorldToScreen, Z as degreeToRadian, oe as eastNorthUpToFixedFrame, H as encodePositionRTE, ae as geodeticSurfaceNormal, Y as geodeticToVector3, B as getHeightFromEllipsoid, R as getPickRay, L as getPlaneFromPointNormal, z as getRayPlaneIntersection, he as getWGS84Eccentricity, pe as getWGS84EccentricitySquared, me as getWGS84Flattening, de as getWGS84SemiMajorAxis, fe as getWGS84SemiMinorAxis, le as headingPitchRollToFixedFrame, J as initNavaraApi, se as northEastDownToFixedFrame, Q as northUpEastToFixedFrame, ce as northWestUpToFixedFrame, re as radianToDegree, X as vector3ToGeodetic, $ as westUpNorthToFixedFrame };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.umd.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@navaramap/engine-api"),require("three")):typeof define==`function`&&define.amd?define([`exports`,`@navaramap/engine-api`,`three`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.NavaraThreeApi={},e._navaramap_engine_api,e.three))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);function u(e,
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@navaramap/engine-api"),require("three")):typeof define==`function`&&define.amd?define([`exports`,`@navaramap/engine-api`,`three`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.NavaraThreeApi={},e._navaramap_engine_api,e.three))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule||!c.call(e,`default`)?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);function u(){return new n.Matrix4().makeRotationY(Math.PI/2)}function d(e,t){let r=new n.Matrix4().multiplyMatrices(e,new n.Matrix4().makeRotationFromEuler(new n.Euler(-n.MathUtils.degToRad(t.pitch??0),-n.MathUtils.degToRad(t.heading??0),n.MathUtils.degToRad(t.roll??0),`YXZ`)));if(t.scale!==void 0){let e=t.scale;r.scale(typeof e==`number`?new n.Vector3(e,e,e):new n.Vector3(e.x,e.y,e.z))}return r}function f(e,r){let i=new t.Vec3(e.x,e.y,e.z),a=new t.Vec3(r.x,r.y,r.z),o=(0,t.getPlaneFromPointNormal)(i,a),s=new n.Vector3(o.normal.x,o.normal.y,o.normal.z),c=new n.Plane(s,-o.distance);return o.free(),c}function p(e,r,i){let a=new t.Window(e.width,e.height,e.pixelRatio);a.width*=a.pixelRatio,a.height*=a.pixelRatio;let o=new t.Transform(r.position.x,r.position.y,r.position.z,r.quaternion.x,r.quaternion.y,r.quaternion.z,r.quaternion.w,r.scale.x,r.scale.y,r.scale.z),s=new t.CameraFrustum(r.near,r.far,(0,t.angleToRadian)(r.fov),r.aspect),c=(0,t.getPickRay)(a,o,s,new t.Vec2(i.x,i.y)),l=new n.Vector3(c.origin.x,c.origin.y,c.origin.z),u=new n.Vector3(c.direction.x,c.direction.y,c.direction.z),d=new n.Ray(l,u);return c.free(),d}function m(e,r){let i=new t.Ray(new t.Vec3(e.origin.x,e.origin.y,e.origin.z),new t.Vec3(e.direction.x,e.direction.y,e.direction.z)),a=(0,t.getPlaneFromPointNormal)(new t.Vec3(r.normal.x*-r.constant,r.normal.y*-r.constant,r.normal.z*-r.constant),new t.Vec3(r.normal.x,r.normal.y,r.normal.z)),o=(0,t.getRayPlaneIntersection)(i,a);if(o){let e=new n.Vector3(o.x,o.y,o.z);return o.free(),e}}function h(e){let n=new t.Vec3(e.x,e.y,e.z);return(0,t.getHeightFromEllipsoid)(n)}var g=Object.freeze({value:1});function _(e,r=new n.Vector3,i=new n.Vector3){let a=(0,t.encodePosition)(e.x,e.y,e.z);return r.set(a.high.x,a.high.y,a.high.z),i.set(a.low.x,a.low.y,a.low.z),a.free(),{high:r,low:i}}var v=(e,t,r=new n.Matrix4)=>{r.multiplyMatrices(t,e);let i=r.elements;return i[12]=0,i[13]=0,i[14]=0,r};function y(e,t,r=new n.Vector3,i=new n.Matrix4){i.multiplyMatrices(e,t),r.setFromMatrixPosition(i);let a=i.elements;return a[12]=0,a[13]=0,a[14]=0,{position:r,rotationScale:i}}var b=new n.Matrix4,x=new n.Vector3,S=(e,r)=>{b.copy(r).invert(),x.copy(e).applyMatrix4(b);let i=(0,t.encodePosition)(x.x,x.y,x.z),a=i.high,o=i.low,s={high:new n.Vector3(a.x,a.y,a.z),low:new n.Vector3(o.x,o.y,o.z)};return i.free(),s},C=class{_raw;constructor(e,n){this._raw=new t.EllipsoidGeodesic(new t.LLE((0,t.angleToRadian)(e.lat),(0,t.angleToRadian)(e.lng),e.height),new t.LLE((0,t.angleToRadian)(n.lat),(0,t.angleToRadian)(n.lng),n.height))}get distance(){return this._raw.distance}get converged(){return this._raw.converged}get startHeading(){return(0,t.angleToDegree)(this._raw.start_heading)}get endHeading(){return(0,t.angleToDegree)(this._raw.end_heading)}get start(){let e=this._raw.start;return{lat:(0,t.angleToDegree)(e.lat),lng:(0,t.angleToDegree)(e.lng),height:e.height}}get end(){let e=this._raw.end;return{lat:(0,t.angleToDegree)(e.lat),lng:(0,t.angleToDegree)(e.lng),height:e.height}}interpolatePoints(e){let n=this._raw.interpolateGeodeticPoints(e??null),r=[];for(let e of n)r.push({lat:(0,t.angleToDegree)(e.lat),lng:(0,t.angleToDegree)(e.lng),height:e.height}),e.free();return r}interpolateDistance(e){let n=this._raw.interpolateDistance(e),r={lat:(0,t.angleToDegree)(n.lat),lng:(0,t.angleToDegree)(n.lng),height:n.height};return n.free(),r}dispose(){this._raw.free()}};async function w(){await(0,t.default)()}function T(e){let r=(0,t.geodeticToXyz)(new t.LLE((0,t.angleToRadian)(e.lat),(0,t.angleToRadian)(e.lng),e.height)),i=new n.Vector3(r.x,r.y,r.z);return r.free(),i}function E(e){let n=new t.Vec3(e.x,e.y,e.z),r=(0,t.xyzToGeodetic)(n),i={lat:(0,t.angleToDegree)(r.lat),lng:(0,t.angleToDegree)(r.lng),height:r.height};return r.free(),i}function D(e){return(0,t.angleToRadian)(e)}function O(e){return(0,t.angleToDegree)(e)}function k(e,r,i){let a=new t.Window(e.width,e.height,e.pixelRatio);a.width*=a.pixelRatio,a.height*=a.pixelRatio;let o=new t.Transform(r.position.x,r.position.y,r.position.z,r.quaternion.x,r.quaternion.y,r.quaternion.z,r.quaternion.w,r.scale.x,r.scale.y,r.scale.z),s=new t.CameraFrustum(r.near,r.far,(0,t.angleToRadian)(r.fov),r.aspect),c=(0,t.screenToWorld)(a,o,s,new t.Vec2(i.x,i.y));if(!c)return;let l=new n.Vector3(c.x,c.y,c.z);return c.free(),l}function A(e){let r=(0,t.geodeticSurfaceNormal)(new t.LLE((0,t.angleToRadian)(e.lat),(0,t.angleToRadian)(e.lng),e.height)),i=new n.Vector3(r.x,r.y,r.z);return r.free(),i}function j(e){let r=new t.Vec3(e.x,e.y,e.z),i=(0,t.eastNorthUpToFixedFrame)(r);return new n.Matrix4().fromArray(i)}function M(e){let r=new t.Vec3(e.x,e.y,e.z),i=(0,t.northEastDownToFixedFrame)(r);return new n.Matrix4().fromArray(i)}function N(e){let r=new t.Vec3(e.x,e.y,e.z),i=(0,t.northUpEastToFixedFrame)(r);return new n.Matrix4().fromArray(i)}function P(e){let r=new t.Vec3(e.x,e.y,e.z),i=(0,t.northWestUpToFixedFrame)(r);return new n.Matrix4().fromArray(i)}function F(e){return N(e).multiply(u())}function I(e){return d(F(T({lng:e.lng,lat:e.lat,height:e.height??0})),e)}function L(e,r,i){let a=new t.Window(e.width,e.height,e.pixelRatio);a.width*=a.pixelRatio,a.height*=a.pixelRatio;let o=new t.Transform(r.position.x,r.position.y,r.position.z,r.quaternion.x,r.quaternion.y,r.quaternion.z,r.quaternion.w,r.scale.x,r.scale.y,r.scale.z),s=new t.CameraFrustum(r.near,r.far,(0,t.angleToRadian)(r.fov),r.aspect),c=(0,t.worldToScreen)(a,o,s,new t.Vec3(i.x,i.y,i.z));if(!c)return;let l=new n.Vector2(c.x,c.y);return c.free(),l}function R(){return(0,t.getWGS84SemiMajorAxis)()}function z(){return(0,t.getWGS84SemiMinorAxis)()}function B(){return(0,t.getWGS84EccentricitySquared)()}function V(){return(0,t.getWGS84Flattening)()}function H(){return(0,t.getWGS84Eccentricity)()}e.EllipsoidGeodesic=C,e.RTE_ONE_UNIFORM=g,e.calcCameraPosition=S,e.calcModelMatrixRTE=v,e.composeWorldMatrixForRTE=y,e.convertScreenToWorld=k,e.convertWorldToScreen=L,e.degreeToRadian=D,e.eastNorthUpToFixedFrame=j,e.encodePositionRTE=_,e.geodeticSurfaceNormal=A,e.geodeticToVector3=T,e.getHeightFromEllipsoid=h,e.getPickRay=p,e.getPlaneFromPointNormal=f,e.getRayPlaneIntersection=m,e.getWGS84Eccentricity=H,e.getWGS84EccentricitySquared=B,e.getWGS84Flattening=V,e.getWGS84SemiMajorAxis=R,e.getWGS84SemiMinorAxis=z,e.headingPitchRollToFixedFrame=I,e.initNavaraApi=w,e.northEastDownToFixedFrame=M,e.northUpEastToFixedFrame=N,e.northWestUpToFixedFrame=P,e.radianToDegree=O,e.vector3ToGeodetic=E,e.westUpNorthToFixedFrame=F});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navaramap/three-api",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"main": "dist/index.umd.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"three": ">=0.183.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@navaramap/
|
|
26
|
-
"@navaramap/
|
|
25
|
+
"@navaramap/core": "0.1.0",
|
|
26
|
+
"@navaramap/engine-api": "0.1.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/three": "0.185.1",
|
package/src/ellipsoidGeodesic.ts
CHANGED
|
@@ -2,17 +2,21 @@ import type { LatLngHeight } from "@navaramap/core";
|
|
|
2
2
|
import {
|
|
3
3
|
EllipsoidGeodesic as EllipsoidGeodesicImpl,
|
|
4
4
|
LLE,
|
|
5
|
+
angleToRadian,
|
|
6
|
+
angleToDegree,
|
|
5
7
|
} from "@navaramap/engine-api";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Wrapper class for geodesic calculations on an ellipsoid surface.
|
|
9
11
|
* Precomputes common variables for optimization when instantiated.
|
|
10
12
|
*
|
|
13
|
+
* All latitudes/longitudes and headings are in **degrees**.
|
|
14
|
+
*
|
|
11
15
|
* @example
|
|
12
16
|
* ```typescript
|
|
13
17
|
* const geodesic = new EllipsoidGeodesic(
|
|
14
|
-
* { lat:
|
|
15
|
-
* { lat:
|
|
18
|
+
* { lat: 35.6812, lng: 139.7671, height: 0 },
|
|
19
|
+
* { lat: 34.7025, lng: 135.4959, height: 0 }
|
|
16
20
|
* );
|
|
17
21
|
*
|
|
18
22
|
* console.log(geodesic.distance);
|
|
@@ -27,13 +31,13 @@ export class EllipsoidGeodesic {
|
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Create a new geodesic between two points on the ellipsoid.
|
|
30
|
-
* @param start - Start point in geodetic coordinates (lat/lng in
|
|
31
|
-
* @param end - End point in geodetic coordinates (lat/lng in
|
|
34
|
+
* @param start - Start point in geodetic coordinates (lat/lng in degrees)
|
|
35
|
+
* @param end - End point in geodetic coordinates (lat/lng in degrees)
|
|
32
36
|
*/
|
|
33
37
|
constructor(start: LatLngHeight, end: LatLngHeight) {
|
|
34
38
|
this._raw = new EllipsoidGeodesicImpl(
|
|
35
|
-
new LLE(start.lat, start.lng, start.height),
|
|
36
|
-
new LLE(end.lat, end.lng, end.height),
|
|
39
|
+
new LLE(angleToRadian(start.lat), angleToRadian(start.lng), start.height),
|
|
40
|
+
new LLE(angleToRadian(end.lat), angleToRadian(end.lng), end.height),
|
|
37
41
|
);
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -52,32 +56,40 @@ export class EllipsoidGeodesic {
|
|
|
52
56
|
return this._raw.converged;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
/** Heading at the start point in
|
|
59
|
+
/** Heading at the start point in degrees */
|
|
56
60
|
get startHeading(): number {
|
|
57
|
-
return this._raw.start_heading;
|
|
61
|
+
return angleToDegree(this._raw.start_heading);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
/** Heading at the end point in
|
|
64
|
+
/** Heading at the end point in degrees */
|
|
61
65
|
get endHeading(): number {
|
|
62
|
-
return this._raw.end_heading;
|
|
66
|
+
return angleToDegree(this._raw.end_heading);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
/** Start point in geodetic coordinates */
|
|
69
|
+
/** Start point in geodetic coordinates (lat/lng in degrees) */
|
|
66
70
|
get start(): LatLngHeight {
|
|
67
71
|
const lle = this._raw.start;
|
|
68
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
lat: angleToDegree(lle.lat),
|
|
74
|
+
lng: angleToDegree(lle.lng),
|
|
75
|
+
height: lle.height,
|
|
76
|
+
};
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
/** End point in geodetic coordinates */
|
|
79
|
+
/** End point in geodetic coordinates (lat/lng in degrees) */
|
|
72
80
|
get end(): LatLngHeight {
|
|
73
81
|
const lle = this._raw.end;
|
|
74
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
lat: angleToDegree(lle.lat),
|
|
84
|
+
lng: angleToDegree(lle.lng),
|
|
85
|
+
height: lle.height,
|
|
86
|
+
};
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
/**
|
|
78
90
|
* Interpolate points along the geodesic path.
|
|
79
91
|
* @param granularity - Distance between interpolated points in meters (optional)
|
|
80
|
-
* @returns Array of interpolated points in geodetic coordinates
|
|
92
|
+
* @returns Array of interpolated points in geodetic coordinates (lat/lng in degrees)
|
|
81
93
|
*/
|
|
82
94
|
interpolatePoints(granularity?: number): LatLngHeight[] {
|
|
83
95
|
const wasmPoints = this._raw.interpolateGeodeticPoints(granularity ?? null);
|
|
@@ -85,8 +97,8 @@ export class EllipsoidGeodesic {
|
|
|
85
97
|
const results: LatLngHeight[] = [];
|
|
86
98
|
for (const point of wasmPoints) {
|
|
87
99
|
results.push({
|
|
88
|
-
lat: point.lat,
|
|
89
|
-
lng: point.lng,
|
|
100
|
+
lat: angleToDegree(point.lat),
|
|
101
|
+
lng: angleToDegree(point.lng),
|
|
90
102
|
height: point.height,
|
|
91
103
|
});
|
|
92
104
|
point.free();
|
|
@@ -98,14 +110,14 @@ export class EllipsoidGeodesic {
|
|
|
98
110
|
/**
|
|
99
111
|
* Interpolate a point at a specific distance along the geodesic path.
|
|
100
112
|
* @param distance - Distance from start point in meters
|
|
101
|
-
* @returns Interpolated point in geodetic coordinates
|
|
113
|
+
* @returns Interpolated point in geodetic coordinates (lat/lng in degrees)
|
|
102
114
|
*/
|
|
103
115
|
interpolateDistance(distance: number): LatLngHeight {
|
|
104
116
|
const wasmPoint = this._raw.interpolateDistance(distance);
|
|
105
117
|
|
|
106
118
|
const result: LatLngHeight = {
|
|
107
|
-
lat: wasmPoint.lat,
|
|
108
|
-
lng: wasmPoint.lng,
|
|
119
|
+
lat: angleToDegree(wasmPoint.lat),
|
|
120
|
+
lng: angleToDegree(wasmPoint.lng),
|
|
109
121
|
height: wasmPoint.height,
|
|
110
122
|
};
|
|
111
123
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Matrix4, Vector3 } from "three";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { composeHeadingPitchRoll, nueToWunBasis } from "./frames";
|
|
5
|
+
|
|
6
|
+
// glTF 2.0: an asset's front faces +Z, its up is +Y, and -X is its right.
|
|
7
|
+
const FRONT = new Vector3(0, 0, 1);
|
|
8
|
+
const RIGHT = new Vector3(-1, 0, 0);
|
|
9
|
+
|
|
10
|
+
// Directions in the WUN local frame, where +x = west, +y = up, +z = north.
|
|
11
|
+
const NORTH = [0, 0, 1] as const;
|
|
12
|
+
const SOUTH = [0, 0, -1] as const;
|
|
13
|
+
const EAST = [-1, 0, 0] as const;
|
|
14
|
+
const WEST = [1, 0, 0] as const;
|
|
15
|
+
const UP = [0, 1, 0] as const;
|
|
16
|
+
const DOWN = [0, -1, 0] as const;
|
|
17
|
+
|
|
18
|
+
const expectDir = (
|
|
19
|
+
m: Matrix4,
|
|
20
|
+
axis: Vector3,
|
|
21
|
+
expected: readonly [number, number, number],
|
|
22
|
+
) => {
|
|
23
|
+
const actual = axis.clone().transformDirection(m);
|
|
24
|
+
expect(actual.x).toBeCloseTo(expected[0], 6);
|
|
25
|
+
expect(actual.y).toBeCloseTo(expected[1], 6);
|
|
26
|
+
expect(actual.z).toBeCloseTo(expected[2], 6);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("composeHeadingPitchRoll", () => {
|
|
30
|
+
it("faces north with no rotation, right hand to the east", () => {
|
|
31
|
+
const m = composeHeadingPitchRoll(new Matrix4(), {});
|
|
32
|
+
expectDir(m, FRONT, NORTH);
|
|
33
|
+
expectDir(m, RIGHT, EAST);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reads heading as a clockwise compass bearing", () => {
|
|
37
|
+
expectDir(
|
|
38
|
+
composeHeadingPitchRoll(new Matrix4(), { heading: 0 }),
|
|
39
|
+
FRONT,
|
|
40
|
+
NORTH,
|
|
41
|
+
);
|
|
42
|
+
expectDir(
|
|
43
|
+
composeHeadingPitchRoll(new Matrix4(), { heading: 90 }),
|
|
44
|
+
FRONT,
|
|
45
|
+
EAST,
|
|
46
|
+
);
|
|
47
|
+
expectDir(
|
|
48
|
+
composeHeadingPitchRoll(new Matrix4(), { heading: 180 }),
|
|
49
|
+
FRONT,
|
|
50
|
+
SOUTH,
|
|
51
|
+
);
|
|
52
|
+
expectDir(
|
|
53
|
+
composeHeadingPitchRoll(new Matrix4(), { heading: 270 }),
|
|
54
|
+
FRONT,
|
|
55
|
+
WEST,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("lifts the nose for positive pitch", () => {
|
|
60
|
+
expectDir(composeHeadingPitchRoll(new Matrix4(), { pitch: 90 }), FRONT, UP);
|
|
61
|
+
expectDir(
|
|
62
|
+
composeHeadingPitchRoll(new Matrix4(), { pitch: -90 }),
|
|
63
|
+
FRONT,
|
|
64
|
+
DOWN,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("drops the right wing for positive roll, leaving the nose alone", () => {
|
|
69
|
+
const m = composeHeadingPitchRoll(new Matrix4(), { roll: 90 });
|
|
70
|
+
expectDir(m, RIGHT, DOWN);
|
|
71
|
+
expectDir(m, FRONT, NORTH);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("applies yaw, then pitch, then roll", () => {
|
|
75
|
+
// Roll is innermost: it turns the asset about its own nose before pitch
|
|
76
|
+
// tips that nose up, so the rolled right wing ends up pointing north.
|
|
77
|
+
// Reversing the order would put the right wing east instead.
|
|
78
|
+
const m = composeHeadingPitchRoll(new Matrix4(), { pitch: 90, roll: 90 });
|
|
79
|
+
expectDir(m, FRONT, UP);
|
|
80
|
+
expectDir(m, RIGHT, NORTH);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("accepts uniform and per-axis scale", () => {
|
|
84
|
+
const uniform = new Vector3().setFromMatrixScale(
|
|
85
|
+
composeHeadingPitchRoll(new Matrix4(), { scale: 2.5 }),
|
|
86
|
+
);
|
|
87
|
+
expect(uniform.x).toBeCloseTo(2.5, 6);
|
|
88
|
+
expect(uniform.y).toBeCloseTo(2.5, 6);
|
|
89
|
+
expect(uniform.z).toBeCloseTo(2.5, 6);
|
|
90
|
+
|
|
91
|
+
const perAxis = new Vector3().setFromMatrixScale(
|
|
92
|
+
composeHeadingPitchRoll(new Matrix4(), { scale: { x: 2, y: 3, z: 4 } }),
|
|
93
|
+
);
|
|
94
|
+
expect(perAxis.x).toBeCloseTo(2, 6);
|
|
95
|
+
expect(perAxis.y).toBeCloseTo(3, 6);
|
|
96
|
+
expect(perAxis.z).toBeCloseTo(4, 6);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("round-trips heading through a bearing, matching setCamera", () => {
|
|
100
|
+
// navara_camera::helpers::get_heading yields 0 for north and 90 for east.
|
|
101
|
+
// This test is the guard against the mesh and camera conventions drifting.
|
|
102
|
+
for (const heading of [0, 17, 45, 90, 137, 180, 233, 270, 359]) {
|
|
103
|
+
const front = FRONT.clone().transformDirection(
|
|
104
|
+
composeHeadingPitchRoll(new Matrix4(), { heading }),
|
|
105
|
+
);
|
|
106
|
+
const bearing = (Math.atan2(-front.x, front.z) * 180) / Math.PI;
|
|
107
|
+
expect((bearing + 360) % 360).toBeCloseTo(heading, 4);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("maps the asset front into ECEF through a real WUN frame", () => {
|
|
112
|
+
// WUN at lng 0, lat 0: west = -Y_ecef, up = +X_ecef, north = +Z_ecef.
|
|
113
|
+
const wun = new Matrix4().makeBasis(
|
|
114
|
+
new Vector3(0, -1, 0),
|
|
115
|
+
new Vector3(1, 0, 0),
|
|
116
|
+
new Vector3(0, 0, 1),
|
|
117
|
+
);
|
|
118
|
+
expectDir(composeHeadingPitchRoll(wun, { heading: 0 }), FRONT, [0, 0, 1]);
|
|
119
|
+
expectDir(composeHeadingPitchRoll(wun, { heading: 90 }), FRONT, [0, 1, 0]);
|
|
120
|
+
expectDir(composeHeadingPitchRoll(wun, { pitch: 90 }), FRONT, [1, 0, 0]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("nueToWunBasis", () => {
|
|
125
|
+
// navara_core's local_frame_to_fixed_frame builds
|
|
126
|
+
// east = normalize(-origin.y, origin.x, 0) and north = up x east,
|
|
127
|
+
// so the ECEF axes at these two points are exact and hand-checkable.
|
|
128
|
+
const cases = [
|
|
129
|
+
{
|
|
130
|
+
name: "lng 0, lat 0",
|
|
131
|
+
up: new Vector3(1, 0, 0),
|
|
132
|
+
east: new Vector3(0, 1, 0),
|
|
133
|
+
north: new Vector3(0, 0, 1),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "lng 90, lat 0",
|
|
137
|
+
up: new Vector3(0, 1, 0),
|
|
138
|
+
east: new Vector3(-1, 0, 0),
|
|
139
|
+
north: new Vector3(0, 0, 1),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
it.each(cases)(
|
|
144
|
+
"turns an NUE basis into a WUN basis at $name",
|
|
145
|
+
({ up, east, north }) => {
|
|
146
|
+
const nue = new Matrix4().makeBasis(north, up, east);
|
|
147
|
+
const wun = new Matrix4().makeBasis(east.clone().negate(), up, north);
|
|
148
|
+
const converted = nue.clone().multiply(nueToWunBasis());
|
|
149
|
+
converted.elements.forEach((value, i) => {
|
|
150
|
+
expect(value).toBeCloseTo(wun.elements[i], 6);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
});
|
package/src/frames.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { XYZ } from "@navaramap/core";
|
|
2
|
+
import { Euler, MathUtils, Matrix4, Vector3 } from "three";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* High-level geographic placement of a mesh object.
|
|
6
|
+
*
|
|
7
|
+
* All angles are in **degrees** and `height` is in metres, matching
|
|
8
|
+
* `ThreeView.setCamera`. The asset is oriented in a West-Up-North tangent
|
|
9
|
+
* frame, which matches glTF's own convention (front `+Z`, up `+Y`,
|
|
10
|
+
* right `-X`) on all three axes — so an unmodified glTF asset needs no
|
|
11
|
+
* up-axis correction.
|
|
12
|
+
*/
|
|
13
|
+
export type GeodeticPlacement = {
|
|
14
|
+
/** Longitude in degrees. */
|
|
15
|
+
lng: number;
|
|
16
|
+
/** Latitude in degrees. */
|
|
17
|
+
lat: number;
|
|
18
|
+
/**
|
|
19
|
+
* Metres above the ellipsoid, or above terrain when `heightReference` is
|
|
20
|
+
* `"terrain"`. Defaults to 0.
|
|
21
|
+
*/
|
|
22
|
+
height?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Degrees clockwise from north that the asset's front (glTF `+Z`) faces.
|
|
25
|
+
* Same convention as `setCamera`'s heading. Defaults to 0.
|
|
26
|
+
*/
|
|
27
|
+
heading?: number;
|
|
28
|
+
/** Degrees, nose up positive. Defaults to 0. */
|
|
29
|
+
pitch?: number;
|
|
30
|
+
/** Degrees, right wing down positive. Defaults to 0. */
|
|
31
|
+
roll?: number;
|
|
32
|
+
/** Uniform or per-axis scale of the frame. Defaults to 1. */
|
|
33
|
+
scale?: number | XYZ;
|
|
34
|
+
/** How `height` is interpreted. Defaults to `"ellipsoid"`. */
|
|
35
|
+
heightReference?: "ellipsoid" | "terrain";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Basis change from the NUE frame `(north, up, east)` to the WUN frame
|
|
40
|
+
* `(west, up, north)`.
|
|
41
|
+
*
|
|
42
|
+
* With `Ry(+90°)` columns `(0,0,-1), (0,1,0), (1,0,0)`, the product
|
|
43
|
+
* `NUE · Ry(+90°)` has columns `(-east, up, north)`, which is WUN. WUN is the
|
|
44
|
+
* unique right-handed, Y-up tangent frame whose `+Z` is north, which is why it
|
|
45
|
+
* agrees with glTF on every axis.
|
|
46
|
+
*
|
|
47
|
+
* Returns a fresh matrix on every call, so callers cannot corrupt a shared
|
|
48
|
+
* instance.
|
|
49
|
+
*
|
|
50
|
+
* @returns A new `Matrix4` performing the NUE-to-WUN basis change.
|
|
51
|
+
*/
|
|
52
|
+
export function nueToWunBasis(): Matrix4 {
|
|
53
|
+
return new Matrix4().makeRotationY(Math.PI / 2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Composes a tangent frame with heading, pitch, roll and scale.
|
|
58
|
+
*
|
|
59
|
+
* Pure: takes the frame as a parameter and uses three.js `MathUtils.degToRad`,
|
|
60
|
+
* so it touches no WASM and is directly unit-testable.
|
|
61
|
+
*
|
|
62
|
+
* `Ry(-heading) · Rx(-pitch) · Rz(+roll)` is exactly three.js Euler order
|
|
63
|
+
* `"YXZ"`, the standard intrinsic yaw -> pitch -> roll order. Signs follow from
|
|
64
|
+
* the glTF axes in a WUN frame: positive heading swings `+Z` toward `-X`
|
|
65
|
+
* (east), positive pitch lifts `+Z` toward `+Y`, and positive roll about `+Z`
|
|
66
|
+
* drives the `-X` wing toward `-Y`.
|
|
67
|
+
*
|
|
68
|
+
* @param frame - Tangent frame to compose onto, normally from
|
|
69
|
+
* `westUpNorthToFixedFrame`
|
|
70
|
+
* @param p - Heading, pitch, roll (degrees) and scale; each defaults to
|
|
71
|
+
* no-op
|
|
72
|
+
* @returns `frame · R(heading, pitch, roll) · S(scale)`
|
|
73
|
+
*/
|
|
74
|
+
export function composeHeadingPitchRoll(
|
|
75
|
+
frame: Matrix4,
|
|
76
|
+
p: Pick<GeodeticPlacement, "heading" | "pitch" | "roll" | "scale">,
|
|
77
|
+
): Matrix4 {
|
|
78
|
+
const result = new Matrix4().multiplyMatrices(
|
|
79
|
+
frame,
|
|
80
|
+
new Matrix4().makeRotationFromEuler(
|
|
81
|
+
new Euler(
|
|
82
|
+
-MathUtils.degToRad(p.pitch ?? 0),
|
|
83
|
+
-MathUtils.degToRad(p.heading ?? 0),
|
|
84
|
+
MathUtils.degToRad(p.roll ?? 0),
|
|
85
|
+
"YXZ",
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (p.scale !== undefined) {
|
|
91
|
+
const s = p.scale;
|
|
92
|
+
result.scale(
|
|
93
|
+
typeof s === "number" ? new Vector3(s, s, s) : new Vector3(s.x, s.y, s.z),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Vector3 } from "three";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// The WASM math is covered by Rust unit tests; mock it with a transparent JS
|
|
5
|
+
// model so these tests exercise only the TypeScript wiring — specifically that
|
|
6
|
+
// the basis change is applied on the RIGHT (`multiply`, not `premultiply`) and that
|
|
7
|
+
// degrees are converted before reaching geodeticToXyz.
|
|
8
|
+
const SEMI_MAJOR = 6378137;
|
|
9
|
+
|
|
10
|
+
vi.mock("@navaramap/engine-api", () => {
|
|
11
|
+
class LLE {
|
|
12
|
+
constructor(
|
|
13
|
+
public lat: number,
|
|
14
|
+
public lng: number,
|
|
15
|
+
public height: number,
|
|
16
|
+
) {}
|
|
17
|
+
free() {}
|
|
18
|
+
}
|
|
19
|
+
class Vec3 {
|
|
20
|
+
constructor(
|
|
21
|
+
public x: number,
|
|
22
|
+
public y: number,
|
|
23
|
+
public z: number,
|
|
24
|
+
) {}
|
|
25
|
+
free() {}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
default: vi.fn(),
|
|
29
|
+
LLE,
|
|
30
|
+
Vec3,
|
|
31
|
+
angleToRadian: (deg: number) => (deg * Math.PI) / 180,
|
|
32
|
+
angleToDegree: (rad: number) => (rad * 180) / Math.PI,
|
|
33
|
+
// Transparent model: the rotation is pinned to the NUE basis at
|
|
34
|
+
// lng 0 / lat 0 (column-major: x = north (0,0,1), y = up (1,0,0),
|
|
35
|
+
// z = east (0,1,0)), but the ORIGIN is threaded through from the
|
|
36
|
+
// argument. Hardcoding the translation would make the
|
|
37
|
+
// degree-conversion assertion below unobservable — it could never
|
|
38
|
+
// fail, whatever the implementation did.
|
|
39
|
+
northUpEastToFixedFrame: (origin: Vec3) => [
|
|
40
|
+
0,
|
|
41
|
+
0,
|
|
42
|
+
1,
|
|
43
|
+
0,
|
|
44
|
+
1,
|
|
45
|
+
0,
|
|
46
|
+
0,
|
|
47
|
+
0,
|
|
48
|
+
0,
|
|
49
|
+
1,
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
origin.x,
|
|
53
|
+
origin.y,
|
|
54
|
+
origin.z,
|
|
55
|
+
1,
|
|
56
|
+
],
|
|
57
|
+
geodeticToXyz: (lle: LLE) => ({
|
|
58
|
+
x: SEMI_MAJOR + lle.height,
|
|
59
|
+
y: lle.lng,
|
|
60
|
+
z: lle.lat,
|
|
61
|
+
free() {},
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const { headingPitchRollToFixedFrame, westUpNorthToFixedFrame } =
|
|
67
|
+
await import("./index");
|
|
68
|
+
|
|
69
|
+
describe("westUpNorthToFixedFrame", () => {
|
|
70
|
+
it("turns the NUE columns into (-east, up, north)", () => {
|
|
71
|
+
const wun = westUpNorthToFixedFrame(new Vector3(SEMI_MAJOR, 0, 0));
|
|
72
|
+
|
|
73
|
+
// `makeRotationY(Math.PI / 2)` leaves cos(PI/2) = 6.1e-17 in the
|
|
74
|
+
// matrix, so these are near-equalities, not exact ones.
|
|
75
|
+
const expectColumn = (
|
|
76
|
+
index: number,
|
|
77
|
+
expected: readonly [number, number, number],
|
|
78
|
+
) => {
|
|
79
|
+
const column = new Vector3().setFromMatrixColumn(wun, index);
|
|
80
|
+
expect(column.x).toBeCloseTo(expected[0], 12);
|
|
81
|
+
expect(column.y).toBeCloseTo(expected[1], 12);
|
|
82
|
+
expect(column.z).toBeCloseTo(expected[2], 12);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expectColumn(0, [0, -1, 0]); // west
|
|
86
|
+
expectColumn(1, [1, 0, 0]); // up
|
|
87
|
+
expectColumn(2, [0, 0, 1]); // north
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("keeps the ECEF origin in the translation column", () => {
|
|
91
|
+
const wun = westUpNorthToFixedFrame(new Vector3(SEMI_MAJOR, 0, 0));
|
|
92
|
+
const origin = new Vector3().setFromMatrixPosition(wun);
|
|
93
|
+
expect(origin.x).toBeCloseTo(SEMI_MAJOR, 6);
|
|
94
|
+
expect(origin.y).toBeCloseTo(0, 12);
|
|
95
|
+
expect(origin.z).toBeCloseTo(0, 12);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("headingPitchRollToFixedFrame", () => {
|
|
100
|
+
it("converts lng/lat from degrees before building the origin", () => {
|
|
101
|
+
// The mock echoes the radian lng/lat back as y/z, so the conversion is
|
|
102
|
+
// observable in the translation column.
|
|
103
|
+
const m = headingPitchRollToFixedFrame({ lng: 180, lat: 90, height: 12 });
|
|
104
|
+
const origin = new Vector3().setFromMatrixPosition(m);
|
|
105
|
+
expect(origin.x).toBeCloseTo(SEMI_MAJOR + 12, 6);
|
|
106
|
+
expect(origin.y).toBeCloseTo(Math.PI, 6);
|
|
107
|
+
expect(origin.z).toBeCloseTo(Math.PI / 2, 6);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("orients the asset front by heading in the WUN frame", () => {
|
|
111
|
+
// At lng 0 / lat 0 the mocked frame gives ECEF north = +Z, east = +Y.
|
|
112
|
+
const front = new Vector3(0, 0, 1);
|
|
113
|
+
|
|
114
|
+
const north = front
|
|
115
|
+
.clone()
|
|
116
|
+
.transformDirection(
|
|
117
|
+
headingPitchRollToFixedFrame({ lng: 0, lat: 0, heading: 0 }),
|
|
118
|
+
);
|
|
119
|
+
expect(north.x).toBeCloseTo(0, 6);
|
|
120
|
+
expect(north.y).toBeCloseTo(0, 6);
|
|
121
|
+
expect(north.z).toBeCloseTo(1, 6);
|
|
122
|
+
|
|
123
|
+
const east = front
|
|
124
|
+
.clone()
|
|
125
|
+
.transformDirection(
|
|
126
|
+
headingPitchRollToFixedFrame({ lng: 0, lat: 0, heading: 90 }),
|
|
127
|
+
);
|
|
128
|
+
expect(east.x).toBeCloseTo(0, 6);
|
|
129
|
+
expect(east.y).toBeCloseTo(1, 6);
|
|
130
|
+
expect(east.z).toBeCloseTo(0, 6);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("defaults height to zero", () => {
|
|
134
|
+
const m = headingPitchRollToFixedFrame({ lng: 0, lat: 0 });
|
|
135
|
+
expect(new Vector3().setFromMatrixPosition(m).x).toBeCloseTo(SEMI_MAJOR, 6);
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,12 @@ import initApi, {
|
|
|
25
25
|
} from "@navaramap/engine-api";
|
|
26
26
|
import { Vector3, Vector2, Matrix4, PerspectiveCamera } from "three";
|
|
27
27
|
|
|
28
|
+
import {
|
|
29
|
+
composeHeadingPitchRoll,
|
|
30
|
+
nueToWunBasis,
|
|
31
|
+
type GeodeticPlacement,
|
|
32
|
+
} from "./frames";
|
|
33
|
+
|
|
28
34
|
export type { LatLngHeight, LatLng } from "@navaramap/core";
|
|
29
35
|
export type {
|
|
30
36
|
Window,
|
|
@@ -35,6 +41,7 @@ export type {
|
|
|
35
41
|
Vec2,
|
|
36
42
|
Vec3,
|
|
37
43
|
} from "@navaramap/core";
|
|
44
|
+
export type { GeodeticPlacement } from "./frames";
|
|
38
45
|
|
|
39
46
|
export * from "./intersection";
|
|
40
47
|
export * from "./rte";
|
|
@@ -49,11 +56,13 @@ export async function initNavaraApi() {
|
|
|
49
56
|
|
|
50
57
|
/**
|
|
51
58
|
* Converts geodetic coordinates (longitude, latitude, height) to a Cartesian Vector3 in ECEF coordinates.
|
|
52
|
-
* @param lle - Geodetic coordinates (lng in
|
|
59
|
+
* @param lle - Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
53
60
|
* @returns Cartesian Vector3 in Earth-Centered Earth-Fixed (ECEF) coordinates
|
|
54
61
|
*/
|
|
55
62
|
export function geodeticToVector3(lle: LatLngHeight): Vector3 {
|
|
56
|
-
const pos = geodeticToXyz(
|
|
63
|
+
const pos = geodeticToXyz(
|
|
64
|
+
new LLE(angleToRadian(lle.lat), angleToRadian(lle.lng), lle.height),
|
|
65
|
+
);
|
|
57
66
|
const result = new Vector3(pos.x, pos.y, pos.z);
|
|
58
67
|
pos.free();
|
|
59
68
|
return result;
|
|
@@ -62,12 +71,16 @@ export function geodeticToVector3(lle: LatLngHeight): Vector3 {
|
|
|
62
71
|
/**
|
|
63
72
|
* Converts a Cartesian Vector3 in ECEF coordinates to geodetic coordinates.
|
|
64
73
|
* @param xyz - Cartesian Vector3 in Earth-Centered Earth-Fixed (ECEF) coordinates
|
|
65
|
-
* @returns Geodetic coordinates (lng in
|
|
74
|
+
* @returns Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
66
75
|
*/
|
|
67
76
|
export function vector3ToGeodetic(xyz: Vector3): LatLngHeight {
|
|
68
77
|
const vec3 = new Vec3(xyz.x, xyz.y, xyz.z);
|
|
69
78
|
const lle = xyzToGeodetic(vec3);
|
|
70
|
-
const result = {
|
|
79
|
+
const result = {
|
|
80
|
+
lat: angleToDegree(lle.lat),
|
|
81
|
+
lng: angleToDegree(lle.lng),
|
|
82
|
+
height: lle.height,
|
|
83
|
+
};
|
|
71
84
|
lle.free();
|
|
72
85
|
return result;
|
|
73
86
|
}
|
|
@@ -148,11 +161,13 @@ export function convertScreenToWorld(
|
|
|
148
161
|
|
|
149
162
|
/**
|
|
150
163
|
* Computes the surface normal vector at a geodetic position on the WGS84 ellipsoid.
|
|
151
|
-
* @param lle - Geodetic coordinates (lng in
|
|
164
|
+
* @param lle - Geodetic coordinates (lng in degrees, lat in degrees, height in meters)
|
|
152
165
|
* @returns Unit normal vector pointing outward from the ellipsoid surface
|
|
153
166
|
*/
|
|
154
167
|
export function geodeticSurfaceNormal(lle: LatLngHeight): Vector3 {
|
|
155
|
-
const pos = nvGeodeticSurfaceNormal(
|
|
168
|
+
const pos = nvGeodeticSurfaceNormal(
|
|
169
|
+
new LLE(angleToRadian(lle.lat), angleToRadian(lle.lng), lle.height),
|
|
170
|
+
);
|
|
156
171
|
const result = new Vector3(pos.x, pos.y, pos.z);
|
|
157
172
|
pos.free();
|
|
158
173
|
return result;
|
|
@@ -206,6 +221,53 @@ export function northWestUpToFixedFrame(origin: Vector3): Matrix4 {
|
|
|
206
221
|
return matrix;
|
|
207
222
|
}
|
|
208
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Creates a local West-Up-North (WUN) reference frame transformation matrix at
|
|
226
|
+
* a position.
|
|
227
|
+
*
|
|
228
|
+
* WUN is the only right-handed, Y-up tangent frame whose `+Z` axis is north, so
|
|
229
|
+
* it agrees with glTF's own convention (front `+Z`, up `+Y`, right `-X`) on all
|
|
230
|
+
* three axes. An unmodified glTF asset placed in this frame needs **no**
|
|
231
|
+
* `Rx(+90°)` up-axis correction — the correction is absorbed here, once, as the
|
|
232
|
+
* `Ry(+90°)` basis change from NUE.
|
|
233
|
+
*
|
|
234
|
+
* @param origin - Origin position in ECEF coordinates
|
|
235
|
+
* @returns 4x4 transformation matrix from WUN to ECEF
|
|
236
|
+
*/
|
|
237
|
+
export function westUpNorthToFixedFrame(origin: Vector3): Matrix4 {
|
|
238
|
+
return northUpEastToFixedFrame(origin).multiply(nueToWunBasis());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Builds a WUN tangent frame at a geodetic position and composes heading,
|
|
243
|
+
* pitch, roll and scale onto it — the single call that turns a geographic
|
|
244
|
+
* placement into a world matrix.
|
|
245
|
+
*
|
|
246
|
+
* `heading` is the compass bearing the asset's front faces, where the front is
|
|
247
|
+
* glTF's own `+Z` — the same convention as `ThreeView.setCamera`. Other globe
|
|
248
|
+
* engines differ in which axis they treat as an asset's front, so a model
|
|
249
|
+
* ported from one may need a multiple of 90° added to its heading.
|
|
250
|
+
*
|
|
251
|
+
* `lng`/`lat` and all angles here are in **degrees**, matching
|
|
252
|
+
* `setCamera` and the `geodetic` mesh Descriptor field.
|
|
253
|
+
*
|
|
254
|
+
* `heightReference` is intentionally not accepted: resolving terrain-relative
|
|
255
|
+
* height needs a view to sample from, so it is the Descriptor's job.
|
|
256
|
+
*
|
|
257
|
+
* @param placement - Geographic placement in degrees and metres
|
|
258
|
+
* @returns 4x4 transformation matrix from the placed local frame to ECEF
|
|
259
|
+
*/
|
|
260
|
+
export function headingPitchRollToFixedFrame(
|
|
261
|
+
placement: Omit<GeodeticPlacement, "heightReference">,
|
|
262
|
+
): Matrix4 {
|
|
263
|
+
const origin = geodeticToVector3({
|
|
264
|
+
lng: placement.lng,
|
|
265
|
+
lat: placement.lat,
|
|
266
|
+
height: placement.height ?? 0,
|
|
267
|
+
});
|
|
268
|
+
return composeHeadingPitchRoll(westUpNorthToFixedFrame(origin), placement);
|
|
269
|
+
}
|
|
270
|
+
|
|
209
271
|
/**
|
|
210
272
|
* Converts world coordinates to screen coordinates.
|
|
211
273
|
* @param window - Window configuration with width, height, and pixelRatio
|