Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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);
}