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

nordisk/facett

facett-graph3d/tests/logo_clip_pose_sweep.rs

//! **THE CLIPPING-BUG FENCE (rabbit logo)** — the same 360° yaw × zoom (pane) × tilt
//! robot sweep `facett-model3d` runs, over the extruded znippy rabbit. Both panes
//! draw through the one shared clip tail
//! ([`facett_core::render::cpu::faces`]), so both are fenced by the same oracles:
//!
//! 1. **no pose leaks ink outside the viewport** (`ink_outside_rect == 0`), and
//! 2. **no pose loses geometry it should keep**.
//!
//! ## The bug this fences
//! `RabbitLogo3D::ui` used to hand every projected face to
//! `egui::Shape::convex_polygon`. `epaint` feathers filled paths with a **miter
//! join** (`epaint::tessellator::Path::add_line_loop` — `normal / length_sq`); an
//! extruded silhouette seen at an angle is *full* of near-degenerate side-wall
//! quads, so `length_sq → 0` and the emitted vertex is flung
//! `0.5 · feathering / sqrt(length_sq)` px away — the thin bright **spokes** that
//! crossed the rabbit pane in the running demo. The raw projection was innocent;
//! the tessellation was not. The pane now rasterises a per-pixel z-buffer instead:
//! no tessellator, no miter, no spoke — and correct occlusion between the logo's
//! four interpenetrating extruded shells, which the painter's-algorithm centroid
//! sort used to tear along the ear/head seam.
//!
//! ## The *other* bug this fences: **the hollow ears**
//! `Rabbit::ear` returned its petal loop wound **clockwise** while
//! [`facett_core::rabbit::rabbit_mesh`] extrudes on the documented "every loop is
//! CCW" contract. Both ears therefore came out inside out — front cap wound
//! backwards (culled), back cap facing the camera, side walls facing inward — and
//! rendered as hollow open troughs. 528 of the mesh's 1040 triangles had a geometric
//! normal contradicting their own authored vertex normal. See
//! [`the_logo_mesh_has_no_inside_out_triangles`].
//!
//! Run: `cargo test -p facett-graph3d --test logo_clip_pose_sweep -- --nocapture`

use egui::{Color32, Rect, pos2};
use facett_core::render::cpu::{Pose, ShadedTri, pose_grid, raster_zbuffer, sweep};
use facett_graph3d::RabbitLogo3D;

/// The logo's lit colour in the sweep — the theme accent a live pane passes in.
const ACCENT: Color32 = Color32::from_rgb(0x2E, 0xC8, 0xC8);

/// The zoom axis: a wide deck pane, a tall phone pane, a square card, and a pane
/// **below the viewer's own `MIN_W`/`MIN_H` floor** (the case that used to amputate
/// the logo when egui cropped the over-allocated rect).
fn viewports() -> Vec<Rect> {
    vec![
        Rect::from_min_size(pos2(8.0, 326.0), egui::vec2(1264.0, 484.0)), // the live deck pane
        Rect::from_min_size(pos2(0.0, 0.0), egui::vec2(320.0, 900.0)),    // tall / phone
        Rect::from_min_size(pos2(40.0, 40.0), egui::vec2(400.0, 400.0)),  // square card
        Rect::from_min_size(pos2(0.0, 0.0), egui::vec2(96.0, 64.0)),      // below the MIN floor
    ]
}

/// 72 yaws (every 5°) × 5 tilts × 4 viewports = 1440 poses.
fn poses() -> Vec<Pose> {
    pose_grid(72, &[-0.60, -0.30, 0.0, 0.32, 0.60], &viewports())
}

/// The **bug lane**: tessellate the same faces the way the pane used to — one
/// `Shape::convex_polygon` per face, straight through `epaint`'s feathering miter.
fn convex_polygon_lane_ink_outside(tris: &[ShadedTri], rect: Rect) -> (usize, f32) {
    let mut tess = epaint::Tessellator::new(1.0, epaint::TessellationOptions::default(), [16, 16], vec![]);
    let mut mesh = epaint::Mesh::default();
    for t in tris {
        tess.tessellate_shape(
            epaint::Shape::convex_polygon(t.pts.to_vec(), t.col, epaint::Stroke::NONE),
            &mut mesh,
        );
    }
    let mut n = 0usize;
    let mut worst = 0.0f32;
    for v in &mesh.vertices {
        let over = (rect.left() - v.pos.x)
            .max(v.pos.x - rect.right())
            .max(rect.top() - v.pos.y)
            .max(v.pos.y - rect.bottom());
        if over > 1e-2 {
            n += 1;
        }
        worst = worst.max(over);
    }
    (n, worst)
}

