facett-map3d/tests/frustum_scissor.rs
//! **Frustum + scissor PROOF** — the OSM 3D view must (1) bound its perspective
//! frustum so distant geometry never smears to the horizon, and (2) confine every
//! painted pixel to its allocated egui `Rect` (no ink bleeds onto the rest of the
//! UI).
//!
//! Built the **real demo path** (`from_ways` with a handful of buildings — NO logo
//! overlay, which is a separate concern), at a **low, dollied-in street-level
//! camera** — the exact pose that used to make the terrain far edge + off-axis
//! buildings project thousands of pixels off-centre and stretch to eternity.
//!
//! Two assertions, fail-on-bug / pass-on-fix:
//! 1. **Bounded frustum (pure math):** every projected vertex `shade_faces` emits
//! has a camera-space depth inside `[near, far_plane]` — the far cull drops the
//! to-eternity terrain edge — AND no emitted triangle spans more than a few
//! viewports (the near-singularity spoke reject holds).
//! 2. **No ink outside the rect (headless render):** the view is allocated a
//! *sub-rectangle* of a larger canvas; after rendering, every pixel in the
//! surrounding margin band is the (untouched) canvas background — nothing the
//! 3D pass drew escaped its own rect.
//!
//! Run: `cargo test -p facett-map3d --test frustum_scissor`
use egui::{Color32, Pos2};
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::{Map3D, V3, ViewPreset};
/// A closed building ring → a real OSM-style filled `Way` (the demo's prism path).
fn building(id: i64, ring: &[(f64, f64)]) -> Way {
let tags = vec![("building".to_string(), "yes".to_string())];
Way::from_lonlat(id, ring, &tags, &Theme::default()).expect("a building way")
}
/// A small town of real buildings spread across the design box — the `from_ways`
/// path the demo's OSM 3D tab actually drives (no logo overlay).
fn town() -> Vec<Way> {
let mut ways = Vec::new();
let mut id = 1;
for r in 0..4 {
for c in 0..4 {
let bx = 9.50 + c as f64 * 0.002;
let by = 47.10 + r as f64 * 0.002;
ways.push(building(
id,
&[(bx, by), (bx + 0.0008, by), (bx + 0.0008, by + 0.0008), (bx, by + 0.0008), (bx, by)],
));
id += 1;
}
}
ways
}
/// Put the camera at the worst-case low, dollied-in street-level pose — the angle
/// that used to smear the terrain edge to the horizon and fling off-axis buildings
/// thousands of pixels off-screen.
fn street_level(m: &mut Map3D) {
m.spin = false;
m.cam.target = V3::new(0.0, 0.0, 0.0);
m.cam.goal_target = m.cam.target;
m.cam.distance = 0.5;
m.cam.goal_distance = 0.5;
m.view_preset(ViewPreset::Front);
m.cam.elevation = 0.02;
m.cam.goal_elevation = 0.02;
m.cam.settle();
}
/// (1) The frustum is **bounded**: nothing `shade_faces` emits sits past the
/// camera far plane, and no emitted triangle spans an unbounded screen extent.
#[test]
fn frustum_is_bounded_no_smear_to_eternity() {
let mut m = Map3D::from_ways("bounded", &town(), 0.22);
street_level(&mut m);
let center = Pos2::new(360.0, 360.0);
let half_h = 360.0_f32; // viewport ≈ 720×720
let faces = m.shade_faces(center, half_h, Color32::WHITE);
assert!(!faces.is_empty(), "the lit city still draws at street level");
// No emitted triangle spans more than the near-singularity ceiling (a real
// on-screen face never exceeds a few viewports — the spoke reject holds).
for (_d, pts, _c) in &faces {
let ext_x = pts.iter().map(|p| p.x).fold(f32::MIN, f32::max) - pts.iter().map(|p| p.x).fold(f32::MAX, f32::min);
let ext_y = pts.iter().map(|p| p.y).fold(f32::MIN, f32::max) - pts.iter().map(|p| p.y).fold(f32::MAX, f32::min);
assert!(
ext_x <= half_h * 2.0 + 1.0 && ext_y <= half_h * 2.0 + 1.0,
"no triangle exceeds the VIEWPORT (extent {ext_x:.0}×{ext_y:.0} px, cap {:.0}) — \
was a legacy half_h·6 multiple, i.e. a cap that could not fail",
half_h * 2.0 + 1.0
);
}
// The far plane actually culls: a vertex placed well beyond it is invisible,
// and one comfortably inside it projects. (Direct probe of the bound.)
let cam = m.render_camera();
let eye = cam.eye();
let (fwd, _r, _u) = cam.basis();
let far = cam.far_plane();
let beyond = eye.add(fwd.scale(far + 5.0));
let inside = eye.add(fwd.scale(far * 0.5));
assert!(!cam.project(beyond, (360.0, 360.0), half_h).visible, "geometry past the far plane is culled");
assert!(cam.project(inside, (360.0, 360.0), half_h).visible, "geometry inside the frustum still draws");
}
/// (2) **No ink outside the rect**: allocate the OSM 3D view a sub-rectangle of a
/// larger canvas painted a known background, render headless, and assert the
/// surrounding margin band is still the untouched background — the 3D pass scissors
/// to its own rect.
#[test]
fn paints_no_ink_outside_its_allocated_rect() {
use egui_kittest::Harness;
// A canvas bigger than the widget, painted a vivid sentinel colour so any
// leaked 3D-pass pixel (dark prisms / grey terrain) is unmistakable against it.
const CANVAS: f32 = 600.0;
const MARGIN: f32 = 90.0; // the band around the widget that must stay untouched
let sentinel = Color32::from_rgb(255, 0, 255); // magenta — the map never paints this
// Render the SAME canvas twice — once with the 3D view drawn into its inset
// sub-rect, once WITHOUT it — and diff. Any pixel outside the widget rect that
// changed between the two = ink the 3D pass leaked. This is renderer- and
// window-padding-independent (no assumption the whole framebuffer is painted).
let render = |draw_view: bool| -> image::RgbaImage {
let mut view = Map3D::from_ways("scissor", &town(), 0.22);
street_level(&mut view);
let mut harness = Harness::builder().with_size(egui::vec2(CANVAS, CANVAS)).build_ui(move |ui| {
let full = ui.max_rect();
// Sentinel-fill the widget's would-be rect so the baseline + the drawn
// frame share an identical background everywhere outside it.
let inner = full.shrink(MARGIN);
ui.painter().rect_filled(inner, egui::CornerRadius::ZERO, sentinel);
if draw_view {
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(inner));
facett_core::Facet::ui(&mut view, &mut child);
}
});
harness.run_steps(3);
harness.render().expect("headless render produced an image")
};
let drawn = render(true);
let baseline = render(false);
assert_eq!(drawn.dimensions(), baseline.dimensions(), "same framebuffer size");
let ppp = 1.0_f32; // CANVAS == framebuffer (ppp 1 in the headless harness)
let (w, h) = drawn.dimensions();
// The widget's pixel rect (the inset sub-rect). Anything strictly outside it
// must be byte-identical to the baseline — the 3D pass touched nothing there.
let m_px = (MARGIN * ppp).round() as u32;
let mut leaked = 0u64;
let mut band = 0u64;
for y in 0..h {
for x in 0..w {
let outside = x < m_px || x >= w.saturating_sub(m_px) || y < m_px || y >= h.saturating_sub(m_px);
if !outside {
continue;
}
band += 1;
if drawn.get_pixel(x, y) != baseline.get_pixel(x, y) {
leaked += 1;
}
}
}
assert!(band > 0, "there is a margin band to check");
let frac = leaked as f64 / band as f64;
eprintln!("[scissor] margin band {band} px, changed-vs-baseline {leaked} ({:.4}%)", frac * 100.0);
// A perfect scissor changes ZERO pixels outside the rect; allow only a hair of
// anti-aliased feathering exactly on the clip boundary (< 0.05% of the band).
assert!(frac < 0.0005, "the 3D pass drew {leaked} px ({:.3}%) OUTSIDE its rect", frac * 100.0);
}