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

nordisk/facett

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