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

nordisk/facett

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:?}");
}