facett-map3d/tests/no_spokes.rs
//! Regression (test-matrix): the perspective renderer must NEVER output a "spoke"
//! face — a triangle projected to a screen extent far larger than the viewport.
//!
//! Root cause it guards: a vertex just past the near plane (cz≈near⁺) with a large
//! off-axis offset divides to a wild off-screen coord, drawing a line shooting
//! across the frame (the "starburst" the terrain quad's far edge produced at low
//! camera angles). The synthetic-only `render_proof` never hit this; this sweeps
//! the spoke-prone angles (Front = horizon-level especially) and asserts via the
//! `shade_faces` DATA — no pixels.
//!
//! FAILS on the pre-fix renderer (the spoke face is emitted, extent ~1e5 px);
//! PASSES once `shade_faces` rejects faces wider than 8× the viewport half-height.
use egui::{Color32, Pos2};
use facett_map3d::{Map3D, ViewPreset};
#[test]
fn camera_angles_never_project_spoke_faces() {
let half_h = 360.0f32;
// VIEWPORT-TIGHT cap (was `half_h * 8.0` — the canonical loose multiple that
// let an 8×-viewport smear pass green, see .nornir/agent-ui-test-fixing.md).
// The contract: `shade_faces` clips every projected face to the screen-bound
// box `±SCREEN_BOUND_K·half_h` with `SCREEN_BOUND_K = 1.0`, so a fully-clipped
// face spans AT MOST the viewport (2·half_h = 720px here); +1px float-boundary
// epsilon so an exactly-bound face is not a false smear. Measured worst over
// this 20-pose sweep: 704px.
let cap = half_h * 2.0 + 1.0;
let center = Pos2::new(360.0, 360.0);
let mut worst = 0f32;
let mut total_faces = 0usize;
// (a) fixed presets — Front is horizon-level, the pose that straddled the
// near plane; Oblique/Iso/Top cover the rest of the elevation range.
let presets = [
ViewPreset::Front,
ViewPreset::Oblique,
ViewPreset::Isometric,
ViewPreset::Top,
];
for p in presets {
let mut m = Map3D::synthetic("spoke-sweep").with_clock(0.0);
m.view_preset(p);
let faces = m.shade_faces(center, half_h, Color32::GRAY);
total_faces += faces.len();
for (_, tri, _) in &faces {
let ex = tri[0].x.max(tri[1].x).max(tri[2].x) - tri[0].x.min(tri[1].x).min(tri[2].x);
let ey = tri[0].y.max(tri[1].y).max(tri[2].y) - tri[0].y.min(tri[1].y).min(tri[2].y);
worst = worst.max(ex.max(ey));
}
}
// (b) auto-orbit sweep — azimuth rotates with the clock at the default
// elevation, catching any spin angle that grazes the near plane.
for k in 0..16 {
let clock = k as f64 * 0.4;
let m = Map3D::synthetic("spoke-orbit").with_clock(clock);
let faces = m.shade_faces(center, half_h, Color32::GRAY);
total_faces += faces.len();
for (_, tri, _) in &faces {
let ex = tri[0].x.max(tri[1].x).max(tri[2].x) - tri[0].x.min(tri[1].x).min(tri[2].x);
let ey = tri[0].y.max(tri[1].y).max(tri[2].y) - tri[0].y.min(tri[1].y).min(tri[2].y);
worst = worst.max(ex.max(ey));
}
}
eprintln!("[no_spokes] worst face extent = {worst:.0} px (cap {cap:.0}), faces={total_faces}");
// Read only by the testmatrix row below.
#[cfg(feature = "testmatrix")]
let ok = worst < cap && total_faces > 0;
assert!(total_faces > 0, "the sweep rendered some faces");
assert!(worst < cap, "a face spans {worst:.0}px (> cap {cap:.0}) — the projection starburst regressed");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-map3d",
"no_projection_spokes",
ok,
&format!("worst_extent_px={worst:.0} cap_px={cap:.0} faces={total_faces}"),
);
}