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