#[test]
fn rabbit_logo_leaks_no_ink_and_loses_no_geometry_across_360x_zoom_x_tilt() {
    let logo = RabbitLogo3D::new("rabbit3d");
    let mesh_tris = logo.mesh().triangle_count();
    let ps = poses();
    let report = sweep(&ps, |p| logo.clip_stats(p.viewport, p.yaw, p.tilt, ACCENT));
    eprintln!("[clip-sweep] rabbit3d ({mesh_tris} mesh tris) {}", report.detail());
    assert_eq!(report.poses, 1440, "72 yaws × 5 tilts × 4 viewports");

    // ── 1. THE BUG ORACLE. ────────────────────────────────────────────────────
    assert_eq!(
        report.worst_ink.0, 0,
        "ink escaped the viewport at pose `{}` — the clipping bug is back",
        report.worst_ink.1
    );

    // ── 2. NO POSE LOSES GEOMETRY IT SHOULD KEEP. The extruded logo is a closed
    //       solid: a fifth of its faces front-face at any orientation.
    let floor = mesh_tris / 5;
    assert!(
        report.min_visible.0 >= floor,
        "pose `{}` drew only {} of {mesh_tris} triangles (floor {floor}) — the clip stage is \
         eating real geometry",
        report.min_visible.1,
        report.min_visible.0
    );

    // ── 3. NO SPOKE. ──────────────────────────────────────────────────────────
    let worst_diag = viewports()
        .iter()
        .map(|r| (r.width() * r.width() + r.height() * r.height()).sqrt())
        .fold(0.0f32, f32::max);
    assert!(
        report.worst_extent.0 <= worst_diag + 1.0,
        "a face spanned {:.1} px at pose `{}` (largest viewport diagonal {worst_diag:.1} px) — \
         that is a spoke",
        report.worst_extent.0,
        report.worst_extent.1
    );

    // ── 4. THE FIT IS POSE-INVARIANT (bounding-sphere, not a magic 0.42). ─────
    assert!(
        report.worst_raw_overshoot.0 <= 0.5,
        "the raw projection overshot by {:.1} px at pose `{}` — the fit is wrong",
        report.worst_raw_overshoot.0,
        report.worst_raw_overshoot.1
    );

    // ── 5. Lit + depth-ordered at every pose. ─────────────────────────────────
    assert!(report.min_lit_fraction.0 > 0.5, "pose `{}` went dark", report.min_lit_fraction.1);
    assert_eq!(report.non_monotone_poses, 0, "the painter order must be monotone far→near");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-graph3d::RabbitLogo3D",
        "clip_pose_sweep",
        report.worst_ink.0 == 0 && report.min_visible.0 >= floor,
        &report.detail(),
    );
}

/// **RED-when-broken control** — the old `Shape::convex_polygon` lane really did
/// leak, at most poses, by an absurd amount; the shipped lane reads 0 at those exact
/// poses. Without this the sweep above proves nothing.
#[test]
fn the_old_convex_polygon_lane_really_did_leak() {
    let logo = RabbitLogo3D::new("rabbit3d");
    let ps = poses();
    let mut leaking_poses = 0usize;
    let mut worst_old = 0.0f32;
    let mut worst_old_where = String::new();
    let mut worst_new = 0usize;
    for p in &ps {
        let tris = logo.project_tris(p.viewport, p.yaw, p.tilt, ACCENT);
        let (n_out, over) = convex_polygon_lane_ink_outside(&tris, p.viewport);
        if n_out > 0 {
            leaking_poses += 1;
        }
        if over > worst_old {
            worst_old = over;
            worst_old_where = p.label.clone();
        }
        worst_new = worst_new.max(logo.clip_stats(p.viewport, p.yaw, p.tilt, ACCENT).ink_outside_rect);
    }
    eprintln!(
        "[clip-sweep] SENSITIVITY: old convex_polygon lane leaked at {leaking_poses}/{} poses, \
         worst overshoot {worst_old:.1} px at `{worst_old_where}`; shipped lane worst ink {worst_new}",
        ps.len()
    );
    assert!(
        leaking_poses > ps.len() / 10,
        "the bug lane must leak at many poses (it leaked at {leaking_poses}/{})",
        ps.len()
    );
    assert!(worst_old > 100.0, "the bug lane's worst spoke must be a real blow-up ({worst_old:.1} px)");
    assert_eq!(worst_new, 0, "the shipped lane leaks nothing at the very poses that used to leak");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-graph3d::RabbitLogo3D",
        "clip_sensitivity",
        leaking_poses > 0 && worst_new == 0,
        &format!(
            "bug_lane_leaking_poses={leaking_poses}/{} bug_lane_worst_overshoot_px={worst_old:.1} \
             fixed_lane_worst_ink=0",
            ps.len()
        ),
    );
}

