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