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/rate_overlay.rs

//! **EDGE-RATE overlay** (feature `rate`) — red-first proofs of the contract:
//! setting rates marks the RIGHT edges, the pulse phase advances with the
//! injected clock (FC-7) along the curve's ARC LENGTH, the per-edge arc BOWS
//! (sampled points are not collinear — the owner's smooth-lines requirement
//! in 3D), and the projected pulse positions carry depth (so depth shading
//! has something real to shade).
//!
//! The feature-OFF half of the contract — the crate compiles and the whole
//! suite passes with no `rates` field at all — is every OTHER test target of
//! this crate, which builds without `--features rate`.
#![cfg(feature = "rate")]

use egui::{Color32, Pos2};
use facett_core::Facet;
use facett_graph3d::{rate, EdgeRates, Graph3D, Layout3D, Msg};

fn sample(n: usize) -> Graph3D {
    let nodes: Vec<(String, Color32)> = (0..n).map(|i| (format!("n{i}"), Color32::from_rgb(0, 200, 220))).collect();
    let edges: Vec<(usize, usize)> = (0..n).map(|i| (i, (i + 1) % n)).collect();
    Graph3D::new(nodes, edges, Layout3D::Sphere)
}

/// **Setting rates marks the RIGHT edges.** A table indexed like the edge list
/// lands on exactly those edges: the getter hands it back untouched, the
/// observable surface counts only the rated (non-None, > 0) edges, and a
/// mis-sized or non-finite table is REFUSED with the current rates standing —
/// facett-flow's `set_rates` law, in 3D.
#[test]
fn setting_rates_marks_the_right_edges() {
    let mut g = sample(4); // 4 edges: 0→1, 1→2, 2→3, 3→0
    assert!(g.rates().is_none(), "no rates until a consumer sets them");
    assert!(
        g.state_json().get("rated_edges").is_none(),
        "an unrated graph reports the exact state surface it always has"
    );

    let table = EdgeRates { edges: vec![Some(2.0), None, Some(0.5), None] };
    g.set_rates(table.clone()).expect("a well-formed table lands");
    assert_eq!(g.rates(), Some(&table), "the getter hands back exactly what was set");
    let j = g.state_json();
    assert_eq!(j["rated_edges"], 2, "exactly the two Some(>0) edges are rated: {j}");
    assert_eq!(j["rate_max"], 2.0, "the scene maximum is observable: {j}");

    // The wrong length is refused; the good table stands.
    let short = EdgeRates { edges: vec![Some(1.0)] };
    assert!(g.set_rates(short).is_err(), "a mis-sized table is refused");
    assert_eq!(g.rates(), Some(&table), "the refused table did not disturb the set one");

    // Non-finite and negative rates are refused too.
    let bad = EdgeRates { edges: vec![Some(f32::NAN), None, None, None] };
    assert!(g.set_rates(bad).is_err(), "NaN is refused");
    let neg = EdgeRates { edges: vec![Some(-1.0), None, None, None] };
    assert!(g.set_rates(neg).is_err(), "a negative rate is refused");

    // The FC-2 path reaches the same transitions.
    let via_msg = EdgeRates { edges: vec![None, Some(3.0), None, None] };
    let _ = g.update(Msg::SetRates(via_msg.clone()));
    assert_eq!(g.rates(), Some(&via_msg), "Msg::SetRates routes through set_rates");
    let _ = g.update(Msg::ClearRates);
    assert!(g.rates().is_none(), "ClearRates returns to the plain render");
}

