geo3d 0.2.0

3D geodesy in pure f64, zero dependencies: geodetic/ECEF/ENU/NED frames on a reference ellipsoid, look angles, ray–ellipsoid intersection, volumes, closest approach, Helmert.
Documentation
# geo3d

3D geodesy in pure `f64`, zero dependencies.

Real 3D geometry cannot be done on geodetic `(lat, lon, height)` directly — it is not a Euclidean
space. This crate provides the metric frames and the closed-form primitives on top of them, in a small,
typed, allocation-free API:

| | |
|---|---|
| `Ellipsoid` (`WGS84`, `GRS80`, …) | geodetic ↔ ECEF (closed form both ways), prime-vertical radius, ray–ellipsoid intersection |
| `Geodetic`, `Ecef`, `Enu`, `Ned`, `Aer` | typed coordinates — lat/lon or east/north cannot be swapped by accident |
| `LocalFrame` | a topocentric East-North-Up frame that precomputes its rotation once; ENU ↔ ECEF ↔ geodetic, look angles (az/el/range) and their inverse, ground strike |
| `AltitudeBand`, `Sphere`, `Cone`, `Cylinder` (`Volume`) | closed-form volumes; compose with any 2D footprint test for extruded prisms |
| `Track`, `Course`, `closest_approach` | dead reckoning, course-over-ground ↔ ECEF velocity, first-order conflict detection |
| `Ellipsoid` extras | radii of curvature, geocentric latitude, horizon distance, geometric line of sight, ECEF interpolation, spherical great-circle distance / bearing / destination / midpoint |
| `eci` | Julian date, GMST (IAU 1982), inertial ↔ Earth-fixed |
| `Helmert7` | 7-parameter datum shifts, both EPSG rotation conventions |

Angles in the public API are degrees, lengths metres. ECEF is the frame CesiumJS renders in, so a
`(lat, lon, height)` track maps straight to what a globe draws.

```rust
use geo3d::{distance3d_m, Geodetic, LocalFrame, Sphere, Volume};

let site = Geodetic::new(34.05, -118.24, 100.0);
let aircraft = Geodetic::new(34.30, -118.10, 3_000.0);

let slant = distance3d_m(site, aircraft);          // through-space distance, metres
let frame = LocalFrame::new(site);
let aer = frame.look_angles(aircraft);              // azimuth / elevation / range
let back = frame.destination(aer);                  // ...and back to the aircraft
let ground = frame.ground_strike(aer.azimuth_deg, -3.0); // where a 3° down-look meets the surface
assert!(Sphere::new(site, 40_000.0).contains(aircraft));
```

`cargo run --example tracking` walks through a ground station tracking an aircraft.

## Design

- **Zero dependencies, pure `f64`.** Every type is `Copy`; nothing allocates; `#![forbid(unsafe_code)]`.
- **Precompute per reference, not per point.** `LocalFrame`, `Sphere` and `Cone` do their
  trigonometry once at construction, so "many targets against one observer" is a subtraction and a
  few dot products per point.
- **Closed forms.** Geodetic ↔ ECEF uses the exact forward form and Bowring's inverse
  (sub-millimetre at terrestrial and orbital heights, no iteration).
- **Ellipsoid-generic.** Everything takes an `Ellipsoid`; WGS84 is the default and has shorthand
  free functions.
- **Optional `serde`** feature derives `Serialize`/`Deserialize` on the coordinate types.

Out of scope, by design: planar 2D geometry (use the [`geo`](https://crates.io/crates/geo)
ecosystem), map projections (`proj`, `geodesy`), geodesics to millimetre accuracy over long lines
(`geographiclib`), terrain and meshes (`parry3d`), and precession/nutation/polar motion (the `eci`
module is the plain GMST rotation).

## Precision

Every function states its accuracy class, and the tests hold it to that:

| class | functions | how it is held |
|---|---|---|
| **exact closed form** | geodetic → ECEF, ENU/NED ↔ ECEF, look angles, ray–ellipsoid, volumes, CPA, Helmert `apply`/`apply_inverse` | round trips to 1e-11° / 1e-6 m over a global grid; cross-validated against `map_3d` (dev-only dependency) to 1e-6 m |
| **iterated to machine precision** | ECEF → geodetic (Bowring, to 1e-15 rad) | same grid, from −11 km to geostationary height |
| **geodesic, ~0.5 mm** | `geodesic_inverse` / `geodesic_direct` (Vincenty) | pinned to GeographicLib (Karney) reference values: Flinders Peak–Buninyong on two ellipsoids and four direct problems from 1 m to 9 000 km; exact quarter-equator and quarter-meridian lengths; worldwide direct↔inverse round trips to 1e-6 m. Nearly antipodal pairs return `None` rather than a wrong answer. |
| **spherical approximation (mean radius)** | `great_circle_*`, `horizon_distance_m` | named as such; within 0.5 % of the geodesic |
| **first-order / linearised** | `Helmert7::inverse` (use `apply_inverse` for exact), `interpolate` (ECEF chord), `Track` (constant velocity), `eci` (GMST only) | documented on each item |

## Develop

```bash
cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test --all-features
cargo publish --dry-run
```

## Versioning

Plain semver, tags `vX.Y.Z` at the published commit. See [CHANGELOG.md](CHANGELOG.md).