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/chain_sweep.rs

//! **CHAIN SWEEP (diagnostic phase)** — drive a GIANT known fixture through the
//! projection chain across 360° azimuth × 100 zoom levels × several elevations,
//! measuring the RAW per-stage geometry (f1 view_space → f2 clip_tri_near →
//! f3 project_view) to find WHERE the smear is born. No 1-value tests — the whole
//! rotation/zoom space, per the LAW (input coverage × oracle exactness).
//!
//! Run: `cargo test -p facett-map3d --test chain_sweep -- --nocapture`

use egui::{Color32, Pos2};
use facett_map::Way;
use facett_core::Theme;
use facett_map3d::{clip_tri_frustum, Map3D, OrbitCamera, V3};

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 GIANT self-calculated fixture: a 20×20 block city (400 footprints) so the
/// extent is large enough to straddle the near plane at street-level poses.
fn giant_city() -> Map3D {
    let mut ways = Vec::new();
    let mut id = 1;
    for r in 0..20 {
        for c in 0..20 {
            let bx = 17.70 + c as f64 * 0.0010;
            let by = 59.61 + r as f64 * 0.0010;
            ways.push(building(
                id,
                &[(bx, by), (bx + 0.0006, by), (bx + 0.0006, by + 0.0006), (bx, by + 0.0006), (bx, by)],
            ));
            id += 1;
        }
    }
    Map3D::from_geometry("giant", &ways)
}

// IGNORED BY DEFAULT: this diagnostic sweep drives 360° × 100 zoom × several
// elevations through the full projection chain and runs >10 min at ~99% CPU,
// serializing the whole suite. It is kept fully runnable on demand — opt in with
// `cargo test -p facett-map3d --test chain_sweep -- --ignored --nocapture`.
#[test]
#[ignore = "slow diagnostic sweep (>10 min, ~99% CPU); run explicitly with --ignored"]
fn sweep_360_x_100_zoom_find_raw_projection_blowup() {
    let canvas = (600.0f32, 600.0f32);
    let center = (canvas.0 * 0.5, canvas.1 * 0.5);
    let half_h = canvas.1 * 0.5;
    let near = OrbitCamera::NEAR_PLANE;
    // The viewport diagonal is the natural bound: a correct projected triangle edge
    // never needs to exceed it (geometry off-screen is clipped, not stretched).
    let viewport_diag = (canvas.0 * canvas.0 + canvas.1 * canvas.1).sqrt();

    type Fixture = (&'static str, fn() -> Map3D);
    let fixtures: &[Fixture] =
        &[("rabbit", || Map3D::synthetic("rabbit")), ("giant_city", giant_city)];

    for (fname, build) in fixtures {
        // Build the mesh ONCE — it is pose-invariant; only the camera changes.
        let mut m = build();
        m.spin = false;
        let tgt = m.render_camera().target;
        let positions: Vec<[f32; 3]> = m.mesh().positions.clone();
        let indices: Vec<u32> = m.mesh().indices.clone();
        // distance band: from very close (street level) to far, 100 steps.
        let dmin = 0.05f32;
        let dmax = 8.0f32;

        let mut worst_edge = 0.0f32;
        let mut worst_case = (0.0f32, 0.0f32, 0.0f32);
        let mut blowup_cases = 0u32;
        let mut total = 0u32;

        for ai in 0..360 {
            let az = (ai as f32).to_radians();
            for zi in 0..100 {
                let dist = dmin + (dmax - dmin) * (zi as f32 / 99.0);
                for &elev_deg in &[5.0f32, 25.0, 55.0, 80.0] {
                    total += 1;
                    m.cam.target = tgt;
                    m.cam.goal_target = tgt;
                    m.cam.azimuth = az;
                    m.cam.goal_azimuth = az;
                    m.cam.elevation = elev_deg.to_radians();
                    m.cam.goal_elevation = elev_deg.to_radians();
                    m.cam.distance = dist;
                    m.cam.goal_distance = dist;
                    m.cam.settle();
                    let cam = m.render_camera();

                    // f1: view_space all verts
                    let view: Vec<V3> = positions
                        .iter()
                        .map(|&p| cam.view_space(V3::new(p[0], p[1], p[2])))
                        .collect();

                    // f2 + f3 per triangle: clip_tri_near, then project the survivors;
                    // measure the longest projected edge (the spoke signal).
                    let idx = &indices;
                    let mut case_edge = 0.0f32;
                    for t in 0..idx.len() / 3 {
                        let tri = [
                            view[idx[t * 3] as usize],
                            view[idx[t * 3 + 1] as usize],
                            view[idx[t * 3 + 2] as usize],
                        ];
                        // FULL frustum clip (near + 4 sides) — slope = tan(fov_y/2),
                        // matching project_view's focal, so survivors project inside
                        // the viewport by construction. THE FIX.
                        let fslope = (cam.fov_y * 0.5).tan().max(1e-4);
                        let (poly, _crossed) = clip_tri_frustum(&tri, near, fslope, fslope);
                        if poly.len() < 3 {
                            continue;
                        }
                        let sp: Vec<Pos2> = poly
                            .iter()
                            .map(|v| {
                                let pr = cam.project_view(*v, center, half_h);
                                Pos2::new(pr.x, pr.y)
                            })
                            .collect();
                        // f2 oracle: every surviving view-space vertex is in front of near.
                        assert!(
                            poly.iter().all(|v| v.z >= near - 1e-3),
                            "[{fname}] clip_tri_near left a behind-eye vertex at az={ai}° elev={elev_deg} d={dist:.3}"
                        );
                        // f3 oracle: projection is finite.
                        for s in &sp {
                            assert!(
                                s.x.is_finite() && s.y.is_finite(),
                                "[{fname}] non-finite projection at az={ai}° elev={elev_deg} d={dist:.3}"
                            );
                        }
                        // longest projected edge in this (clipped) polygon
                        for i in 0..sp.len() {
                            for j in i + 1..sp.len() {
                                let e = sp[i].distance(sp[j]);
                                case_edge = case_edge.max(e);
                            }
                        }
                    }
                    if case_edge > viewport_diag * 1.5 {
                        blowup_cases += 1;
                    }
                    if case_edge > worst_edge {
                        worst_edge = case_edge;
                        worst_case = (ai as f32, elev_deg, dist);
                    }
                }
            }
        }
        eprintln!(
            "[{fname}] cases={total} viewport_diag={viewport_diag:.0}px  WORST projected edge={worst_edge:.0}px at az={:.0}° elev={:.0}° dist={:.3}  blowups(>1.5×diag)={blowup_cases}",
            worst_case.0, worst_case.1, worst_case.2
        );
        let _ = (Color32::WHITE,);
        // THE FIX, proven across the WHOLE 360°×100-zoom×elev joint space: the full
        // frustum clip bounds every projected edge to the viewport (was up to
        // 84 000px / 99× viewport with the near-only clip, at ~21% of poses). Tight
        // oracle — the viewport itself, NOT a loose multiple of it.
        assert!(
            worst_edge <= viewport_diag * 1.05,
            "[{fname}] frustum clip did NOT bound projection: worst edge {worst_edge:.0}px > viewport {viewport_diag:.0}px (smear) at az={:.0}° elev={:.0}° dist={:.3}",
            worst_case.0, worst_case.1, worst_case.2
        );
        assert_eq!(blowup_cases, 0, "[{fname}] {blowup_cases} poses still blow up past 1.5× viewport");
    }
}