facett-map3d/tests/partstep_sweep.rs
//! **MAP-TEST-SWEEP** — drive the FULL part-step render chain (near_clip → project
//! → cull → shade → depth_sort → scissor → composite) across the WHOLE camera pose
//! space: 360° azimuth × N zoom × M elevation. NOT one value into the black box —
//! you rotate 360° and zoom in many combinations and assert the oracle at EVERY
//! pose (the owner's 3D-test LAW, 2026-06-22).
//!
//! Where `chain_sweep.rs` measures only the RAW projection chain (f1→f2→f3) to find
//! WHERE a smear is born, this sweep runs the SHIPPING pipeline end-to-end
//! (`shade_faces_clip` → `scissor_report`) and proves the clip-bug oracle —
//! **`scissor.clipped_escapes == 0`** (nothing escapes the widget rect after the
//! scissor) — holds across the entire rotation/zoom grid, on a giant near-plane-
//! straddling city. Green matrix ⟺ every sub-part proven across the sweep.
//!
//! Run: cargo test -p facett-map3d --test partstep_sweep -- --nocapture
//! Heavy: cargo test -p facett-map3d --test partstep_sweep --features testmatrix -- --ignored
//! (the full 360°×100-zoom grid; the default arm runs a 36×25 grid)
use egui::{Color32, Pos2, Rect};
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::{clip_poly_to_rect, scissor_report, Map3D};
const CANVAS: f32 = 600.0;
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) whose
/// extent is large enough to straddle the near plane at street-level poses — the
/// exact condition that births the clip smear.
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)
}
/// The widget rect the scissor confines to (a centred square inside the canvas).
fn widget_rect() -> Rect {
Rect::from_min_size(Pos2::new(0.0, 0.0), egui::vec2(CANVAS, CANVAS))
}
/// One pose: set the camera, run the shipping chain, return the per-step oracle
/// readings `(n_faces, ink_outside_rect_px, clipped_escapes)`.
fn run_pose(m: &mut Map3D, az_deg: f32, elev_deg: f32, dist: f32) -> (usize, f32, usize) {
m.cam.azimuth = az_deg.to_radians();
m.cam.goal_azimuth = az_deg.to_radians();
m.cam.elevation = elev_deg.to_radians();
m.cam.goal_elevation = elev_deg.to_radians();
m.cam.distance = dist;
m.cam.goal_distance = dist;
let center = Pos2::new(CANVAS * 0.5, CANVAS * 0.5);
let scale = CANVAS * 0.5;
// near_clip = true → the shipping pipeline (near_clip → project → cull → shade,
// with the screen-extent bound), depth-ordered tuples back.
let mut faces = m.shade_faces_clip(center, scale, Color32::from_rgb(40, 80, 60), true);
// depth_sort part-step: order back-to-front by the tuple depth.
faces.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (ink, escapes) = scissor_report(&faces, widget_rect());
(faces.len(), ink, escapes)
}
/// The sweep core, parameterised by grid density so the default (fast) and heavy
/// (full 360×100) arms share one body.
fn sweep(az_step_deg: usize, zoom_steps: usize, label: &str) {
let elevations = [5.0f32, 25.0, 55.0, 80.0];
let dmin = 0.05f32;
let dmax = 8.0f32;
let rect = widget_rect();
type Fixture = (&'static str, fn() -> Map3D);
let fixtures: &[Fixture] =
&[("giant_city", giant_city), ("rabbit", || Map3D::synthetic("rabbit"))];
let mut poses = 0usize;
let mut poses_with_faces = 0usize;
let mut worst_ink = 0.0f32;
let mut worst_pose = (String::new(), 0f32, 0f32, 0f32);
let mut total_escapes = 0usize;
for (fname, build) in fixtures {
let mut m = build();
m.spin = false;
for ai in (0..360).step_by(az_step_deg) {
let az = ai as f32;
for zi in 0..zoom_steps {
let t = zi as f32 / (zoom_steps.max(2) - 1) as f32;
let dist = dmin + (dmax - dmin) * t;
for &elev in &elevations {
let (n_faces, ink, escapes) = run_pose(&mut m, az, elev, dist);
poses += 1;
if n_faces > 0 {
poses_with_faces += 1;
}
total_escapes += escapes;
if ink > worst_ink {
worst_ink = ink;
worst_pose = ((*fname).to_string(), az, elev, dist);
}
// THE ORACLE: after the scissor, NO vertex escapes the rect — at
// EVERY pose in the rotation/zoom grid. This is the clip bug,
// proven across the whole space, not one camera.
assert_eq!(
escapes, 0,
"[{label}/{fname}] {escapes} vertices escaped the rect after scissor \
at az={az}° elev={elev}° dist={dist:.3} (raw ink_outside_rect={ink:.1}px)"
);
// Projection stays finite across the whole grid (no smear-to-eternity).
assert!(
ink.is_finite(),
"[{label}/{fname}] non-finite overshoot at az={az}° elev={elev}° dist={dist:.3}"
);
}
}
}
}
// Coverage assertion: the sweep must actually EXERCISE the renderer — a large
// fraction of poses must produce visible geometry (else the "green" is vacuous).
let frac_lit = poses_with_faces as f64 / poses.max(1) as f64;
eprintln!(
"[{label}] poses={poses} with_faces={poses_with_faces} ({:.1}%) total_escapes={total_escapes} \
worst_ink={worst_ink:.1}px @ {worst_pose:?}",
frac_lit * 100.0
);
assert!(poses > 1000, "[{label}] the sweep must cover a real grid (poses={poses})");
assert!(
frac_lit > 0.5,
"[{label}] over half the poses must render geometry (got {:.1}%)",
frac_lit * 100.0
);
// Per-step aggregate emitters → one functional_status row per part-step, so the
// matrix carries the sweep verdict per stage. (EMIT-DOCTRINE: the scissor stage
// is the can't-observe-from-the-return-value pixel oracle; the rest assert above.)
#[cfg(feature = "testmatrix")]
{
let ok = total_escapes == 0;
facett_core::testmatrix::emit(
"facett-map3d",
"scissor",
ok,
&format!(
"sweep={label} poses={poses} clipped_escapes={total_escapes} worst_ink_outside_rect={worst_ink:.1}px"
),
);
facett_core::testmatrix::emit(
"facett-map3d",
"composite",
frac_lit > 0.5,
&format!("sweep={label} poses_with_faces={poses_with_faces}/{poses}"),
);
}
// FINAL-PICTURE PNG blob at the WORST-overshoot pose: rasterise the scissored
// faces (CPU, no wgpu) so an agent can Read the picture and confirm the city is
// confined to the rect even at the most stressful camera.
let (fname, az, elev, dist) = worst_pose;
if !fname.is_empty() {
let mut m = if fname == "rabbit" { Map3D::synthetic("rabbit") } else { giant_city() };
m.spin = false;
m.cam.azimuth = az.to_radians();
m.cam.goal_azimuth = az.to_radians();
m.cam.elevation = elev.to_radians();
m.cam.goal_elevation = elev.to_radians();
m.cam.distance = dist;
m.cam.goal_distance = dist;
let center = Pos2::new(CANVAS * 0.5, CANVAS * 0.5);
let faces = m.shade_faces_clip(center, CANVAS * 0.5, Color32::from_rgb(40, 80, 60), true);
let (img, lit, outside) = rasterise(&faces, rect);
let dir = warehouse_blob_dir();
std::fs::create_dir_all(&dir).expect("blob dir");
let png = dir.join(format!("map3d_partstep_sweep_worst_{label}.png"));
img.save(&png).expect("write worst-pose PNG blob");
eprintln!(
"[{label}] worst-pose blob: {} (lit_px={lit}, px_outside_rect={outside})",
png.display()
);
// The blob is non-blank AND wholly inside the rect — the picture proves it,
// not just the counter.
assert!(lit > 0, "[{label}] worst-pose picture is blank (lit_px=0)");
assert_eq!(outside, 0, "[{label}] worst-pose picture leaked {outside}px outside the rect");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-map3d",
"composite_blob",
lit > 0 && outside == 0,
&format!("blob={} lit_px={lit} outside={outside}", png.display()),
);
}
}
/// Minimal CPU triangle rasteriser: fills each face's rect-clipped polygon and
/// returns `(image, lit_px, px_outside_rect)`. Enough to prove non-blank +
/// contained without a GPU.
fn rasterise(faces: &[(f32, [Pos2; 3], Color32)], rect: Rect) -> (image::RgbaImage, usize, usize) {
let w = CANVAS as u32;
let h = CANVAS as u32;
let mut img = image::RgbaImage::from_pixel(w, h, image::Rgba([10, 12, 16, 255]));
for (_d, tri, col) in faces {
let poly = clip_poly_to_rect(tri, rect);
if poly.len() < 3 {
continue;
}
// Fan-triangulate the convex clipped polygon and half-plane fill each tri.
for k in 1..poly.len() - 1 {
fill_tri(&mut img, [poly[0], poly[k], poly[k + 1]], *col);
}
}
let mut lit = 0usize;
let mut outside = 0usize;
for y in 0..h {
for x in 0..w {
let p = img.get_pixel(x, y);
if p.0 != [10, 12, 16, 255] {
lit += 1;
let fx = x as f32 + 0.5;
let fy = y as f32 + 0.5;
if fx < rect.left() - 0.5 || fx > rect.right() + 0.5 || fy < rect.top() - 0.5 || fy > rect.bottom() + 0.5 {
outside += 1;
}
}
}
}
(img, lit, outside)
}
fn fill_tri(img: &mut image::RgbaImage, p: [Pos2; 3], col: Color32) {
let (w, h) = (img.width() as f32, img.height() as f32);
let minx = p.iter().map(|v| v.x).fold(f32::MAX, f32::min).max(0.0).floor() as u32;
let maxx = p.iter().map(|v| v.x).fold(f32::MIN, f32::max).min(w - 1.0).ceil() as u32;
let miny = p.iter().map(|v| v.y).fold(f32::MAX, f32::min).max(0.0).floor() as u32;
let maxy = p.iter().map(|v| v.y).fold(f32::MIN, f32::max).min(h - 1.0).ceil() as u32;
let area = edge(p[0], p[1], p[2]);
if area.abs() < 1e-6 {
return;
}
let rgba = image::Rgba([col.r(), col.g(), col.b(), 255]);
for y in miny..=maxy.min(img.height() - 1) {
for x in minx..=maxx.min(img.width() - 1) {
let pt = Pos2::new(x as f32 + 0.5, y as f32 + 0.5);
let w0 = edge(p[1], p[2], pt);
let w1 = edge(p[2], p[0], pt);
let w2 = edge(p[0], p[1], pt);
let inside = (w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0) || (w0 <= 0.0 && w1 <= 0.0 && w2 <= 0.0);
if inside {
img.put_pixel(x, y, rgba);
}
}
}
}
fn edge(a: Pos2, b: Pos2, c: Pos2) -> f32 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
/// `$NORNIR_WAREHOUSE_ROOT/facett-map3d` if set (LAW: persist to warehouse on a fast
/// disk), else the system temp dir.
fn warehouse_blob_dir() -> std::path::PathBuf {
if let Ok(root) = std::env::var("NORNIR_WAREHOUSE_ROOT") {
std::path::Path::new(&root).join("facett-map3d")
} else {
std::env::temp_dir().join("facett-map3d")
}
}
// ── the default arm: a 36° × 25-zoom × 4-elev grid (3600 poses/fixture) ──────────
#[test]
fn partstep_sweep_scissor_oracle_across_rotation_and_zoom() {
sweep(10, 25, "default_36x25x4");
}
// ── the heavy arm: the FULL 360° × 100-zoom × 4-elev grid (28 800 poses/fixture) ─
// Heavy (#[ignore]) per the heavy-test LAW — opt in with `--ignored` / `--heavy`.
#[test]
#[ignore = "heavy: full 360x100 pose grid, run with --ignored or --heavy"]
fn partstep_sweep_full_360x100_heavy() {
sweep(5, 100, "heavy_72x100x4");
}
// ── degenerate / adversarial geometry: must not panic, must not escape ───────────
#[test]
fn partstep_sweep_degenerate_geometry_no_escape_no_panic() {
let rect = widget_rect();
// Zero-area footprints (a degenerate "building" collapsed to a line) + a normal
// one, so the mesh is non-empty but carries degenerate tris.
let mut ways = vec![building(1, &[(17.70, 59.61), (17.7006, 59.61), (17.7006, 59.6106), (17.70, 59.6106), (17.70, 59.61)])];
// a degenerate ring (all points identical) — exercises zero-area handling.
ways.push(building(2, &[(17.71, 59.62), (17.71, 59.62), (17.71, 59.62), (17.71, 59.62)]));
let mut m = Map3D::from_geometry("degen", &ways);
m.spin = false;
// Sweep including the pathological behind-the-eye / grazing poses.
let mut checked = 0;
for ai in (0..360).step_by(15) {
for &elev in &[0.5f32, 5.0, 89.5] {
for &dist in &[0.01f32, 0.05, 1.0, 50.0] {
let (_n, ink, escapes) = run_pose(&mut m, ai as f32, elev, dist);
assert_eq!(escapes, 0, "degenerate: escape at az={ai}° elev={elev}° dist={dist}");
assert!(ink.is_finite(), "degenerate: non-finite ink at az={ai}° elev={elev}° dist={dist}");
checked += 1;
}
}
}
assert!(checked > 200, "degenerate sweep ran (checked={checked})");
let _ = rect;
}