facett-model3d/src/tests.rs
//! Unit + headless tests — mirror facett-graph3d's logo3d proofs, plus the
//! status-tint (green→yellow→red) asserted as DATA.
use crate::*;
use facett_core::Facet;
use facett_core::harness;
// ── mesh + glTF loader ──────────────────────────────────────────────────────
#[test]
fn loads_the_bundled_skull_with_named_groups() {
let mesh = load_glb(SKULL_GLB).expect("bundled skull loads");
assert!(mesh.has_group("skull"), "has the bone group");
assert!(mesh.has_group("eyes"), "has the status-tinted eyes group");
assert!(mesh.triangle_count() > 50, "a real mesh: {} tris", mesh.triangle_count());
assert!(mesh.vertex_count() > 0);
}
#[test]
fn loads_the_bundled_avocado_real_gltf() {
let m = Model3D::avocado("avocado3d");
assert!(m.mesh().has_group("Avocado"), "the real glTF group is present");
assert!(m.mesh().triangle_count() > 50, "{} tris", m.mesh().triangle_count());
// The whole fruit is the status target (ripeness readout).
assert_eq!(m.state_json()["status_group"], "Avocado");
}
#[test]
fn skull_is_normalised_into_the_unit_box() {
let mesh = load_glb(SKULL_GLB).unwrap();
for g in &mesh.groups {
for p in &g.positions {
for c in p {
assert!(c.is_finite() && c.abs() <= 1.0001, "in [-1,1]: {c}");
}
}
}
}
#[test]
fn glb_writer_roundtrips_through_the_real_reader() {
// to_glb(procedural skull) → load_glb recovers the same groups + triangles.
let src = skull::skull_mesh();
let bytes = glb::to_glb(&src);
let back = load_glb(&bytes).expect("round-trip loads");
assert_eq!(back.group_names(), src.group_names(), "group order + names preserved");
assert_eq!(back.triangle_count(), src.triangle_count(), "triangle count preserved");
}
#[test]
fn empty_buffer_is_a_clean_error_not_a_panic() {
assert!(load_glb(b"not a glb").is_err());
}
// ── component render ──────────────────────────────────────────────────────────
#[test]
fn headless_render_draws_the_skull() {
let mut m = Model3D::skull("skull3d").with_clock(1.0);
let r = harness::headless_render(&mut m);
assert!(r.drew(), "the lit, depth-sorted skull tessellates to vertices");
assert!(r.state["triangles"].as_u64().unwrap() > 50);
assert_eq!(r.state["clock_s"], 1.0);
let groups = r.state["groups"].as_array().unwrap();
assert!(groups.iter().any(|g| g == "eyes"));
}
#[test]
fn injected_clock_makes_the_spin_reproducible() {
let mut a = Model3D::skull("s").with_clock(2.5);
let mut b = Model3D::skull("s").with_clock(2.5);
let ra = harness::headless_render(&mut a);
let rb = harness::headless_render(&mut b);
assert_eq!(ra.vertices, rb.vertices, "same clock → same drawn geometry");
assert_eq!(ra.state["yaw"], rb.state["yaw"]);
}
// ── status tint (green→yellow→red), asserted as DATA ─────────────────────────
#[test]
fn status_color_fades_green_to_red() {
let crit = Model3D::skull("s").with_status(0.0).status_color();
let warn = Model3D::skull("s").with_status(0.5).status_color();
let ok = Model3D::skull("s").with_status(1.0).status_color();
// critical = red-dominant, healthy = green-dominant, warn = both high (yellow).
assert!(crit[0] > crit[1] && crit[0] > 0.6, "critical is red: {crit:?}");
assert!(ok[1] > ok[0] && ok[1] > 0.6, "healthy is green: {ok:?}");
assert!(warn[0] > 0.6 && warn[1] > 0.6, "warn is yellow: {warn:?}");
}
#[test]
fn state_json_reports_the_eye_color_and_status_group() {
let red = Model3D::skull("s").with_status(0.0);
let green = Model3D::skull("s").with_status(1.0);
let rj = red.state_json();
let gj = green.state_json();
assert_eq!(rj["status_group"], "eyes");
let r: Vec<u64> = rj["status_rgb"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
let g: Vec<u64> = gj["status_rgb"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
assert!(r[0] > r[1], "status 0 → red channel dominates: {r:?}");
assert!(g[1] > g[0], "status 1 → green channel dominates: {g:?}");
}
#[test]
fn status_clamps_and_disabling_the_group_keeps_base_colors() {
let mut m = Model3D::skull("s");
m.set_status(5.0); // clamps to 1.0
assert_eq!(m.status(), 1.0);
let none = Model3D::skull("s").without_status_group();
assert!(none.state_json()["status_group"].is_null());
}
// ── FC-2 → FC-3 as a *headless* property: feed a `Vec<Msg>` through the core
// harness and assert the resulting `Model3DState` — no egui, no GPU. ──────────
#[test]
fn harness_snapshot_drives_orbit_spin_status_and_clock() {
let mut m = Model3D::skull("s");
// Default pose: yaw 0.5, tilt 0.30, spin on, status 1.0.
assert_eq!(m.state().yaw, 0.5);
assert!(m.state().spin);
// Orbit accumulates AND pauses auto-spin (the drag gesture).
let snap = harness::snapshot(&mut m, [Msg::Orbit(0.2), Msg::Orbit(0.1)]);
assert!((snap.yaw - 0.8).abs() < 1e-6, "orbit deltas accumulate onto yaw: {}", snap.yaw);
assert!(!snap.spin, "a drag-orbit pauses the auto-spin");
assert_eq!(&snap, m.state(), "the snapshot is a clone of the live control state");
// Spin advances yaw but KEEPS the spin flag (the live auto-spin tick).
let snap = harness::snapshot(&mut m, [Msg::SetSpin(true), Msg::Spin(0.05)]);
assert!((snap.yaw - 0.85).abs() < 1e-6, "spin advances yaw: {}", snap.yaw);
assert!(snap.spin, "an auto-spin tick keeps spinning");
// Tilt + absolute yaw are settable; ResetView restores the default pose (keeps status).
let snap = harness::snapshot(&mut m, [Msg::SetYaw(2.0), Msg::SetTilt(0.9), Msg::SetStatus(0.25)]);
assert_eq!(snap.yaw, 2.0);
assert_eq!(snap.tilt, 0.9);
let snap = harness::snapshot(&mut m, [Msg::ResetView]);
assert_eq!(snap.yaw, 0.5, "reset restores the default yaw");
assert_eq!(snap.tilt, 0.30, "reset restores the default tilt");
assert_eq!(snap.status, 0.25, "reset keeps the status readout");
// Status clamps to [0,1]; the status group + inline control toggle are observable.
let snap = harness::snapshot(
&mut m,
[Msg::SetStatus(5.0), Msg::SetStatusControl(true), Msg::SetStatusGroup(Some("bone".into()))],
);
assert_eq!(snap.status, 1.0, "status clamps at the 1.0 ceiling");
assert!(snap.status_control);
assert_eq!(snap.status_group.as_deref(), Some("bone"));
let snap = harness::snapshot(&mut m, [Msg::SetStatusGroup(None)]);
assert!(snap.status_group.is_none(), "None disables the status tint");
// The injected clock (FC-7): Tick accumulates, SetClock pins absolutely.
let snap = harness::snapshot(&mut m, [Msg::SetClock(1.0), Msg::Tick(0.5)]);
assert_eq!(snap.clock_s, Some(1.5), "Tick accumulates onto the pinned clock");
let snap = harness::snapshot(&mut m, [Msg::SetClock(3.0)]);
assert_eq!(snap.clock_s, Some(3.0), "SetClock pins the clock absolutely");
}
#[test]
fn drive_reports_no_effects_and_state_round_trips() {
let mut m = Model3D::skull("s");
// FC-8: the viewer does no I/O, so the effect stream is always empty.
let effects = harness::drive(&mut m, [Msg::Orbit(0.3), Msg::SetStatus(0.4), Msg::Tick(1.0)]);
assert!(effects.is_empty(), "FC-8: model3d emits no Effects");
assert!((m.state().status - 0.4).abs() < 1e-6, "status applied: {}", m.state().status);
// FC-3: the observable Model round-trips through serde byte-for-byte.
let json = serde_json::to_value(m.state()).unwrap();
assert_eq!(json["clock_s"], 1.0);
let back: Model3DState = serde_json::from_value(json).unwrap();
assert_eq!(&back, m.state(), "serde(state) -> state round-trips");
}
#[test]
fn public_setters_route_through_the_update_mutation_path() {
// The public affordances are thin wrappers over the FC-2 mutation path.
let mut m = Model3D::skull("s");
m.set_status(0.5);
assert_eq!(m.state().status, 0.5);
m.set_clock(2.0);
m.tick(0.25);
assert_eq!(m.state().clock_s, Some(2.25), "set_clock + tick route through update");
}
#[test]
fn state_json_keys_are_preserved_exactly() {
// The mega_matrix + facett-demo read these keys — Form 3 custom_state_json must
// publish the identical surface the hand-written impl did.
let m = Model3D::avocado("avocado3d").with_clock(1.0);
let j = m.state_json();
for key in [
"title", "vertices", "triangles", "groups", "yaw", "tilt", "spin", "spin_rate",
"clock_s", "status", "status_group", "status_control", "status_rgb",
] {
assert!(j.get(key).is_some(), "state_json must carry `{key}`");
}
assert_eq!(j["status_group"], "Avocado");
assert_eq!(j["clock_s"], 1.0);
let tint: Vec<u64> = j["status_rgb"].as_array().unwrap().iter().map(|c| c.as_u64().unwrap()).collect();
assert!(tint[1] > tint[0], "default status is healthy → green-dominant tint: {tint:?}");
}