facett-core/tests/l0_geometry.rs
//! **L0 geometry + state coverage** — additive tests over the public L0 kernel API
//! that pin behaviour the per-module unit tests don't already cover:
//!
//! - the [`quad_coverage`] shape dispatch for **every** [`shape`] tag (the inline
//! test exercises circle/ring/diamond; here square + triangle + the unknown-tag
//! `_ => 0.0` arm),
//! - [`point_segment`] — the distance-to-segment + clamped `t` helper the thick-line
//! raster and any edge **hit-test** funnel through (only exercised indirectly by
//! `line_coverage` today),
//! - the [`camera::V3`] vector algebra (`cross`/`dot`/`len`/`normalized`) that
//! `Camera::basis`/`eye` are built from — pinned directly, not just via the seam,
//! - the 2D pan/zoom affine + its analytic inverse (screen→world **picking** math),
//! - [`Frame::lit_px`] — the "did it draw" oracle — on transparent vs mixed frames,
//! - the [`decor`] node/interaction geometry helpers (`glow_halo`, `ao_shadow`,
//! `shockwave_ring`, `lerp_rgba`) whose shapes/clamps the inline tests reach only
//! through the batch helpers.
//!
//! All pure CPU math — no GPU, no `wgpu`/`l1-vello` feature — so this runs headless
//! on the default build (the L0-stands-alone promise).
use facett_core::render::camera::{self, Camera, Pos, V3};
use facett_core::render::cpu::sdf::{point_segment, quad_coverage};
use facett_core::render::decor;
use facett_core::render::prim::{shape, MarkerInstance};
use facett_core::render::Frame;
// ── SDF dispatch: the two shapes + the unknown arm the inline test skips ───────
#[test]
fn quad_coverage_dispatches_square_triangle_and_rejects_unknown() {
let mk = |s: u32| MarkerInstance {
center: [0.0; 2],
radius: 10.0,
corner: 0.0,
color: [1.0; 4],
aa: 0.5,
shape: s,
}
.lower();
// SQUARE fills its corner (|8|,|8| is inside a half=10 axis-aligned box) …
let sq = mk(shape::SQUARE);
assert!(quad_coverage(&sq, 0.0, 0.0) > 0.99, "square centre lit");
assert!(quad_coverage(&sq, 8.0, 8.0) > 0.9, "square fills its corner");
assert!(quad_coverage(&sq, 14.0, 0.0) < 0.01, "square dark past the edge+aa");
// … TRIANGLE (apex up) is lit low-centre, dark at the top corners.
let tri = mk(shape::TRIANGLE);
assert!(quad_coverage(&tri, 0.0, 7.0) > 0.9, "triangle base-centre lit");
assert!(quad_coverage(&tri, 9.0, -9.0) < 0.1, "triangle top-corner dark");
// An unknown shape tag hits the `_ => 0.0` arm → never draws (defensive default).
let mut bogus = sq;
bogus.shape = 999;
assert_eq!(quad_coverage(&bogus, 0.0, 0.0), 0.0, "unknown shape draws nothing");
}
// ── point_segment: the edge hit-test / distance kernel ─────────────────────────
#[test]
fn point_segment_gives_distance_and_clamped_parameter() {
let a = [0.0, 0.0];
let b = [10.0, 0.0];
// A point above the middle: nearest point is the foot of the perpendicular at
// t=0.5, squared distance = 3^2 = 9.
let (d2, t) = point_segment([5.0, 3.0], a, b);
assert!((d2 - 9.0).abs() < 1e-4, "squared distance to the segment, got {d2}");
assert!((t - 0.5).abs() < 1e-6, "foot at the segment midpoint, got {t}");
// On the segment → zero distance.
let (d2on, ton) = point_segment([7.0, 0.0], a, b);
assert!(d2on < 1e-6 && (ton - 0.7).abs() < 1e-6, "on-segment: d=0, t=0.7");
// Past an endpoint: t clamps to [0,1] (a butt cap reads t==1 to reject the
// overshoot); distance is measured to the endpoint, not the infinite line.
let (d2past, tpast) = point_segment([20.0, 0.0], a, b);
assert_eq!(tpast, 1.0, "t clamps at the far endpoint");
assert!((d2past - 100.0).abs() < 1e-4, "distance measured to endpoint b (10 px away)");
let (_, tbefore) = point_segment([-5.0, 0.0], a, b);
assert_eq!(tbefore, 0.0, "t clamps at the near endpoint");
// Degenerate zero-length segment: t is defined (0), distance is to the point.
let (d2deg, tdeg) = point_segment([3.0, 4.0], [1.0, 1.0], [1.0, 1.0]);
assert_eq!(tdeg, 0.0, "zero-length segment → t=0 (no divide-by-zero)");
// (3-1)^2 + (4-1)^2 = 4 + 9 = 13.
assert!((d2deg - 13.0).abs() < 1e-4, "distance to the degenerate point, got {d2deg}");
}
// ── V3 algebra: the building blocks of Camera::basis/eye ───────────────────────
#[test]
fn v3_cross_is_right_handed_and_dot_len_normalize_agree() {
let x = V3::new(1.0, 0.0, 0.0);
let y = V3::new(0.0, 1.0, 0.0);
let z = V3::new(0.0, 0.0, 1.0);
// Right-hand rule: x × y = +z (the handedness Camera::basis relies on so
// `right = fwd × up` points the expected way).
let c = x.cross(y);
assert_eq!((c.x, c.y, c.z), (0.0, 0.0, 1.0), "x × y = z (right-handed)");
let c2 = y.cross(x);
assert_eq!((c2.x, c2.y, c2.z), (0.0, 0.0, -1.0), "y × x = -z (anticommutes)");
// Orthonormal basis: pairwise dots are 0, self-dot is 1.
assert_eq!(x.dot(y), 0.0);
assert_eq!(z.dot(z), 1.0);
// len + normalized: a (3,4,0) vector has length 5 and normalises to a unit.
let v = V3::new(3.0, 4.0, 0.0);
assert!((v.len() - 5.0).abs() < 1e-6, "3-4-5 length");
let u = v.normalized();
assert!((u.len() - 1.0).abs() < 1e-6, "normalized is unit length");
assert!((u.x - 0.6).abs() < 1e-6 && (u.y - 0.8).abs() < 1e-6, "direction preserved");
// A zero vector normalises without NaN (the 1e-9 guard) — a degenerate basis
// input must not poison the camera.
let zn = V3::new(0.0, 0.0, 0.0).normalized();
assert!(zn.x.is_finite() && zn.y.is_finite() && zn.z.is_finite(), "no NaN from zero vec");
// add/sub/scale round-trip.
let a = V3::new(1.0, 2.0, 3.0);
let b = V3::new(4.0, -1.0, 0.5);
let back = a.add(b).sub(b);
assert_eq!((back.x, back.y, back.z), (a.x, a.y, a.z), "add then sub is identity");
let s = a.scale(2.0);
assert_eq!((s.x, s.y, s.z), (2.0, 4.0, 6.0));
}
// ── 2D pan/zoom projection + its inverse (screen→world picking) ────────────────
#[test]
fn camera_2d_affine_is_invertible_for_picking() {
let cam = Camera { pan_x: 30.0, pan_y: -12.0, zoom: 2.5, ..Camera::default() };
let world = Pos::new(10.0, 4.0);
let (sx, sy) = cam.project2d(world);
// The analytic inverse a click→world picker uses: (screen - pan) / zoom. Round
// a projected point back and it lands on the original world coordinate.
let (wx, wy) = ((sx - cam.pan_x) / cam.zoom, (sy - cam.pan_y) / cam.zoom);
assert!((wx - world.x).abs() < 1e-4 && (wy - world.y).abs() < 1e-4, "project→unproject round-trips");
// A fractional zoom (zoomed out) still round-trips — the picker holds at any scale.
let cam2 = Camera { pan_x: -5.0, pan_y: 100.0, zoom: 0.25, ..Camera::default() };
let (sx2, sy2) = cam2.project2d(Pos::new(400.0, -80.0));
let (wx2, wy2) = ((sx2 - cam2.pan_x) / cam2.zoom, (sy2 - cam2.pan_y) / cam2.zoom);
assert!((wx2 - 400.0).abs() < 1e-3 && (wy2 + 80.0).abs() < 1e-3, "fractional-zoom round-trip");
}
// ── 3D orbit invariants (public superset math) ─────────────────────────────────
#[test]
fn camera_eye_orbits_at_distance_and_far_plane_pads_it() {
// The eye sits on a sphere of radius `distance` around the target, at any
// azimuth/elevation (the turntable invariant).
for &(az, el) in &[(0.0f32, 0.0f32), (0.7, 0.3), (-1.2, -0.9), (3.0, 1.4)] {
let cam = Camera {
azimuth: az,
elevation: el,
distance: 5.0,
target: V3::new(1.0, -2.0, 0.5),
..Camera::default()
};
let r = cam.eye().sub(cam.target).len();
assert!((r - 5.0).abs() < 1e-3, "eye radius == distance at ({az},{el}), got {r}");
}
// far_plane pads the dolly radius (mirrors OrbitCamera::far_plane = distance+4).
let cam = Camera { distance: 6.0, ..Camera::default() };
assert!((cam.far_plane() - 10.0).abs() < 1e-6, "far plane = distance + 4");
assert_eq!(Camera::NEAR_PLANE, 0.02);
let _ = camera::InputFeel::default(); // the FEEL type is part of the public seam.
}
// ── Frame::lit_px oracle ───────────────────────────────────────────────────────
#[test]
fn frame_lit_px_counts_only_nonzero_alpha() {
// A fully transparent frame lit nothing.
let clear = Frame { width: 2, height: 2, rgba: vec![0; 2 * 2 * 4] };
assert_eq!(clear.lit_px(), 0, "transparent frame → 0 lit");
// Mixed: 2 of 4 pixels carry alpha (colour with a==0 does NOT count — the oracle
// keys off alpha, the "did the compositor put ink here" signal).
let mixed = Frame {
width: 2,
height: 2,
rgba: vec![
255, 0, 0, 255, // lit
9, 9, 9, 0, // opaque-looking colour but a==0 → not lit
0, 0, 0, 1, // barely lit
0, 0, 0, 0, // clear
],
};
assert_eq!(mixed.lit_px(), 2, "only the two non-zero-alpha pixels count");
}
// ── decor geometry helpers (shapes + clamps) ───────────────────────────────────
#[test]
fn decor_node_helpers_have_the_right_geometry() {
// glow_halo: a ring wider than the marker, hole just outside it, alpha clamped
// into [0,1], hue carried from the node colour.
let halo = decor::glow_halo([40.0, 40.0], 8.0, [0.2, 0.8, 1.0], 1.7 /*>1 → clamps*/, 2.1);
assert!(halo.radius > 8.0 && halo.inner >= 8.0, "halo blooms outside the marker");
assert!(halo.inner < halo.radius, "valid annulus");
assert_eq!(halo.color[3], 1.0, "intensity clamped to 1.0");
assert!((halo.color[2] - 1.0).abs() < 1e-6, "carries the node blue");
// ao_shadow: a dark disc offset down-right (drop-shadow), larger than the marker,
// alpha (depth) clamped.
let ao = decor::ao_shadow([40.0, 40.0], 10.0, 1.5 /*>1 → clamps*/);
assert!(ao.center[0] > 40.0 && ao.center[1] > 40.0, "shadow offset down-right");
assert!(ao.radius > 10.0, "shadow spreads past the marker");
assert!(ao.color[0] < 0.05 && ao.color[3] == 1.0, "black, depth clamped to 1.0");
}
#[test]
fn decor_ramps_and_shockwave_clamp() {
// lerp_rgba: endpoints exact, midpoint blends, out-of-range t clamps.
let a = [0.0, 0.0, 0.0, 1.0];
let b = [1.0, 0.5, 0.25, 0.0];
assert_eq!(decor::lerp_rgba(a, b, 0.0), a);
assert_eq!(decor::lerp_rgba(a, b, 1.0), b);
assert_eq!(decor::lerp_rgba(a, b, 2.0), b, "t clamps above 1");
assert_eq!(decor::lerp_rgba(a, b, -1.0), a, "t clamps below 0");
let mid = decor::lerp_rgba(a, b, 0.5);
assert!((mid[0] - 0.5).abs() < 1e-6 && (mid[3] - 0.5).abs() < 1e-6, "midpoint blends");
// shockwave_ring: the inner radius never goes negative even for a tiny start ring
// whose thickness exceeds it (the `.max(0.0)` clamp), and alpha fades to ~0 at t=1.
let tiny = decor::shockwave_ring([50.0, 50.0], 0.0, 5.0, [1.0, 0.4, 0.9], 0.0);
assert!(tiny.inner >= 0.0, "inner radius clamped to >= 0 (valid annulus)");
let gone = decor::shockwave_ring([50.0, 50.0], 10.0, 100.0, [1.0, 0.4, 0.9], 1.0);
assert!(gone.color[3] < 1e-3, "faded out at t=1");
assert!(gone.radius > 100.0, "expanded to r0 + reach at t=1");
}