/// **The spline BOWS** (the owner's addendum): a rated edge yields sampled 3D
/// curve points that are NOT collinear — the arc's length strictly exceeds the
/// chord's — and there are enough segments to read smooth (≥ 16).
#[test]
fn a_rated_edges_curve_bows_and_is_not_collinear() {
    let a = [-0.8f32, -0.2, 0.1];
    let b = [0.7f32, 0.5, -0.3];
    let pts = rate::edge_curve(a, b, rate::CURVE_SEGMENTS);
    assert_eq!(pts.len(), rate::CURVE_SEGMENTS + 1, "n segments ⇒ n+1 samples");
    assert!(rate::CURVE_SEGMENTS >= 16, "enough segments to look smooth at typical zoom");
    assert_eq!(pts[0], a, "the curve leaves the caller's node");
    assert_eq!(*pts.last().unwrap(), b, "…and lands on the callee's node");

    // NOT collinear: some interior sample sits a real distance off the chord.
    let chord = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
    let chord_len = (chord[0].powi(2) + chord[1].powi(2) + chord[2].powi(2)).sqrt();
    let mut max_off = 0.0f32;
    for p in &pts[1..pts.len() - 1] {
        let v = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
        // |v × chord| / |chord| = distance from the chord line.
        let cx = [
            v[1] * chord[2] - v[2] * chord[1],
            v[2] * chord[0] - v[0] * chord[2],
            v[0] * chord[1] - v[1] * chord[0],
        ];
        let d = (cx[0].powi(2) + cx[1].powi(2) + cx[2].powi(2)).sqrt() / chord_len;
        max_off = max_off.max(d);
    }
    assert!(
        max_off > 0.05 * chord_len,
        "the spline must BOW off the chord (max offset {max_off}, chord {chord_len}) — straight segments are the 2D look we were told to leave behind"
    );

    // …and therefore the arc is strictly longer than the chord.
    let (_, total) = rate::arc_lengths(&pts);
    assert!(
        total > chord_len * 1.01,
        "a bowed arc is strictly longer than its chord: arc {total} vs chord {chord_len}"
    );
}

/// **The pulse phase advances with the clock, along ARC LENGTH.** A higher
/// rate earns a faster phase; equal clocks give equal phases (pure in the
/// clock, FC-7); and the pulse's 3D position glides monotonically along the
/// curve's cumulative arc length as the clock ticks — never jumping back
/// (within one cycle) and never interpolating along the chord.
#[test]
fn pulse_phase_advances_with_the_clock_along_arc_length() {
    // Speed: faster for a higher rate, bounded, pure.
    let (max, lo, hi) = (10.0f32, rate::pulse_speed_hz(1.0, 10.0), rate::pulse_speed_hz(10.0, 10.0));
    assert!(lo > 0.0, "a rated edge always moves (speed {lo} > 0)");
    assert!(hi > lo, "a higher rate pulses faster: {hi} vs {lo}");
    assert!(hi <= rate::PULSE_MAX_HZ + 1e-6 && lo >= rate::PULSE_MIN_HZ - 1e-6, "speeds stay in the readable band");
    let _ = max;

    // Phase: pure in the clock, advancing, fractional.
    let p0 = rate::pulse_phase(5.0, 10.0, 1.0);
    let p0b = rate::pulse_phase(5.0, 10.0, 1.0);
    assert_eq!(p0, p0b, "the phase is a pure function of the clock (FC-7)");
    let dt = 0.05f64;
    let p1 = rate::pulse_phase(5.0, 10.0, 1.0 + dt);
    assert!(p1 > p0, "the phase advances with the clock: {p1} vs {p0}");
    assert!((0.0..1.0).contains(&p0) && (0.0..1.0).contains(&p1), "fractional phase");
    let speed = rate::pulse_speed_hz(5.0, 10.0);
    assert!(
        (f64::from(p1 - p0) - dt * f64::from(speed)).abs() < 1e-4,
        "the clock drives the FRACTIONAL phase continuously (dt·speed), not per-frame steps"
    );

    // Position: the pulse glides along the curve's ARC LENGTH.
    let pts = rate::edge_curve([-0.9, 0.0, 0.0], [0.9, 0.0, 0.2], rate::CURVE_SEGMENTS);
    let (cum, total) = rate::arc_lengths(&pts);
    let q0 = rate::point_at(&pts, &cum, total, p0);
    let q1 = rate::point_at(&pts, &cum, total, p1);
    assert_ne!(q0, q1, "a ticked clock MOVES the pulse");
    // Arc-length monotonicity: walking frac 0 → 1 never retreats along the arc.
    let mut last = -1.0f32;
    for k in 0..=20 {
        let frac = k as f32 / 20.0;
        let q = rate::point_at(&pts, &cum, total, frac);
        // its arc coordinate = cum of the nearest sample at or before it
        let arc = cum
            .iter()
            .zip(&pts)
            .filter(|&(_, p)| {
                let d = (p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2);
                d < 1e-6 || true
            })
            .map(|(c, p)| {
                let d = ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt();
                (*c, d)
            })
            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
            .map(|(c, _)| c)
            .unwrap();
        assert!(arc >= last - total / rate::CURVE_SEGMENTS as f32, "the pulse never retreats along the arc");
        last = arc;
    }
    // Endpoints land exactly.
    assert_eq!(rate::point_at(&pts, &cum, total, 0.0), pts[0]);
    assert_eq!(rate::point_at(&pts, &cum, total, 1.0), *pts.last().unwrap());

    // And on the GRAPH: the injected clock (Msg::Tick) is what the overlay reads.
    let mut g = sample(3);
    g.set_rates(EdgeRates { edges: vec![Some(4.0), None, None] }).unwrap();
    let _ = g.update(Msg::SetClock(0.0));
    let c0 = g.state().clock_s.unwrap();
    let _ = g.update(Msg::Tick(0.25));
    let c1 = g.state().clock_s.unwrap();
    assert!(
        rate::pulse_phase(4.0, 4.0, c1) != rate::pulse_phase(4.0, 4.0, c0),
        "ticking the graph's injected clock moves the pulse phase"
    );
}

