facett-demo/src/demo_graph3d_given.rs
//! **The `graph3d-given` tab** — a [`Graph3D`] whose nodes are placed by the **HOST**
//! ([`Layout3D::Given`]), not by an index or a force relaxation.
//!
//! Every other [`Layout3D`] derives a position from the node INDEX (sphere / helix /
//! grid) or from the edge set (force). That is the right answer for a graph that
//! knows nothing about itself, and the wrong one for a graph whose nodes have a
//! **place in the world** and a **measurement** that earns the vertical axis. This
//! tab is that second case, drawn small enough to read:
//!
//! * **x / y ← a plausible 2-D map coordinate.** A synthetic traffic-sensor network
//! over a Stockholm-shaped grid of twelve named roads (six east-west, six
//! north-south), each carrying [`SENSORS_PER_ROAD`] sensors. The lon/lat is
//! projected by the HOST (a plain equirectangular scale here; korp runs its road
//! junctions through the same `mapquery::merc_xy` the 2-D map uses) — the crate
//! owns no projection.
//! * **z ← the DATA.** Each sensor's measured **speed** (km/h), mapped to the
//! crate's normalised `~[-1, 1]` cube. The three road classes (motorway ·
//! arterial · local) sit at three base speeds, so the cloud reads as **terraces**;
//! a congestion basin over the city centre pulls every road that crosses it down,
//! so each road reads as a **ribbon that dips**. Spin it and the shape is the
//! dataset — which is the whole point of `Given`, and exactly what a sphere or a
//! force blob destroys.
//! * **colour ← the same scalar**, green (free-flowing) → amber → red (jammed), so
//! the height and the hue agree and a still frame is readable too.
//!
//! The placement is **complete** (one position per node), so this tab reports
//! `layout_fallback: null`, `error_code: null` and [`Severity::Info`]. The short-
//! placement path — `layout_fallback: "grid"` + `facet-graph3d-5` +
//! [`Severity::Warning`] — is proven in `facett-graph3d/tests/given_positions.rs`;
//! a demo tab may not sit amber, so it is not demonstrated by shipping a broken one.
//! (The fallback is the O(n) **grid lattice**, not the force layout: a regular
//! lattice is unmistakably not-data, where a force blob looks like a graph somebody
//! meant — and `Graph3D::new` lays out once BEFORE `with_positions` can supply
//! anything, so the fallback has to be cheap.)
//!
//! Deterministic: closed-form trigonometry over the road/sensor indices, no RNG and
//! no wall clock, so the tab is byte-reproducible frame to frame.
//!
//! [`Severity::Info`]: facett_core::Severity
use egui::Color32;
use facett::graph3d::{Graph3D, Layout3D};
/// The stable tab title — the deck/discovery-contract key, the `LEGEND` key, the
/// mega-matrix cell key and the robot's `tab(..)` target, all one string.
pub const TITLE: &str = "graph3d-given";
/// East-west roads (constant latitude), named south → north.
const EW_ROADS: [&str; 6] =
["Ringvägen", "Hornsgatan", "Vasagatan", "Odengatan", "Sveavägen N", "Norrtull"];
/// North-south roads (constant longitude), named west → east.
const NS_ROADS: [&str; 6] =
["Essingeleden", "Fleminggatan", "Kungsgatan", "Birger Jarl", "Valhallavägen", "Lidingövägen"];
/// Sensors placed along EACH road. 12 roads × 26 = [`NODES`] sensors — enough that
/// the ribbons and terraces are legible when it spins, small enough to stay a demo.
pub const SENSORS_PER_ROAD: usize = 26;
/// Roads in the scene (6 east-west + 6 north-south).
pub const ROADS: usize = EW_ROADS.len() + NS_ROADS.len();
/// **The node count** — the number of `[x, y, z]` positions the host hands over, and
/// therefore the value `state_json()["given_positions"]` must report. The robot
/// test derives its assertion from this const rather than pinning a literal.
pub const NODES: usize = ROADS * SENSORS_PER_ROAD;
/// Free-flow speed (km/h) per road class: motorway · arterial · local. The three
/// values are what makes the cloud read as three TERRACES rather than one slab.
const CLASS_SPEED: [f32; 3] = [110.0, 70.0, 40.0];
/// The speed scale the z axis is normalised against (km/h → `~[-1, 1]`).
const SPEED_MAX: f32 = 120.0;
/// City centre in the projected plane, and the width of the congestion basin around
/// it. Every road crossing the basin is dragged down, so a ribbon DIPS instead of
/// running flat — the visible proof that z carries data and not an index.
const BASIN_CENTRE: [f32; 2] = [0.05, -0.05];
const BASIN_WIDTH: f32 = 0.55;
/// How much of its free-flow speed a sensor loses at the centre of the basin.
const BASIN_DEPTH: f32 = 0.62;
/// Approximate geographic frame of the synthetic city (Stockholm inner city).
const LON_MIN: f32 = 17.96;
const LON_MAX: f32 = 18.16;
const LAT_MIN: f32 = 59.29;
const LAT_MAX: f32 = 59.38;
/// **The HOST's projection** — equirectangular lon/lat → the crate's normalised
/// `~[-1, 1]` plane, with the cos(lat) x-scale that keeps the city the right shape.
/// It lives here, in the demo, because `facett-graph3d` owns no projection: that is
/// precisely the seam [`Layout3D::Given`] exists to open.
fn project(lon: f32, lat: f32) -> (f32, f32) {
let lat_mid = (LAT_MIN + LAT_MAX) * 0.5;
let k = lat_mid.to_radians().cos();
// Normalise longitude by the SAME span the latitude uses, scaled by cos(lat), so
// one degree of latitude and one of longitude keep their true ratio (a uniform
// scale — the city is not stretched to fill the box).
let lat_span = LAT_MAX - LAT_MIN;
let x = ((lon - (LON_MIN + LON_MAX) * 0.5) * k) / (lat_span * 0.5);
let y = (lat - lat_mid) / (lat_span * 0.5);
(x.clamp(-1.0, 1.0) * 0.92, y.clamp(-1.0, 1.0) * 0.92)
}
/// The measured speed (km/h) at a sensor: its road class' free-flow speed, minus the
/// congestion basin over the city centre, minus a small deterministic per-road wave
/// so two roads of the same class are not the same flat line.
fn speed_kmh(class: usize, road_seed: f32, t: f32, x: f32, y: f32) -> f32 {
let free = CLASS_SPEED[class];
let dx = x - BASIN_CENTRE[0];
let dy = y - BASIN_CENTRE[1];
let d2 = (dx * dx + dy * dy) / (BASIN_WIDTH * BASIN_WIDTH);
let basin = 1.0 - BASIN_DEPTH * (-d2).exp();
// A gentle along-the-road ripple (queue → clear → queue), phase-shifted per road.
let ripple = 1.0 + 0.10 * (t * std::f32::consts::TAU * 1.5 + road_seed).sin();
(free * basin * ripple).clamp(4.0, SPEED_MAX)
}
/// Speed → the crate's normalised z. Fast is HIGH: the motorway terrace sits on top,
/// the jammed basin sinks toward the floor.
fn speed_to_z(kmh: f32) -> f32 {
(kmh / SPEED_MAX).clamp(0.0, 1.0) * 1.8 - 0.9
}
/// Speed → colour: red (jammed) → amber → green (free-flowing), the same scalar the
/// height carries, so a still frame reads the same as a spinning one.
fn speed_color(kmh: f32) -> Color32 {
let t = (kmh / SPEED_MAX).clamp(0.0, 1.0);
if t < 0.5 {
// red → amber
let u = t / 0.5;
Color32::from_rgb(232, (70.0 + 130.0 * u) as u8, 60)
} else {
// amber → green
let u = (t - 0.5) / 0.5;
Color32::from_rgb((232.0 - 152.0 * u) as u8, (200.0 + 30.0 * u) as u8, (60.0 + 70.0 * u) as u8)
}
}
/// The whole scene, computed once: labelled + coloured nodes, the road/intersection
/// edge set, and the HOST placement (one `[x, y, z]` per node, in node order).
struct Scene {
nodes: Vec<(String, Color32)>,
edges: Vec<(usize, usize)>,
pos: Vec<[f32; 3]>,
}
fn scene() -> Scene {
let n = SENSORS_PER_ROAD;
let mut nodes: Vec<(String, Color32)> = Vec::with_capacity(NODES);
let mut pos: Vec<[f32; 3]> = Vec::with_capacity(NODES);
let mut edges: Vec<(usize, usize)> = Vec::new();
// The lon/lat each road is pinned to (evenly spread across the city frame,
// inset so no road sits exactly on the boundary).
let ew_lat = |i: usize| LAT_MIN + (LAT_MAX - LAT_MIN) * (i as f32 + 0.7) / (EW_ROADS.len() as f32 + 0.4);
let ns_lon = |j: usize| LON_MIN + (LON_MAX - LON_MIN) * (j as f32 + 0.7) / (NS_ROADS.len() as f32 + 0.4);
// ── the twelve roads, each a run of `SENSORS_PER_ROAD` sensors ────────────
for r in 0..ROADS {
let east_west = r < EW_ROADS.len();
let idx = if east_west { r } else { r - EW_ROADS.len() };
let name = if east_west { EW_ROADS[idx] } else { NS_ROADS[idx] };
// Class cycles motorway · arterial · local, so the three terraces interleave
// across the map instead of stacking on one side of it.
let class = r % CLASS_SPEED.len();
let seed = r as f32 * 1.37;
for s in 0..n {
let t = s as f32 / (n - 1) as f32;
let (lon, lat) = if east_west {
// A slight sinusoidal wander so a road is a real polyline, not a ruler.
let lat = ew_lat(idx) + 0.004 * (t * std::f32::consts::TAU + seed).sin();
(LON_MIN + (LON_MAX - LON_MIN) * t, lat)
} else {
let lon = ns_lon(idx) + 0.008 * (t * std::f32::consts::TAU + seed).sin();
(lon, LAT_MIN + (LAT_MAX - LAT_MIN) * t)
};
let (x, y) = project(lon, lat);
let kmh = speed_kmh(class, seed, t, x, y);
pos.push([x, y, speed_to_z(kmh)]);
nodes.push((format!("{name} · {:.0} km/h", kmh), speed_color(kmh)));
if s > 0 {
let base = r * n;
edges.push((base + s - 1, base + s));
}
}
}
// ── intersections: every east-west road crosses every north-south road ────
// The crossing sensor pair is found in closed form from the pinned coordinate,
// so the wiring follows the geometry instead of a hand-typed table.
let last = (n - 1) as f32;
for i in 0..EW_ROADS.len() {
for j in 0..NS_ROADS.len() {
let t_on_ew = ((ns_lon(j) - LON_MIN) / (LON_MAX - LON_MIN)).clamp(0.0, 1.0);
let t_on_ns = ((ew_lat(i) - LAT_MIN) / (LAT_MAX - LAT_MIN)).clamp(0.0, 1.0);
let a = i * n + (t_on_ew * last).round() as usize;
let b = (EW_ROADS.len() + j) * n + (t_on_ns * last).round() as usize;
edges.push((a, b));
}
}
Scene { nodes, edges, pos }
}
/// Build the `graph3d-given` tab: [`NODES`] traffic sensors over a synthetic
/// Stockholm road grid, placed by the HOST — x/y from the projected lon/lat, z from
/// the measured speed — and handed to [`Graph3D`] as [`Layout3D::Given`].
///
/// The placement covers every node, so the pane draws exactly what was asked for:
/// `state_json()["given_positions"] == NODES`, `layout == "given"`,
/// `layout_fallback == null`, `error_code == null` — never the grid fallback.
pub fn demo_graph3d_given() -> Graph3D {
let Scene { nodes, edges, pos } = scene();
debug_assert_eq!(nodes.len(), NODES);
debug_assert_eq!(pos.len(), NODES);
Graph3D::new(nodes, edges, Layout3D::Given).with_title(TITLE).with_positions(pos)
}
#[cfg(test)]
mod tests {
use super::*;
use facett::Facet;
/// The host placement covers the cloud: the pane draws `given`, reports every
/// position, and raises neither the fallback nor `facet-graph3d-5`.
#[test]
fn the_host_placement_is_complete_and_drawn() {
let g = demo_graph3d_given();
assert!(g.placement_is_complete(), "the demo must ship a COMPLETE placement");
let j = g.state_json();
assert_eq!(j["layout"], "given");
assert_eq!(j["given_positions"], NODES as u64);
assert_eq!(j["nodes"], NODES as u64);
assert!(j["layout_fallback"].is_null(), "nothing degraded: {}", j["layout_fallback"]);
assert!(j["error_code"].is_null(), "no facet-graph3d-5 here: {}", j["error_code"]);
assert_eq!(g.severity(), facett_core::Severity::Info);
}
/// z carries DATA, not an index: the three road classes occupy three separated
/// height bands, and the congestion basin drags the centre of the map below the
/// free-flowing edges. A blob (or an index-derived layout) fails both.
#[test]
fn the_z_axis_is_the_speed_scalar_not_an_index() {
let s = scene();
// Three terraces: sample the free-flowing END of a road of each class (the
// basin only bites in the middle), and assert they are ORDERED and apart.
let z_at = |road: usize, sensor: usize| s.pos[road * SENSORS_PER_ROAD + sensor][2];
let (motorway, arterial, local) = (z_at(0, 0), z_at(1, 0), z_at(2, 0));
assert!(motorway > arterial + 0.15, "motorway terrace sits above the arterial one");
assert!(arterial > local + 0.15, "arterial terrace sits above the local one");
// The basin: the mid-road sensor of a road crossing the centre is far below
// its own free-flowing end.
let mid = SENSORS_PER_ROAD / 2;
let dip = z_at(2, 0) - z_at(2, mid);
assert!(dip > 0.10, "the congestion basin pulls the ribbon down, got {dip}");
// Not a plane and not a blob: real spread on every axis.
for axis in 0..3 {
let lo = s.pos.iter().map(|p| p[axis]).fold(f32::INFINITY, f32::min);
let hi = s.pos.iter().map(|p| p[axis]).fold(f32::NEG_INFINITY, f32::max);
assert!(hi - lo > 0.5, "axis {axis} carries real extent, got {}", hi - lo);
assert!(lo >= -1.05 && hi <= 1.05, "axis {axis} stays in the normalised cube");
}
}
/// The scene is wired like a road network: every road is a chain, and every
/// east-west road meets every north-south one.
#[test]
fn the_roads_are_chained_and_the_intersections_are_wired() {
let s = scene();
assert_eq!(s.nodes.len(), NODES);
assert_eq!(s.edges.len(), ROADS * (SENSORS_PER_ROAD - 1) + EW_ROADS.len() * NS_ROADS.len());
for (a, b) in &s.edges {
assert!(*a < NODES && *b < NODES, "edge {a}->{b} is in range");
}
}
/// Deterministic — no RNG, no wall clock: two builds are identical.
#[test]
fn the_scene_is_deterministic() {
assert_eq!(scene().pos, scene().pos);
}
}