rustial_engine/precision_invariants.rs
1//! # Meter-Based Precision Invariants
2//!
3//! This module documents the precision invariants that Rustial maintains
4//! across the engine and both renderer paths (WGPU and Bevy). These
5//! invariants are validated by tests in
6//! [`support_contract`](crate::support_contract) and by headless
7//! regression tests in the renderer crates.
8//!
9//! ## 1. Internal units are meters
10//!
11//! All internal linear distances, world-space coordinates, and
12//! elevation values in the engine are expressed in **meters**.
13//!
14//! | Type | Unit |
15//! |------|------|
16//! | [`WorldCoord`](rustial_math::WorldCoord) | meters (east, north, up) |
17//! | [`GeoCoord::alt`](rustial_math::GeoCoord) | meters above WGS-84 ellipsoid |
18//! | [`ElevationGrid`](rustial_math::ElevationGrid) samples | meters |
19//! | [`Camera::distance()`](crate::Camera) | meters |
20//! | [`Camera::meters_per_pixel()`](crate::Camera) | meters per screen pixel |
21//! | Tile bounds ([`tile_bounds_world`](rustial_math::tile_bounds_world)) | meters |
22//! | Geodesic distances ([`geodesic_distance`](rustial_math::geodesic_distance)) | meters |
23//!
24//! ## 2. f64 engine, f32 GPU
25//!
26//! The engine performs **all math in f64**. GPU-bound data is cast to
27//! f32 only at upload time. This ensures centimetre-scale precision
28//! even at world-edge coordinates (x ~ 20 million metres).
29//!
30//! ## 3. Camera-relative rendering
31//!
32//! Both renderers use **camera-relative** vertex positions to avoid
33//! catastrophic f32 precision loss at large world coordinates:
34//!
35//! ```text
36//! GPU vertex position = world_position - camera_target_world()
37//! ```
38//!
39//! Because the subtraction happens in f64 *before* the cast to f32,
40//! the resulting f32 values are small (within a few kilometres of the
41//! origin) and retain sub-centimetre precision.
42//!
43//! ### Why this is necessary
44//!
45//! At longitude ~180 degrees, the absolute Mercator X coordinate is
46//! approximately 20 million metres. A 32-bit float at that magnitude
47//! has an epsilon of approximately 2 metres -- sub-metre detail would
48//! be lost. Camera-relative rendering eliminates this problem.
49//!
50//! ### What is subtracted
51//!
52//! | Renderer | Origin |
53//! |----------|--------|
54//! | WGPU | `MapState::scene_world_origin()` (= `camera.target_world()`) |
55//! | Bevy | Same -- the Bevy camera is at `eye_offset()`, tiles at `(world - origin)` |
56//!
57//! ## 4. Projection round-trip guarantees
58//!
59//! For **stable** (v1.0) projections, the following round-trip invariant
60//! holds at all valid inputs:
61//!
62//! ```text
63//! let world = projection.project(&geo);
64//! let back = projection.unproject(&world);
65//! assert!((back.lat - geo.lat).abs() < 1e-8);
66//! assert!((back.lon - geo.lon).abs() < 1e-8);
67//! ```
68//!
69//! | Projection | Latitude accuracy | Longitude accuracy | Altitude |
70//! |------------|------------------|--------------------|----------|
71//! | Web Mercator | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
72//! | Equirectangular | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
73//! | Globe | < 1e-6 deg | < 1e-6 deg | < 1 m |
74//! | Vertical Perspective | < 1e-5 deg | < 1e-5 deg | best-effort |
75//!
76//! ## 5. Anti-meridian handling
77//!
78//! - [`GeoCoord::clamped_mercator()`](rustial_math::GeoCoord::clamped_mercator)
79//! wraps longitude to `[-180, 180]` via modular arithmetic.
80//! - `+180 deg` and `-180 deg` project to the same world X (validated by test).
81//! - Geodesic distance across the dateline follows the **short** path
82//! (e.g. 179 deg E to 179 deg W is approximately 220 km, not 39 800 km).
83//!
84//! ## 6. High-latitude behaviour
85//!
86//! - Web Mercator is valid only within approximately +/-85.051 129 deg latitude.
87//! - `project_clamped` saturates latitude to this limit.
88//! - `project_checked` returns `None` outside the limit.
89//! - Scale factor diverges as `sec(lat)`: at 85 deg it is approximately 11.5.
90//! - Equirectangular handles poles (+/-90 deg) without singularity.
91//!
92//! ## 7. Large-world numeric stability
93//!
94//! - f64 arithmetic preserves centimetre-scale deltas at the Mercator
95//! extent boundary (approximately 20 million metres).
96//! - Camera-relative f32 vertices preserve sub-centimetre precision
97//! for geometry within approximately 100 km of the camera target.
98//! - At zoom 14 (approximately 10 m per tile pixel), on-screen tile
99//! corners have camera-relative f32 error < 1 cm (validated by test).
100//!
101//! ## 8. Terrain elevation
102//!
103//! - `ElevationGrid` stores heights in f32 metres. The full Earth
104//! elevation range (-11 034 m to +8 848 m) survives the f32 cast
105//! with < 1 cm error.
106//! - Bilinear interpolation produces correct intermediate values.
107//! - Altitude (`GeoCoord::alt`) passes through projections unchanged.
108//! - `TerrainConfig::vertical_exaggeration` scales elevation in the
109//! GPU shader, not in the engine grid, preserving raw accuracy.
110//!
111//! ## 9. Scale factor
112//!
113//! - Web Mercator: `sec(lat)` -- unity at the equator, 2.0 at 60 deg.
114//! - Equirectangular: `sec(lat)` along X, 1.0 along Y.
115//! - `Camera::meters_per_pixel()` uses the scale factor to convert
116//! screen resolution to ground resolution.
117//!
118//! ## 10. Tile coordinate contract
119//!
120//! - `geo_to_tile` / `tile_to_geo` round-trip at all zoom levels.
121//! - Adjacent tiles share edges with < 1 mm world-space gap.
122//! - `tile_bounds_world` at zoom 0 spans the full Mercator world size.
123//! - `TileId::quadkey()` / `TileId::from_quadkey()` round-trip.
124//! - Parent / child relationships are consistent.
125//!
126//! ## 11. Renderer parity contract
127//!
128//! Both WGPU and Bevy renderers consume the same engine outputs:
129//!
130//! | Engine output | Consumed by |
131//! |---------------|-------------|
132//! | `camera.target_world()` | Scene origin subtraction |
133//! | `camera.eye_offset()` | Camera placement |
134//! | `camera.view_matrix(DVec3::ZERO)` | View matrix (camera-relative) |
135//! | `camera.projection_matrix()` | Projection matrix |
136//! | `tile_bounds_world` + UV mapping | Tile quad vertices |
137//! | `ElevationGrid` | Terrain GPU texture (R32Float) |
138//! | `VectorMeshData` positions | Vector vertex buffers |
139//! | `ModelInstance` position + altitude | Model transform buffers |
140//!
141//! Because both renderers apply camera-relative subtraction in f64
142//! before the f32 cast, the precision guarantees are identical.
143//!
144//! # Meter-Based Precision Invariants
145//!
146//! This module documents the precision invariants that Rustial maintains
147//! across the engine and both renderer paths (WGPU and Bevy). These
148//! invariants are validated by tests in
149//! [`support_contract`](crate::support_contract) and by headless
150//! regression tests in the renderer crates.
151//!
152//! ## 1. Internal units are meters
153//!
154//! All internal linear distances, world-space coordinates, and
155//! elevation values in the engine are expressed in **meters**.
156//!
157//! | Type | Unit |
158//! |------|------|
159//! | [`WorldCoord`](rustial_math::WorldCoord) | meters (east, north, up) |
160//! | [`GeoCoord::alt`](rustial_math::GeoCoord) | meters above WGS-84 ellipsoid |
161//! | [`ElevationGrid`](rustial_math::ElevationGrid) samples | meters |
162//! | [`Camera::distance()`](crate::Camera) | meters |
163//! | [`Camera::meters_per_pixel()`](crate::Camera) | meters per screen pixel |
164//! | Tile bounds ([`tile_bounds_world`](rustial_math::tile_bounds_world)) | meters |
165//! | Geodesic distances ([`geodesic_distance`](rustial_math::geodesic_distance)) | meters |
166//!
167//! ## 2. f64 engine, f32 GPU
168//!
169//! The engine performs **all math in f64**. GPU-bound data is cast to
170//! f32 only at upload time. This ensures centimetre-scale precision
171//! even at world-edge coordinates (x ~ 20 million metres).
172//!
173//! ## 3. Camera-relative rendering
174//!
175//! Both renderers use **camera-relative** vertex positions to avoid
176//! catastrophic f32 precision loss at large world coordinates:
177//!
178//! ```text
179//! GPU vertex position = world_position - camera_target_world()
180//! ```
181//!
182//! Because the subtraction happens in f64 *before* the cast to f32,
183//! the resulting f32 values are small (within a few kilometres of the
184//! origin) and retain sub-centimetre precision.
185//!
186//! ### Why this is necessary
187//!
188//! At longitude ~180 degrees, the absolute Mercator X coordinate is
189//! approximately 20 million metres. A 32-bit float at that magnitude
190//! has an epsilon of approximately 2 metres -- sub-metre detail would
191//! be lost. Camera-relative rendering eliminates this problem.
192//!
193//! ### What is subtracted
194//!
195//! | Renderer | Origin |
196//! |----------|--------|
197//! | WGPU | `MapState::scene_world_origin()` (= `camera.target_world()`) |
198//! | Bevy | Same -- the Bevy camera is at `eye_offset()`, tiles at `(world - origin)` |
199//!
200//! ## 4. Projection round-trip guarantees
201//!
202//! For **stable** (v1.0) projections, the following round-trip invariant
203//! holds at all valid inputs:
204//!
205//! ```text
206//! let world = projection.project(&geo);
207//! let back = projection.unproject(&world);
208//! assert!((back.lat - geo.lat).abs() < 1e-8);
209//! assert!((back.lon - geo.lon).abs() < 1e-8);
210//! ```
211//!
212//! | Projection | Latitude accuracy | Longitude accuracy | Altitude |
213//! |------------|------------------|--------------------|----------|
214//! | Web Mercator | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
215//! | Equirectangular | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
216//! | Globe | < 1e-6 deg | < 1e-6 deg | < 1 m |
217//! | Vertical Perspective | < 1e-5 deg | < 1e-5 deg | best-effort |
218//!
219//! ## 5. Anti-meridian handling
220//!
221//! - [`GeoCoord::clamped_mercator()`](rustial_math::GeoCoord::clamped_mercator)
222//! wraps longitude to `[-180, 180]` via modular arithmetic.
223//! - `+180 deg` and `-180 deg` project to the same world X (validated by test).
224//! - Geodesic distance across the dateline follows the **short** path
225//! (e.g. 179 deg E to 179 deg W is approximately 220 km, not 39 800 km).
226//!
227//! ## 6. High-latitude behaviour
228//!
229//! - Web Mercator is valid only within approximately +/-85.051 129 deg latitude.
230//! - `project_clamped` saturates latitude to this limit.
231//! - `project_checked` returns `None` outside the limit.
232//! - Scale factor diverges as `sec(lat)`: at 85 deg it is approximately 11.5.
233//! - Equirectangular handles poles (+/-90 deg) without singularity.
234//!
235//! ## 7. Large-world numeric stability
236//!
237//! - f64 arithmetic preserves centimetre-scale deltas at the Mercator
238//! extent boundary (approximately 20 million metres).
239//! - Camera-relative f32 vertices preserve sub-centimetre precision
240//! for geometry within approximately 100 km of the camera target.
241//! - At zoom 14 (approximately 10 m per tile pixel), on-screen tile
242//! corners have camera-relative f32 error < 1 cm (validated by test).
243//!
244//! ## 8. Terrain elevation
245//!
246//! - `ElevationGrid` stores heights in f32 metres. The full Earth
247//! elevation range (-11 034 m to +8 848 m) survives the f32 cast
248//! with < 1 cm error.
249//! - Bilinear interpolation produces correct intermediate values.
250//! - Altitude (`GeoCoord::alt`) passes through projections unchanged.
251//! - `TerrainConfig::vertical_exaggeration` scales elevation in the
252//! GPU shader, not in the engine grid, preserving raw accuracy.
253//!
254//! ## 9. Scale factor
255//!
256//! - Web Mercator: `sec(lat)` -- unity at the equator, 2.0 at 60 deg.
257//! - Equirectangular: `sec(lat)` along X, 1.0 along Y.
258//! - `Camera::meters_per_pixel()` uses the scale factor to convert
259//! screen resolution to ground resolution.
260//!
261//! ## 10. Tile coordinate contract
262//!
263//! - `geo_to_tile` / `tile_to_geo` round-trip at all zoom levels.
264//! - Adjacent tiles share edges with < 1 mm world-space gap.
265//! - `tile_bounds_world` at zoom 0 spans the full Mercator world size.
266//! - `TileId::quadkey()` / `TileId::from_quadkey()` round-trip.
267//! - Parent / child relationships are consistent.
268//!
269//! ## 11. Renderer parity contract
270//!
271//! Both WGPU and Bevy renderers consume the same engine outputs:
272//!
273//! | Engine output | Consumed by |
274//! |---------------|-------------|
275//! | `camera.target_world()` | Scene origin subtraction |
276//! | `camera.eye_offset()` | Camera placement |
277//! | `camera.view_matrix(DVec3::ZERO)` | View matrix (camera-relative) |
278//! | `camera.projection_matrix()` | Projection matrix |
279//! | `tile_bounds_world` + UV mapping | Tile quad vertices |
280//! | `ElevationGrid` | Terrain GPU texture (R32Float) |
281//! | `VectorMeshData` positions | Vector vertex buffers |
282//! | `ModelInstance` position + altitude | Model transform buffers |
283//!
284//! Because both renderers apply camera-relative subtraction in f64
285//! before the f32 cast, the precision guarantees are identical.
286//!
287//! # Meter-Based Precision Invariants
288//!
289//! This module documents the precision invariants that Rustial maintains
290//! across the engine and both renderer paths (WGPU and Bevy). These
291//! invariants are validated by tests in
292//! [`support_contract`](crate::support_contract) and by headless
293//! regression tests in the renderer crates.
294//!
295//! ## 1. Internal units are meters
296//!
297//! All internal linear distances, world-space coordinates, and
298//! elevation values in the engine are expressed in **meters**.
299//!
300//! | Type | Unit |
301//! |------|------|
302//! | [`WorldCoord`](rustial_math::WorldCoord) | meters (east, north, up) |
303//! | [`GeoCoord::alt`](rustial_math::GeoCoord) | meters above WGS-84 ellipsoid |
304//! | [`ElevationGrid`](rustial_math::ElevationGrid) samples | meters |
305//! | [`Camera::distance()`](crate::Camera) | meters |
306//! | [`Camera::meters_per_pixel()`](crate::Camera) | meters per screen pixel |
307//! | Tile bounds ([`tile_bounds_world`](rustial_math::tile_bounds_world)) | meters |
308//! | Geodesic distances ([`geodesic_distance`](rustial_math::geodesic_distance)) | meters |
309//!
310//! ## 2. f64 engine, f32 GPU
311//!
312//! The engine performs **all math in f64**. GPU-bound data is cast to
313//! f32 only at upload time. This ensures centimetre-scale precision
314//! even at world-edge coordinates (x ~ 20 million metres).
315//!
316//! ## 3. Camera-relative rendering
317//!
318//! Both renderers use **camera-relative** vertex positions to avoid
319//! catastrophic f32 precision loss at large world coordinates:
320//!
321//! ```text
322//! GPU vertex position = world_position - camera_target_world()
323//! ```
324//!
325//! Because the subtraction happens in f64 *before* the cast to f32,
326//! the resulting f32 values are small (within a few kilometres of the
327//! origin) and retain sub-centimetre precision.
328//!
329//! ### Why this is necessary
330//!
331//! At longitude ~180 degrees, the absolute Mercator X coordinate is
332//! approximately 20 million metres. A 32-bit float at that magnitude
333//! has an epsilon of approximately 2 metres -- sub-metre detail would
334//! be lost. Camera-relative rendering eliminates this problem.
335//!
336//! ### What is subtracted
337//!
338//! | Renderer | Origin |
339//! |----------|--------|
340//! | WGPU | `MapState::scene_world_origin()` (= `camera.target_world()`) |
341//! | Bevy | Same -- the Bevy camera is at `eye_offset()`, tiles at `(world - origin)` |
342//!
343//! ## 4. Projection round-trip guarantees
344//!
345//! For **stable** (v1.0) projections, the following round-trip invariant
346//! holds at all valid inputs:
347//!
348//! ```text
349//! let world = projection.project(&geo);
350//! let back = projection.unproject(&world);
351//! assert!((back.lat - geo.lat).abs() < 1e-8);
352//! assert!((back.lon - geo.lon).abs() < 1e-8);
353//! ```
354//!
355//! | Projection | Latitude accuracy | Longitude accuracy | Altitude |
356//! |------------|------------------|--------------------|----------|
357//! | Web Mercator | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
358//! | Equirectangular | < 1e-8 deg | < 1e-8 deg | passthrough (exact) |
359//! | Globe | < 1e-6 deg | < 1e-6 deg | < 1 m |
360//! | Vertical Perspective | < 1e-5 deg | < 1e-5 deg | best-effort |
361//!
362//! ## 5. Anti-meridian handling
363//!
364//! - [`GeoCoord::clamped_mercator()`](rustial_math::GeoCoord::clamped_mercator)
365//! wraps longitude to `[-180, 180]` via modular arithmetic.
366//! - `+180 deg` and `-180 deg` project to the same world X (validated by test).
367//! - Geodesic distance across the dateline follows the **short** path
368//! (e.g. 179 deg E to 179 deg W is approximately 220 km, not 39 800 km).
369//!
370//! ## 6. High-latitude behaviour
371//!
372//! - Web Mercator is valid only within approximately +/-85.051 129 deg latitude.
373//! - `project_clamped` saturates latitude to this limit.
374//! - `project_checked` returns `None` outside the limit.
375//! - Scale factor diverges as `sec(lat)`: at 85 deg it is approximately 11.5.
376//! - Equirectangular handles poles (+/-90 deg) without singularity.
377//!
378//! ## 7. Large-world numeric stability
379//!
380//! - f64 arithmetic preserves centimetre-scale deltas at the Mercator
381//! extent boundary (approximately 20 million metres).
382//! - Camera-relative f32 vertices preserve sub-centimetre precision
383//! for geometry within approximately 100 km of the camera target.
384//! - At zoom 14 (approximately 10 m per tile pixel), on-screen tile
385//! corners have camera-relative f32 error < 1 cm (validated by test).
386//!
387//! ## 8. Terrain elevation
388//!
389//! - `ElevationGrid` stores heights in f32 metres. The full Earth
390//! elevation range (-11 034 m to +8 848 m) survives the f32 cast
391//! with < 1 cm error.
392//! - Bilinear interpolation produces correct intermediate values.
393//! - Altitude (`GeoCoord::alt`) passes through projections unchanged.
394//! - `TerrainConfig::vertical_exaggeration` scales elevation in the
395//! GPU shader, not in the engine grid, preserving raw accuracy.
396//!
397//! ## 9. Scale factor
398//!
399//! - Web Mercator: `sec(lat)` -- unity at the equator, 2.0 at 60 deg.
400//! - Equirectangular: `sec(lat)` along X, 1.0 along Y.
401//! - `Camera::meters_per_pixel()` uses the scale factor to convert
402//! screen resolution to ground resolution.
403//!
404//! ## 10. Tile coordinate contract
405//!
406//! - `geo_to_tile` / `tile_to_geo` round-trip at all zoom levels.
407//! - Adjacent tiles share edges with < 1 mm world-space gap.
408//! - `tile_bounds_world` at zoom 0 spans the full Mercator world size.
409//! - `TileId::quadkey()` / `TileId::from_quadkey()` round-trip.
410//! - Parent / child relationships are consistent.
411//!
412//! ## 11. Renderer parity contract
413//!
414//! Both WGPU and Bevy renderers consume the same engine outputs:
415//!
416//! | Engine output | Consumed by |
417//! |---------------|-------------|
418//! | `camera.target_world()` | Scene origin subtraction |
419//! | `camera.eye_offset()` | Camera placement |
420//! | `camera.view_matrix(DVec3::ZERO)` | View matrix (camera-relative) |
421//! | `camera.projection_matrix()` | Projection matrix |
422//! | `tile_bounds_world` + UV mapping | Tile quad vertices |
423//! | `ElevationGrid` | Terrain GPU texture (R32Float) |
424//! | `VectorMeshData` positions | Vector vertex buffers |
425//! | `ModelInstance` position + altitude | Model transform buffers |
426//!
427//! Because both renderers apply camera-relative subtraction in f64
428//! before the f32 cast, the precision guarantees are identical.
429
430// This module is documentation-only; it contains no runtime code.
431// The invariants documented here are validated by tests in
432// `crates/rustial-engine/src/support_contract.rs`.