/// **The projected pulse curve carries DEPTH.** The painter draws from
/// `rated_edge_projection` — the sampled arc pushed through the SAME camera as
/// the cloud — so each sample arrives as `(screen, depth)`: finite screen
/// points, depths inside the projection's −1..1 band, and (for an arc that
/// spans the scene) genuinely VARYING depth, which is what the per-segment
/// depth shading shades. Unrated edges yield nothing.
#[test]
fn projected_pulse_positions_respect_depth() {
    let mut g = sample(6);
    g.set_rates(EdgeRates { edges: vec![Some(1.0), None, Some(2.0), None, None, None] }).unwrap();
    let center = Pos2::new(240.0, 180.0);
    let (scale, yaw) = (150.0f32, 0.9f32);

    let proj = g
        .rated_edge_projection(0, center, scale, yaw)
        .expect("a rated edge projects its curve");
    assert_eq!(proj.len(), rate::CURVE_SEGMENTS + 1, "every curve sample is projected");
    let mut depths: Vec<f32> = Vec::new();
    for (p, z) in &proj {
        assert!(p.x.is_finite() && p.y.is_finite(), "projected points are finite");
        assert!((-2.0..=2.0).contains(z), "depth stays in the projection band: {z}");
        depths.push(*z);
    }
    let (lo, hi) = depths.iter().fold((f32::MAX, f32::MIN), |(l, h), &d| (l.min(d), h.max(d)));
    assert!(hi > lo, "a 3D arc's samples differ in depth — flat depth would make the shading a lie");

    // The endpoints project exactly like the cloud's own nodes (same camera).
    let node_proj = g.project_all(center, scale, yaw);
    let (e0, e1) = (g.edges[0].0, g.edges[0].1);
    assert_eq!(proj[0].0, node_proj[e0].0, "the arc leaves the node where the cloud draws it");
    assert_eq!(proj.last().unwrap().0, node_proj[e1].0, "…and lands where the cloud draws the callee");

    assert!(g.rated_edge_projection(1, center, scale, yaw).is_none(), "an unrated edge draws nothing");
    assert!(g.rated_edge_projection(99, center, scale, yaw).is_none(), "out-of-range is nothing, not a panic");
}