facett-map3d/tests/water_follows_the_ring.rs
//! **A lake's 3D boundary must be the ring, not a grid of squares** — lane MP-b,
//! The owner 2026-09-05 (live t14s): *"in 3D, zoomed in, lakes are Minecraft-squared."*
//!
//! # The brief's diagnosis was wrong, and this is what is actually happening
//!
//! The report guessed the 3D lane *"extrudes from the DP-simplified rung geometry and
//! never swaps to the full-precision level the 2D one-paint rule swaps to at rest."*
//! Measured instead:
//!
//! * the 3D lane is fed **the same ways the 2D facet holds** (`rebuild_map3d`: *"the SAME
//! ways the flat 2D facet draws (reuse, not a twin)"*), and
//! * `self.map3d = None` on every geometry change, so it DOES re-extrude when a sharper
//! level lands.
//!
//! The squares come from the mesher. `water_grid_into` builds a grid over the ring's
//! **bounding box** and, in its own words, emits *"a quad (2 tris) per cell whose CENTRE
//! is inside the polygon"*. The ring's vertices are never used as geometry — only as an
//! inside/outside test for whole cells. **So the boundary is quantised to the grid at any
//! source precision**, and handing it the sharpest level would not round a single edge.
//!
//! That changes the cure: tessellate the RING (the fill lane already ear-clips) and
//! displace those vertices for the wave, instead of stencilling a bbox grid.
//!
//! | gate | red arm |
//! |---|---|
//! | a diagonal shore produces boundary vertices ON the ring, not on a lattice | keep the grid mesher |
//! | a circular lake's silhouette is not axis-aligned | — |
//!
//! ```bash
//! cargo test -p facett-map3d --test water_follows_the_ring
//! ```
use facett_map3d::extrude::{water_grid_into, WaterMesh};
/// A regular polygon ring, closed, in design space.
fn circle(n: usize, r: f32) -> Vec<(f32, f32)> {
let mut v: Vec<(f32, f32)> = (0..n)
.map(|i| {
let a = std::f32::consts::TAU * (i as f32) / (n as f32);
(r * a.cos(), r * a.sin())
})
.collect();
v.push(v[0]);
v
}
/// **The lake's SILHOUETTE must lie on the ring.**
///
/// The first version of this gate measured "vertices at radius > 0.80 R" and called them
/// the shore. That was wrong and it flattered the fix: interior wave-sample points near
/// the edge are legitimately not on the ring, so the metric could never reach 100 % and
/// the number it did report (18 % before, 50 % after) said nothing precise about the
/// thing the owner can see.
///
/// What he sees is the **silhouette**, and a mesh's silhouette is its **boundary edges** —
/// the edges used by exactly ONE triangle. Those, and only those, are the shore. A bbox
/// grid stencilled by cell-centre containment has boundary edges on the lattice; a
/// ring triangulation has them on the ring.
#[test]
fn a_round_lake_has_a_round_edge() {
const R: f32 = 1.0;
let ring = circle(64, R);
let mut mesh = WaterMesh::default();
water_grid_into(&mut mesh, &ring, 16, 0.0);
assert!(!mesh.indices.is_empty(), "the mesher produced nothing to judge");
// Boundary edges: used by exactly one triangle.
let mut seen: std::collections::HashMap<(u32, u32), usize> = std::collections::HashMap::new();
for t in mesh.indices.as_chunks::<3>().0 {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*seen.entry((a.min(b), a.max(b))).or_default() += 1;
}
}
let boundary: Vec<(u32, u32)> =
seen.iter().filter(|kv| *kv.1 == 1).map(|kv| *kv.0).collect();
assert!(!boundary.is_empty(), "a closed mesh with no boundary is not a lake");
let radius = |i: u32| {
let p = mesh.positions[i as usize];
(p[0] * p[0] + p[2] * p[2]).sqrt()
};
let off: Vec<f32> = boundary
.iter()
.flat_map(|&(a, b)| [radius(a), radius(b)])
.filter(|r| (r - R).abs() > R * 0.02)
.collect();
assert!(
off.is_empty(),
"every SILHOUETTE vertex must lie on the ring: {} of {} boundary endpoints are \
off it (worst radius {:?}). A bbox grid stencilled by cell-centre containment \
puts the shore on a lattice at ANY source precision — which is why handing the \
3D lane a sharper level would not round a single edge.",
off.len(),
boundary.len() * 2,
off.iter().cloned().fold(0.0f32, f32::max)
);
}