/// **THE HOLLOW-EARS FENCE at the pane.** No triangle of the logo the pane actually
/// renders may be wound against its own authored normal — that is what made both
/// ears vanish into hollow troughs. RED-when-broken: reversing the mesh flags every
/// triangle, so this is not an assertion nothing can trip.
#[test]
fn the_logo_mesh_has_no_inside_out_triangles() {
    let logo = RabbitLogo3D::new("rabbit3d");
    let m = logo.mesh();
    let bad = facett_core::render::cpu::inside_out_triangles(&m.positions, &m.normals, &m.indices);
    eprintln!("[clip-sweep] rabbit3d: inside_out_triangles={bad}/{}", m.triangle_count());
    assert_eq!(bad, 0, "{bad} of {} logo triangles are wound inside out", m.triangle_count());

    let mut flipped = m.clone();
    for t in flipped.indices.as_chunks_mut::<3>().0 {
        t.swap(1, 2);
    }
    assert_eq!(
        facett_core::render::cpu::inside_out_triangles(&flipped.positions, &flipped.normals, &flipped.indices),
        m.triangle_count(),
        "sensitivity: a fully reversed mesh flags every triangle"
    );

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-graph3d::RabbitLogo3D",
        "mesh_winding",
        bad == 0,
        &format!("inside_out_triangles={bad}/{}", m.triangle_count()),
    );
}

/// **THE "GEOMETRY WENT MISSING" ORACLE** — the logo paints as a solid body at every
/// pose, covering its own projected footprint. This is the measurement
/// `ink_outside_rect` cannot make: it catches faces wrongly *culled*, not ink that
/// escaped. With the ears inside out this could not have held.
#[test]
fn no_pose_of_the_logo_is_see_through() {
    let logo = RabbitLogo3D::new("rabbit3d");
    let rect = Rect::from_min_size(pos2(0.0, 0.0), egui::vec2(400.0, 400.0));
    let (w, h) = (400usize, 400usize);
    let covered = |ts: &[ShadedTri]| {
        raster_zbuffer(ts, w, h, rect.min, 1.0).iter().filter(|c| c.a() != 0).count()
    };
    let mut worst = (f32::MAX, String::new());
    for p in &pose_grid(24, &[-0.60, -0.30, 0.0, 0.32, 0.60], &[rect]) {
        // The extruded logo is a CLOSED solid, so its front faces ARE its full
        // projected footprint — the painted area is the coverage measurement.
        let drawn = logo.project_tris(p.viewport, p.yaw, p.tilt, ACCENT);
        let total = covered(&drawn);
        assert!(total > 0, "pose `{}` drew nothing at all", p.label);
        let solid = total as f32 / (w * h) as f32;
        if solid < worst.0 {
            worst = (solid, p.label.clone());
        }
    }
    eprintln!("[clip-sweep] rabbit3d: smallest painted area {:.3} of the pane at `{}`", worst.0, worst.1);
    // A logo fitted to 94% of the pane's half-extent never shrinks to a sliver: even
    // edge-on (the extruded slab seen from the side) it keeps a real body.
    assert!(
        worst.0 > 0.02,
        "pose `{}` painted only {:.3} of the pane — the logo collapsed",
        worst.1,
        worst.0
    );
}