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

nordisk/dwarves

dwarves-gui/src/coderoom3d.rs

//! **The 🏛 Code room** — the Mímir graph as a map you fly over (owner, 2026-10-16:
//! "the parsing of many crates to a graph … I wanted the 3D room to be that graph";
//! and of the Wreck-It Ralph code room: "function heads and connections from other
//! functions, a 3d graph and under the function head … attributes/vars").
//!
//! Semantic zoom, map style:
//!
//! * **far** — the consumed repos as regions, their crates as plates (the
//!   dependencies further out, dimmer), crate → crate edges bundled with their counts;
//! * **near** — on a plate, every function a CARD: its head carries its name, under it
//!   hang its params, its return type, its locals and the fields it reads, then its call
//!   sites in source order; from each call site's marker a SPLINE leaves the body
//!   sideways and lands on the top of the callee's head (tinted by where the callee
//!   lives: this crate · another crate · another repo · a dependency);
//! * resting the pointer on a head unfolds its **source** from the commit the room was
//!   read at, half transparent, the call lines marked and tied to their markers.
//!
//! Everything placed here is the SERVER's: one `Viz.CodeMap` answer per level of
//! detail — the world, the card tiles the camera sees, a search, a source page —
//! through the forge (`CodeRoom.Answer`, [`crate::room`]), each a jera job on the
//! server, kept per room version. Nothing here lays out, resolves or reads a file:
//! this module turns the answer's rects into a 3-D scene ([`build_scene`], pure and
//! tested) and draws it — on the GPU through facett's 3-D scene path when the host
//! installed it, else with the CPU painter.
//!
//! The camera is an orbit camera on facett's shared flight ([`facett_core::fly`]):
//! 📌 Stay / 🎯 Focus, ⊙ fit, a smooth fly-to; the wheel (and a pinch) zooms to the
//! cursor and is CONSUMED by the canvas, so no surrounding scroll area takes it.

use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;

use egui::{Color32, Pos2, Rect, Vec2};
use facett_core::fly::{Flight, NavMode};
use facett_core::Facet;
use serde_json::Value;

use crate::room::{self, ForgeRoom, Pending, RoomAnswer, RoomSource, RoomState};

/// The reader surface.
pub const S_CODE_MAP: &str = "Viz.CodeMap";
/// Below this camera distance the cards of the tiles in view are asked and drawn.
pub const CARDS_DIST: f32 = 160.0;
/// Below this camera distance a plate shows its modules (and their types).
pub const MODULES_DIST: f32 = 700.0;
/// Plates whose modules are asked at once at most (nearest the target first).
pub const PLATES_MAX: usize = 16;
/// Card tiles asked at once at most (nearest the camera's target first).
pub const TILES_MAX: usize = 12;
/// A resting pointer unfolds a head's source after this long (seconds).
pub const HOVER_OPEN_S: f64 = 0.3;
/// The vertical field of view (radians).
pub const FOVY: f32 = 0.8;
/// Monospace advance, in ems (the map's text is Hack; 1233 / 2048).
pub const ADV: f32 = facett_core::render::scene3d::MONO_ADVANCE;
/// Text whose em is smaller than this on screen is not drawn (px).
pub const MIN_TEXT_PX: f32 = 6.0;
/// Source lines a panel shows at once.
pub const PANEL_LINES: usize = 28;
/// Source panel em (world units) and its width in chars.
pub const PANEL_EM: f32 = 0.6;
pub const PANEL_COLS: usize = 84;
/// A queued answer is given up (said, not asked again) after this long (seconds).
pub const GIVE_UP_S: f64 = 90.0;
/// Retry a queued answer after this long (seconds).
const RETRY_S: f64 = 1.5;
/// Head text em and row text em (world units).
const HEAD_EM: f32 = 0.62;
const ROW_EM: f32 = 0.38;
/// World-level edges drawn at most (the heaviest).
const WORLD_EDGES_MAX: usize = 250;

// ------------------------------------------------------------------ the answer

/// The card geometry the server laid out with.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Geometry {
    pub card_w: f32,
    pub head_h: f32,
    pub row_h: f32,
}

impl Default for Geometry {
    fn default() -> Self {
        Self { card_w: 8.0, head_h: 1.2, row_h: 0.6 }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct Plate {
    pub id: String,
    pub name: String,
    pub repo: Option<String>,
    pub local: bool,
    pub rect: [f32; 4],
    /// The band its name lies on, at its top (world units).
    pub title: f32,
    pub cards: u64,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct World {
    pub version: u64,
    pub bounds: [f32; 4],
    pub tile: f32,
    pub geometry: Geometry,
    /// (name, rect, the band its name lies on)
    pub repos: Vec<(String, [f32; 4], f32)>,
    pub crates: Vec<Plate>,
    /// (from plate, to plate, depends_on, calls, call_sites)
    pub edges: Vec<(usize, usize, u64, u64, u64)>,
    pub tiles: BTreeSet<(i32, i32)>,
    pub total_cards: u64,
    /// `crate` or `role`.
    pub layout: String,
    /// By role: each tile's main crate and its cards (tile x, tile z, crate, cards).
    pub tints: Vec<(i32, i32, usize, u64)>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Row {
    pub k: String,
    pub name: String,
    pub ty: Option<String>,
    pub line: Option<u32>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct SiteRow {
    /// The callee head's height (by role).
    pub to_y: f32,
    pub to: String,
    pub to_name: String,
    pub to_card: u64,
    pub to_at: [f32; 2],
    pub rel: String,
    pub lines: Vec<u32>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct CardView {
    pub card: u64,
    /// Its height above the plane (by role: the third axis; by crate 0).
    pub y: f32,
    pub id: String,
    pub path: String,
    pub name: String,
    pub krate: usize,
    pub home: bool,
    pub rect: [f32; 4],
    pub start_line: Option<u32>,
    pub rows: Vec<Row>,
    pub more_rows: u64,
    pub sites: Vec<SiteRow>,
    pub more_sites: u64,
}

fn f(v: &Value) -> f32 {
    v.as_f64().unwrap_or(0.0) as f32
}

fn rect4(v: &Value) -> [f32; 4] {
    [f(&v[0]), f(&v[1]), f(&v[2]), f(&v[3])]
}

fn geometry_of(v: &Value) -> Geometry {
    let d = Geometry::default();
    Geometry {
        card_w: v["card_w"].as_f64().map_or(d.card_w, |x| x as f32),
        head_h: v["head_h"].as_f64().map_or(d.head_h, |x| x as f32),
        row_h: v["row_h"].as_f64().map_or(d.row_h, |x| x as f32),
    }
}

/// The world of a measured answer, or the refusal line of one that is not.
pub fn world_of(a: &RoomAnswer) -> Result<World, String> {
    if !a.is_measured() {
        return Err(format!("[{}] {}", a.state, a.evidence_text()));
    }
    let b = &a.body;
    let arr = |k: &str| b[k].as_array().cloned().unwrap_or_default();
    Ok(World {
        version: b["version"].as_u64().unwrap_or(0),
        bounds: rect4(&b["bounds"]),
        tile: b["tile"].as_f64().unwrap_or(128.0) as f32,
        geometry: geometry_of(&b["geometry"]),
        repos: arr("repos").iter().map(|r| (r["name"].as_str().unwrap_or("").to_string(), rect4(&r["rect"]), r["title"].as_f64().unwrap_or(12.0) as f32)).collect(),
        crates: arr("crates")
            .iter()
            .map(|c| Plate {
                id: c["id"].as_str().unwrap_or("").to_string(),
                name: c["name"].as_str().unwrap_or("").to_string(),
                repo: c["repo"].as_str().map(str::to_string),
                local: c["local"].as_bool().unwrap_or(false),
                rect: rect4(&c["rect"]),
                title: c["title"].as_f64().unwrap_or(5.0) as f32,
                cards: c["cards"].as_u64().unwrap_or(0),
            })
            .collect(),
        edges: arr("edges")
            .iter()
            .filter_map(|e| Some((e[0].as_u64()? as usize, e[1].as_u64()? as usize, e[2].as_u64()?, e[3].as_u64()?, e[4].as_u64()?)))
            .collect(),
        tiles: arr("tiles").iter().filter_map(|t| Some((t[0].as_i64()? as i32, t[1].as_i64()? as i32))).collect(),
        total_cards: b["total_cards"].as_u64().unwrap_or(0),
        layout: b["layout"].as_str().unwrap_or("crate").to_string(),
        tints: arr("tile_tints").iter().filter_map(|t| Some((t[0].as_i64()? as i32, t[1].as_i64()? as i32, t[2].as_u64()? as usize, t[3].as_u64()?))).collect(),
    })
}

/// One module block on a plate.
#[derive(Debug, Clone, PartialEq)]
pub struct ModuleView {
    pub name: String,
    pub rect: [f32; 4],
    pub cards: u64,
    /// (name, kind) of the types it defines, capped by the server.
    pub types: Vec<(String, String)>,
    pub total_types: u64,
}

/// The module blocks of a measured `modules` answer.
pub fn modules_of(a: &RoomAnswer) -> Result<Vec<ModuleView>, String> {
    if !a.is_measured() {
        return Err(format!("[{}] {}", a.state, a.evidence_text()));
    }
    Ok(a.body["modules"]
        .as_array()
        .into_iter()
        .flatten()
        .map(|m| ModuleView {
            name: m["name"].as_str().unwrap_or("").to_string(),
            rect: rect4(&m["rect"]),
            cards: m["cards"].as_u64().unwrap_or(0),
            types: m["types"].as_array().into_iter().flatten().map(|t| (t[0].as_str().unwrap_or("").to_string(), t[1].as_str().unwrap_or("").to_string())).collect(),
            total_types: m["total_types"].as_u64().unwrap_or(0),
        })
        .collect())
}

/// The cards of a measured tile answer.
pub fn cards_of(a: &RoomAnswer) -> Result<Vec<CardView>, String> {
    if !a.is_measured() {
        return Err(format!("[{}] {}", a.state, a.evidence_text()));
    }
    let s = |v: &Value| v.as_str().unwrap_or("").to_string();
    Ok(a.body["cards"]
        .as_array()
        .cloned()
        .unwrap_or_default()
        .iter()
        .map(|c| CardView {
            card: c["card"].as_u64().unwrap_or(0),
            y: f(&c["y"]),
            id: s(&c["id"]),
            path: s(&c["path"]),
            name: s(&c["name"]),
            krate: c["crate"].as_u64().unwrap_or(0) as usize,
            home: c["home"].as_bool().unwrap_or(false),
            rect: rect4(&c["rect"]),
            start_line: c["start_line"].as_u64().map(|n| n as u32),
            rows: c["rows"]
                .as_array()
                .cloned()
                .unwrap_or_default()
                .iter()
                .map(|r| Row { k: s(&r["k"]), name: s(&r["name"]), ty: r["ty"].as_str().map(str::to_string), line: r["line"].as_u64().map(|n| n as u32) })
                .collect(),
            more_rows: c["more_rows"].as_u64().unwrap_or(0),
            sites: c["sites"]
                .as_array()
                .cloned()
                .unwrap_or_default()
                .iter()
                .map(|x| SiteRow {
                    to_y: f(&x["to_y"]),
                    to: s(&x["to"]),
                    to_name: s(&x["to_name"]),
                    to_card: x["to_card"].as_u64().unwrap_or(u64::MAX),
                    to_at: [f(&x["to_at"][0]), f(&x["to_at"][1])],
                    rel: s(&x["rel"]),
                    lines: x["lines"].as_array().map(|l| l.iter().filter_map(|n| n.as_u64().map(|n| n as u32)).collect()).unwrap_or_default(),
                })
                .collect(),
            more_sites: c["more_sites"].as_u64().unwrap_or(0),
        })
        .collect())
}

// ------------------------------------------------------------------ the camera

/// An orbit camera over the map plane (y up; the map's z runs down the screen at yaw 0).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Camera {
    pub target: [f32; 3],
    pub dist: f32,
    pub yaw: f32,
    pub pitch: f32,
}

fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn mul(a: [f32; 3], k: f32) -> [f32; 3] {
    [a[0] * k, a[1] * k, a[2] * k]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn norm(a: [f32; 3]) -> [f32; 3] {
    let l = dot(a, a).sqrt().max(1e-9);
    mul(a, 1.0 / l)
}

impl Camera {
    /// Looking down on `bounds` (x0, z0, x1, z1) so it fills a view.
    pub fn fit(bounds: [f32; 4]) -> Self {
        let (w, d) = ((bounds[2] - bounds[0]).max(1.0), (bounds[3] - bounds[1]).max(1.0));
        // never inside the cards' distance: the whole map opens far, on its crates
        let dist = (w.max(d) * 1.35 + 20.0).max(CARDS_DIST * 1.25);
        Camera { target: [(bounds[0] + bounds[2]) * 0.5, 0.0, (bounds[1] + bounds[3]) * 0.5], dist, yaw: 0.0, pitch: 1.0 }
    }

    pub fn eye(&self) -> [f32; 3] {
        let (cp, sp) = (self.pitch.cos(), self.pitch.sin());
        add(self.target, mul([self.yaw.sin() * cp, sp, self.yaw.cos() * cp], self.dist))
    }

    /// (forward, right, up) unit vectors.
    pub fn basis(&self) -> ([f32; 3], [f32; 3], [f32; 3]) {
        let fwd = norm(sub(self.target, self.eye()));
        let right = norm(cross(fwd, [0.0, 1.0, 0.0]));
        let up = cross(right, fwd);
        (fwd, right, up)
    }

    /// World → screen (in `rect`) and the depth along the view; `None` behind the eye.
    pub fn project(&self, p: [f32; 3], rect: Rect) -> Option<(Pos2, f32)> {
        let (fwd, right, up) = self.basis();
        let d = sub(p, self.eye());
        let z = dot(d, fwd);
        if z < 0.05 {
            return None;
        }
        let k = rect.height() * 0.5 / (FOVY * 0.5).tan();
        Some((Pos2::new(rect.center().x + dot(d, right) / z * k, rect.center().y - dot(d, up) / z * k), z))
    }

    /// The ray through a screen point: (origin, unit direction).
    pub fn ray(&self, s: Pos2, rect: Rect) -> ([f32; 3], [f32; 3]) {
        let (fwd, right, up) = self.basis();
        let k = rect.height() * 0.5 / (FOVY * 0.5).tan();
        let (x, y) = ((s.x - rect.center().x) / k, -(s.y - rect.center().y) / k);
        (self.eye(), norm(add(fwd, add(mul(right, x), mul(up, y)))))
    }

    /// Where a screen point meets the map plane (y = 0), if it does.
    pub fn ground(&self, s: Pos2, rect: Rect) -> Option<[f32; 3]> {
        let (o, d) = self.ray(s, rect);
        if d[1] > -1e-4 {
            return None;
        }
        let t = -o[1] / d[1];
        Some(add(o, mul(d, t)))
    }

    /// **Zoom to the cursor**: distance × `factor`, the map point under `at` stays under it.
    pub fn zoom_at(&mut self, factor: f32, at: Pos2, rect: Rect) {
        let before = self.ground(at, rect);
        self.dist = (self.dist * factor).clamp(4.0, 200_000.0);
        if let (Some(b), Some(a)) = (before, self.ground(at, rect)) {
            self.target = add(self.target, sub(b, a));
        }
    }
}

/// The camera a flight state stands for.
fn cam_from(c: ([f64; 3], f64), yaw: f32, pitch: f32) -> Camera {
    Camera { target: [c.0[0] as f32, c.0[1] as f32, c.0[2] as f32], dist: c.1 as f32, yaw, pitch }
}

// ------------------------------------------------------------------ the scene

/// A flat parallelogram `origin + s·u + t·v` (s, t in 0..1).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quad {
    pub origin: [f32; 3],
    pub u: [f32; 3],
    pub v: [f32; 3],
    pub color: Color32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line {
    pub a: [f32; 3],
    pub b: [f32; 3],
    pub width_px: f32,
    pub color: Color32,
}

/// Flat text on a plane: baseline-left at `origin`, one em along `right` / `up`.
#[derive(Debug, Clone, PartialEq)]
pub struct Text {
    pub origin: [f32; 3],
    pub right: [f32; 3],
    pub up: [f32; 3],
    pub text: String,
    pub color: Color32,
}

/// One spline: a call site's marker → the callee's head.
#[derive(Debug, Clone, PartialEq)]
pub struct Spline {
    pub from_card: u64,
    pub to_card: u64,
    pub marker: [f32; 3],
    pub head: [f32; 3],
    pub points: Vec<[f32; 3]>,
    pub color: Color32,
    pub lit: bool,
}

/// What a head is on screen (for picking and for a robot to point at).
#[derive(Debug, Clone, PartialEq)]
pub struct HeadPick {
    pub card: u64,
    pub corners: [[f32; 3]; 4],
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct Scene {
    pub quads: Vec<Quad>,
    pub lines: Vec<Line>,
    pub texts: Vec<Text>,
    /// Translucent sheets (an unfolded source panel): on the GPU drawn last without
    /// writing depth, so a spline behind one shows through it.
    pub sheets: Vec<Quad>,
    /// The words on the sheets, drawn after them.
    pub sheet_texts: Vec<Text>,
    pub splines: Vec<Spline>,
    pub heads: Vec<HeadPick>,
    /// Per drawn plate: its index and corners (for a click that flies into it).
    pub plates: Vec<(usize, [[f32; 3]; 4])>,
}

/// A crate's colour: stable per name; a dependency's is grey.
pub fn crate_color(name: &str, local: bool) -> Color32 {
    if !local {
        return Color32::from_rgb(0x70, 0x76, 0x86);
    }
    let h = name.bytes().fold(2166136261u32, |h, b| (h ^ b as u32).wrapping_mul(16777619));
    Color32::from(egui::ecolor::Hsva::new((h % 360) as f32 / 360.0, 0.55, 0.85, 1.0))
}

/// A spline's tint by where its callee lives.
pub fn rel_color(rel: &str, own: Color32) -> Color32 {
    match rel {
        "same" => own,
        "crate" => Color32::from_rgb(0x4f, 0xc3, 0xf7),
        "repo" => Color32::from_rgb(0xff, 0xb7, 0x4d),
        _ => Color32::from_rgb(0x9e, 0x9e, 0x9e),
    }
}

/// `text` cut to `cols` chars, the start elided (a path's tail is its name).
pub fn fit_tail(text: &str, cols: usize) -> String {
    let n = text.chars().count();
    if n <= cols {
        return text.to_string();
    }
    let keep: String = text.chars().skip(n - cols.saturating_sub(1)).collect();
    format!("…{keep}")
}

/// `text` cut to `cols` chars, the end elided.
pub fn fit_head(text: &str, cols: usize) -> String {
    if text.chars().count() <= cols {
        return text.to_string();
    }
    let mut s: String = text.chars().take(cols.saturating_sub(1)).collect();
    s.push('…');
    s
}

/// A head's label: the last two segments of its path (`module::name`), fitted.
pub fn head_label(path: &str, name: &str, cols: usize) -> String {
    let segs: Vec<&str> = path.split("::").collect();
    let short = if segs.len() >= 2 {
        segs[segs.len() - 2..].join("::")
    } else if path.is_empty() {
        name.to_string()
    } else {
        path.to_string()
    };
    fit_tail(&short, cols)
}

/// One attribute row's words.
pub fn row_text(r: &Row) -> String {
    let ty = r.ty.as_deref().unwrap_or("?");
    match r.k.as_str() {
        "param" => format!("{}: {ty}", r.name),
        "ret" => format!("-> {ty}"),
        "local" => format!("let {}: {ty}", r.name),
        _ => format!(".{}: {ty}", r.name),
    }
}

/// The text colour of a row kind.
fn row_color(k: &str) -> Color32 {
    match k {
        "param" => Color32::from_rgb(0xb3, 0xe5, 0xfc),
        "ret" => Color32::from_rgb(0xff, 0xe0, 0x82),
        "local" => Color32::from_rgb(0xc5, 0xe1, 0xa5),
        _ => Color32::from_rgb(0xf8, 0xbb, 0xd0),
    }
}

/// The row index (below the head) of a card's `k`-th call-site row.
pub fn site_row_index(c: &CardView, k: usize) -> usize {
    c.rows.len() + usize::from(c.more_rows > 0) + k
}

/// The point on a card's right edge where its `k`-th call site's spline leaves.
pub fn marker_at(c: &CardView, g: Geometry, k: usize) -> [f32; 3] {
    let z = c.rect[1] + g.head_h + (site_row_index(c, k) as f32 + 0.5) * g.row_h;
    [c.rect[2], 0.06 + c.y, z]
}

/// The top middle of a card's head: where splines to it land.
pub fn head_top(rect: [f32; 4]) -> [f32; 3] {
    [(rect[0] + rect[2]) * 0.5, 0.06, rect[1]]
}

/// **The spline** from a call site's marker to a head: a cubic Bézier that leaves the
/// body sideways (+x) and rises, and comes down onto the head's top edge from above.
pub fn spline_points(a: [f32; 3], b: [f32; 3], n: usize) -> Vec<[f32; 3]> {
    let l = ((b[0] - a[0]).powi(2) + (b[2] - a[2]).powi(2)).sqrt();
    let h = (l * 0.35).clamp(1.5, 400.0);
    let c1 = [a[0] + (l * 0.3).clamp(2.0, 60.0), a[1] + h, a[2]];
    let c2 = [b[0], b[1] + h, b[2] - (l * 0.2).clamp(1.0, 40.0)];
    (0..=n)
        .map(|i| {
            let t = i as f32 / n as f32;
            let s = 1.0 - t;
            let w = [s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t];
            [0, 1, 2].map(|j| w[0] * a[j] + w[1] * c1[j] + w[2] * c2[j] + w[3] * b[j])
        })
        .collect()
}

/// A low arc between two plates (height `rise` × the distance), for the far map.
pub fn arc_points(a: [f32; 3], b: [f32; 3], n: usize, rise: f32) -> Vec<[f32; 3]> {
    let l = ((b[0] - a[0]).powi(2) + (b[2] - a[2]).powi(2)).sqrt();
    (0..=n)
        .map(|i| {
            let t = i as f32 / n as f32;
            [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t + 4.0 * t * (1.0 - t) * l * rise, a[2] + (b[2] - a[2]) * t]
        })
        .collect()
}

/// A point lifted by `y`.
pub fn lift(p: [f32; 3], y: f32) -> [f32; 3] {
    [p[0], p[1] + y, p[2]]
}

/// Lift what was pushed since `from` (quads, texts, heads) by `y` — a card built on
/// the plane, raised to its height.
fn lift_since(sc: &mut Scene, from: (usize, usize, usize), y: f32) {
    if y == 0.0 {
        return;
    }
    for q in &mut sc.quads[from.0..] {
        q.origin[1] += y;
    }
    for t in &mut sc.texts[from.1..] {
        t.origin[1] += y;
    }
    for h in &mut sc.heads[from.2..] {
        for c in &mut h.corners {
            c[1] += y;
        }
    }
}

/// What is lit: a card (hovered or selected) lights its own splines and those into it.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Lit {
    pub card: Option<u64>,
}

/// A flat text on the map plane (reading along +x, its top towards −z).
fn flat_text(x: f32, z: f32, em: f32, text: String, color: Color32) -> Text {
    Text { origin: [x, 0.08, z], right: [em, 0.0, 0.0], up: [0.0, 0.0, -em], text, color }
}

/// A flat rect on the map plane at height `y`.
fn flat_quad(r: [f32; 4], y: f32, color: Color32) -> Quad {
    Quad { origin: [r[0], y, r[1]], u: [r[2] - r[0], 0.0, 0.0], v: [0.0, 0.0, r[3] - r[1]], color }
}

/// **Build the scene** — pure: the world's regions and plates (with their names lying
/// flat on them), its bundled crate edges when far, and when near the cards of the
/// tiles held, their rows under their heads, their call-site markers and the splines
/// to the heads they call.
pub fn build_scene(world: &World, cards: &[&CardView], mods: &[&ModuleView], near: bool, lit: Lit) -> Scene {
    let mut sc = Scene::default();
    let g = world.geometry;
    // names lie flat in their bands, as big as the band (and the width) allows: a big
    // region or crate is named from as far as it is seen
    for (name, r, band) in &world.repos {
        sc.quads.push(flat_quad(*r, -0.2, Color32::from_rgba_unmultiplied(0x30, 0x36, 0x48, 120)));
        let em = ((r[2] - r[0]) * 0.9 / (name.chars().count().max(1) as f32 * ADV)).min(band * 0.7);
        sc.texts.push(flat_text(r[0] + 2.0, r[1] + (band + em * 0.7) * 0.5 + 1.0, em, name.clone(), Color32::from_rgb(0xcf, 0xd8, 0xdc)));
    }
    // by role there are no plates: each tile is tinted by its main crate, as strong as it is full
    for &(x, z, k, n) in &world.tints {
        let t = world.tile;
        let col = world.crates.get(k).map_or(Color32::GRAY, |p| crate_color(&p.name, p.local));
        sc.quads.push(flat_quad([x as f32 * t, z as f32 * t, (x + 1) as f32 * t, (z + 1) as f32 * t], -0.1, col.gamma_multiply(0.08 + 0.32 * (n as f32 / 60.0).min(1.0))));
    }
    for (i, p) in world.crates.iter().enumerate() {
        if p.rect[2] <= p.rect[0] {
            continue; // by role: a crate is a tint, not a plate
        }
        let c = crate_color(&p.name, p.local);
        sc.quads.push(flat_quad(p.rect, -0.1, c.gamma_multiply(if p.local { 0.30 } else { 0.18 })));
        sc.plates.push((i, [[p.rect[0], 0.0, p.rect[1]], [p.rect[2], 0.0, p.rect[1]], [p.rect[2], 0.0, p.rect[3]], [p.rect[0], 0.0, p.rect[3]]]));
        let label = format!("{} · {}", p.name, p.cards);
        let em = ((p.rect[2] - p.rect[0] - 2.0) / (label.chars().count().max(1) as f32 * ADV)).min(p.title * 0.7);
        sc.texts.push(flat_text(p.rect[0] + 1.0, p.rect[1] + (p.title + em * 0.7) * 0.5 + 0.5, em, label, c));
    }
    // a plate up close: its module blocks, each named; the types it defines while the
    // cards are not drawn yet
    for m in mods {
        let r = m.rect;
        sc.quads.push(flat_quad(r, -0.05, Color32::from_rgba_unmultiplied(0x2a, 0x30, 0x3e, 200)));
        let short = m.name.rsplit("::").next().unwrap_or(&m.name);
        let label = format!("{short} · {} fn{}", m.cards, if m.cards == 1 { "" } else { "s" });
        let em = ((r[2] - r[0] - 1.0) / (label.chars().count().max(1) as f32 * ADV)).min(1.4);
        sc.texts.push(flat_text(r[0] + 0.5, r[1] + 0.4 + em, em, label, Color32::from_rgb(0xb0, 0xbe, 0xc5)));
        if !near && !m.types.is_empty() {
            let more = m.total_types.saturating_sub(m.types.len() as u64);
            let words = m.types.iter().map(|(n, k)| format!("{k} {n}")).collect::<Vec<_>>().join(" · ") + &if more > 0 { format!(" · +{more}") } else { String::new() };
            let tem = (em * 0.6).min(0.8);
            let cols = ((r[2] - r[0] - 1.0) / (tem * ADV)).floor() as usize;
            sc.texts.push(flat_text(r[0] + 0.5, r[1] + 0.6 + em + tem * 1.2, tem, fit_head(&words, cols), Color32::from_rgb(0xf8, 0xbb, 0xd0)));
        }
    }
    if !near {
        // the workspace's own crate → crate edges: every crate calls std, and a fan of
        // those into the dependencies says nothing (a dependency's edges show up close)
        let mut edges: Vec<&(usize, usize, u64, u64, u64)> = world.edges.iter().filter(|e| e.0 < world.crates.len() && e.1 < world.crates.len() && world.crates[e.0].local && world.crates[e.1].local).collect();
        edges.sort_by(|a, b| (b.3 + b.2).cmp(&(a.3 + a.2)));
        for e in edges.into_iter().take(WORLD_EDGES_MAX) {
            let (a, b) = (world.crates[e.0].rect, world.crates[e.1].rect);
            let pa = [(a[0] + a[2]) * 0.5, 0.0, (a[1] + a[3]) * 0.5];
            let pb = [(b[0] + b[2]) * 0.5, 0.0, (b[1] + b[3]) * 0.5];
            let pts = arc_points(pa, pb, 16, 0.12);
            let w = 1.0 + ((e.3 + e.2) as f32).log2().max(0.0) * 0.3;
            let col = if e.3 > 0 { Color32::from_rgba_unmultiplied(0x4f, 0xc3, 0xf7, 150) } else { Color32::from_rgba_unmultiplied(0x9e, 0x9e, 0x9e, 110) };
            for s in pts.windows(2) {
                sc.lines.push(Line { a: s[0], b: s[1], width_px: w, color: col });
            }
        }
        return sc;
    }
    let by_card: HashMap<u64, &CardView> = cards.iter().map(|c| (c.card, *c)).collect();
    let lit_card = lit.card;
    for c in cards {
        // a card is built on the plane, then lifted to its height (by role)
        let (q0, t0, h0) = (sc.quads.len(), sc.texts.len(), sc.heads.len());
        let own = world.crates.get(c.krate).map_or(Color32::GRAY, |p| crate_color(&p.name, p.local));
        let r = c.rect;
        // the body, then the head band over it
        sc.quads.push(flat_quad(r, 0.0, Color32::from_rgba_unmultiplied(0x1c, 0x22, 0x2e, 235)));
        let head = [r[0], r[1], r[2], r[1] + g.head_h];
        let hl = lit_card == Some(c.card);
        sc.quads.push(flat_quad(head, 0.02, if hl { Color32::WHITE.gamma_multiply(0.85) } else { own.gamma_multiply(if c.home { 0.9 } else { 0.5 }) }));
        sc.heads.push(HeadPick { card: c.card, corners: [[head[0], 0.0, head[1]], [head[2], 0.0, head[1]], [head[2], 0.0, head[3]], [head[0], 0.0, head[3]]] });
        let cols = ((g.card_w - 0.4) / (HEAD_EM * ADV)).floor() as usize;
        sc.texts.push(flat_text(r[0] + 0.2, r[1] + g.head_h * 0.5 + HEAD_EM * 0.35, HEAD_EM, head_label(&c.path, &c.name, cols), Color32::from_rgb(0x10, 0x12, 0x18)));
        let rcols = ((g.card_w - 0.5) / (ROW_EM * ADV)).floor() as usize;
        let row_base = |k: usize| r[1] + g.head_h + (k as f32 + 0.5) * g.row_h + ROW_EM * 0.35;
        for (k, row) in c.rows.iter().enumerate() {
            sc.texts.push(flat_text(r[0] + 0.3, row_base(k), ROW_EM, fit_head(&row_text(row), rcols), row_color(&row.k)));
        }
        if c.more_rows > 0 {
            sc.texts.push(flat_text(r[0] + 0.3, row_base(c.rows.len()), ROW_EM, format!("… {} more", c.more_rows), Color32::GRAY));
        }
        for (k, s) in c.sites.iter().enumerate() {
            let lines: Vec<String> = s.lines.iter().map(u32::to_string).collect();
            let words = format!("→ {}  :{}", s.to_name, lines.join(","));
            sc.texts.push(flat_text(r[0] + 0.3, row_base(site_row_index(c, k)), ROW_EM, fit_head(&words, rcols), Color32::from_rgb(0xe0, 0xe0, 0xe0)));
            let m = marker_at(c, g, k);
            let tint = rel_color(&s.rel, own);
            // built on the plane like the rest of the card, lifted with it
            sc.quads.push(Quad { origin: [m[0] - 0.18, 0.04, m[2] - 0.18], u: [0.36, 0.0, 0.0], v: [0.0, 0.0, 0.36], color: tint });
            let head = match by_card.get(&s.to_card) {
                Some(t) => lift(head_top(t.rect), t.y),
                None => [s.to_at[0], 0.06 + s.to_y, s.to_at[1]],
            };
            let lit_one = lit_card.is_some_and(|l| l == c.card || l == s.to_card);
            sc.splines.push(Spline { from_card: c.card, to_card: s.to_card, marker: m, head, points: spline_points(m, head, 24), color: tint, lit: lit_one });
        }
        if c.more_sites > 0 {
            sc.texts.push(flat_text(r[0] + 0.3, row_base(site_row_index(c, c.sites.len())), ROW_EM, format!("… {} more calls", c.more_sites), Color32::GRAY));
        }
        lift_since(&mut sc, (q0, t0, h0), c.y);
    }
    // a callee outside the tiles held: a ghost head where it stands
    let mut ghosts: BTreeMap<u64, ([f32; 2], f32, String)> = BTreeMap::new();
    for c in cards {
        for s in &c.sites {
            if !by_card.contains_key(&s.to_card) {
                ghosts.entry(s.to_card).or_insert((s.to_at, s.to_y, s.to_name.clone()));
            }
        }
    }
    for (_, (at, gy, name)) in ghosts {
        let (q0, t0) = (sc.quads.len(), sc.texts.len());
        let r = [at[0] - g.card_w * 0.5, at[1], at[0] + g.card_w * 0.5, at[1] + g.head_h];
        sc.quads.push(flat_quad(r, 0.0, Color32::from_rgba_unmultiplied(0x60, 0x66, 0x76, 140)));
        sc.texts.push(flat_text(r[0] + 0.2, r[1] + g.head_h * 0.5 + HEAD_EM * 0.35, HEAD_EM, fit_tail(&name, ((g.card_w - 0.4) / (HEAD_EM * ADV)) as usize), Color32::from_rgb(0xdd, 0xdd, 0xdd)));
        let h0 = sc.heads.len();
        lift_since(&mut sc, (q0, t0, h0), gy);
    }
    let any_lit = lit_card.is_some();
    for s in &sc.splines {
        let (w, col) = match (any_lit, s.lit) {
            (true, true) => (3.0, s.color),
            (true, false) => (1.0, s.color.gamma_multiply(0.25)),
            _ => (1.6, s.color.gamma_multiply(0.85)),
        };
        for seg in s.points.windows(2) {
            sc.lines.push(Line { a: seg[0], b: seg[1], width_px: w, color: col });
        }
    }
    sc
}

// ------------------------------------------------------------------ expanded head

/// One caller or callee of an expanded head, where the server says its head is.
#[derive(Debug, Clone, PartialEq)]
pub struct Link {
    pub card: u64,
    pub name: String,
    pub rect: [f32; 4],
    pub rel: String,
    pub lines: Vec<u32>,
}

/// A head expanded (double-click): every caller and callee, not only the card's rows.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Expanded {
    pub card: u64,
    pub callers: Vec<Link>,
    pub callees: Vec<Link>,
    pub total_callers: u64,
    pub total_callees: u64,
}

/// The expansion of a measured `calls` answer.
pub fn expanded_of(a: &RoomAnswer) -> Option<Expanded> {
    if !a.is_measured() {
        return None;
    }
    let b = &a.body;
    let links = |k: &str| -> Vec<Link> {
        b[k].as_array()
            .into_iter()
            .flatten()
            .map(|x| Link {
                card: x["card"].as_u64().unwrap_or(u64::MAX),
                name: x["name"].as_str().unwrap_or("").to_string(),
                rect: rect4(&x["rect"]),
                rel: x["rel"].as_str().unwrap_or("").to_string(),
                lines: x["lines"].as_array().map(|l| l.iter().filter_map(|n| n.as_u64().map(|n| n as u32)).collect()).unwrap_or_default(),
            })
            .collect()
    };
    Some(Expanded {
        card: b["card"].as_u64()?,
        callers: links("callers"),
        callees: links("callees"),
        total_callers: b["total_callers"].as_u64().unwrap_or(0),
        total_callees: b["total_callees"].as_u64().unwrap_or(0),
    })
}

/// **An expanded head in the scene**: a lit spline from every caller's head (its right
/// edge) down onto this head, and from this card's foot to every callee's head; a ghost
/// head for each one not on a held tile.
pub fn expand_scene(sc: &mut Scene, world: &World, x: &Expanded, cards: &[&CardView]) {
    let g = world.geometry;
    let held: HashMap<u64, &CardView> = cards.iter().map(|c| (c.card, *c)).collect();
    let Some(me) = held.get(&x.card).map(|c| c.rect) else { return };
    let own = Color32::from_rgb(0xff, 0xd5, 0x4f);
    let ghost = |sc: &mut Scene, l: &Link| {
        if !held.contains_key(&l.card) {
            let r = [l.rect[0], l.rect[1], l.rect[2], l.rect[1] + g.head_h];
            sc.quads.push(flat_quad(r, 0.0, Color32::from_rgba_unmultiplied(0x60, 0x66, 0x76, 160)));
            sc.texts.push(flat_text(r[0] + 0.2, r[1] + g.head_h * 0.5 + HEAD_EM * 0.35, HEAD_EM, fit_tail(&l.name, ((g.card_w - 0.4) / (HEAD_EM * ADV)) as usize), Color32::from_rgb(0xdd, 0xdd, 0xdd)));
        }
    };
    for l in &x.callers {
        ghost(sc, l);
        let from = [l.rect[2], 0.06, l.rect[1] + g.head_h * 0.5];
        let to = head_top(me);
        let pts = spline_points(from, to, 24);
        let col = rel_color(&l.rel, own);
        for seg in pts.windows(2) {
            sc.lines.push(Line { a: seg[0], b: seg[1], width_px: 2.5, color: col });
        }
        sc.splines.push(Spline { from_card: l.card, to_card: x.card, marker: from, head: to, points: pts, color: col, lit: true });
    }
    for l in &x.callees {
        ghost(sc, l);
        let from = [me[2], 0.06, me[3] - g.row_h * 0.5];
        let to = head_top(l.rect);
        let pts = spline_points(from, to, 24);
        let col = rel_color(&l.rel, own);
        for seg in pts.windows(2) {
            sc.lines.push(Line { a: seg[0], b: seg[1], width_px: 2.5, color: col });
        }
        sc.splines.push(Spline { from_card: x.card, to_card: l.card, marker: from, head: to, points: pts, color: col, lit: true });
    }
}

// ------------------------------------------------------------------ source panel

/// One function's source as the server read it.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Source {
    pub moniker: String,
    pub start_line: u32,
    pub end_line: u32,
    pub sha: Option<String>,
    pub lines: Vec<String>,
    pub truncated: bool,
}

/// A head's unfolded source.
#[derive(Debug, Clone, PartialEq)]
pub struct Panel {
    pub card: u64,
    pub moniker: String,
    pub pinned: bool,
    /// The first line shown (index into the loaded lines).
    pub scroll: f32,
    pub source: Option<Source>,
    /// The refusal line when the source did not answer.
    pub state: Option<String>,
}

const KEYWORDS: &[&str] = &[
    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self",
    "Self", "static", "struct", "super", "trait", "type", "unsafe", "use", "where", "while",
];

const PLAIN: Color32 = Color32::from_rgb(0xe6, 0xe6, 0xe6);

/// **Syntax tint** of one line: (column, piece, colour) — comments, strings, keywords,
/// numbers; the pieces cover the line, in order.
pub fn tint_line(line: &str) -> Vec<(usize, String, Color32)> {
    const KW: Color32 = Color32::from_rgb(0xc7, 0x92, 0xea);
    const STR: Color32 = Color32::from_rgb(0xc3, 0xe8, 0x8d);
    const COM: Color32 = Color32::from_rgb(0x80, 0x8a, 0x99);
    const NUM: Color32 = Color32::from_rgb(0xf7, 0x8c, 0x6c);
    let chars: Vec<char> = line.chars().collect();
    let mut out: Vec<(usize, String, Color32)> = Vec::new();
    let mut plain = String::new();
    let mut plain_at = 0;
    let flush = |plain: &mut String, at: usize, out: &mut Vec<(usize, String, Color32)>| {
        if !plain.is_empty() {
            out.push((at, std::mem::take(plain), PLAIN));
        }
    };
    let mut i = 0;
    while i < chars.len() {
        let c = chars[i];
        if c == '/' && chars.get(i + 1) == Some(&'/') {
            flush(&mut plain, plain_at, &mut out);
            out.push((i, chars[i..].iter().collect(), COM));
            return out;
        }
        if c == '"' {
            flush(&mut plain, plain_at, &mut out);
            let s = i;
            i += 1;
            while i < chars.len() && chars[i] != '"' {
                i += if chars[i] == '\\' { 2 } else { 1 };
            }
            i = (i + 1).min(chars.len());
            out.push((s, chars[s..i].iter().collect(), STR));
            continue;
        }
        if c.is_alphanumeric() || c == '_' {
            let s = i;
            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
                i += 1;
            }
            let w: String = chars[s..i].iter().collect();
            let col = if KEYWORDS.contains(&w.as_str()) {
                Some(KW)
            } else if c.is_ascii_digit() {
                Some(NUM)
            } else {
                None
            };
            match col {
                Some(col) => {
                    flush(&mut plain, plain_at, &mut out);
                    out.push((s, w, col));
                }
                None => {
                    if plain.is_empty() {
                        plain_at = s;
                    }
                    plain.push_str(&w);
                }
            }
            continue;
        }
        if plain.is_empty() {
            plain_at = i;
        }
        plain.push(c);
        i += 1;
    }
    flush(&mut plain, plain_at, &mut out);
    out
}

/// Where a panel stands: anchored at its head's top-right corner, facing the camera.
/// Returns (top-left corner, right unit, down unit, line height).
pub fn panel_frame(card: [f32; 4], cam: &Camera) -> ([f32; 3], [f32; 3], [f32; 3], f32) {
    let (_, right, up) = cam.basis();
    let lh = PANEL_EM * 1.25;
    let top_left = add([card[2] + 0.8, 0.6, card[1]], mul(up, lh * (PANEL_LINES as f32 + 1.5) * 0.5));
    (top_left, right, mul(up, -1.0), lh)
}

/// The panel's quad corners (for hover and for the scene).
pub fn panel_corners(card: [f32; 4], cam: &Camera) -> [[f32; 3]; 4] {
    let (tl, right, down, lh) = panel_frame(card, cam);
    let w = PANEL_COLS as f32 * PANEL_EM * ADV + 0.8;
    let h = lh * (PANEL_LINES as f32 + 1.5);
    [tl, add(tl, mul(right, w)), add(add(tl, mul(right, w)), mul(down, h)), add(tl, mul(down, h))]
}

/// **The unfolded source** as scene parts: a half-transparent sheet, a header, the lines
/// from `scroll`, syntax-tinted, the call lines marked and tied to their markers.
pub fn panel_scene(sc: &mut Scene, p: &Panel, card: &CardView, cam: &Camera, g: Geometry) {
    let (tl, right, down, lh) = panel_frame(card.rect, cam);
    let c = panel_corners(card.rect, cam);
    // a SHEET, not a quad: it writes no depth, so the splines behind it stay visible
    // through its half alpha
    sc.sheets.push(Quad { origin: c[0], u: sub(c[1], c[0]), v: sub(c[3], c[0]), color: Color32::from_rgba_unmultiplied(0x0c, 0x0f, 0x16, 128) });
    // the panel's words lie a hair in front of its sheet (towards the eye)
    let toward = mul(cam.basis().0, -0.03);
    let at = |col: f32, row: f32| add(add(add(tl, mul(right, 0.4 + col * PANEL_EM * ADV)), mul(down, lh * (row + 1.0))), toward);
    let text_at = |col: f32, row: f32, text: String, color: Color32| Text { origin: at(col, row), right: mul(right, PANEL_EM), up: mul(down, -PANEL_EM), text, color };
    let Some(src) = &p.source else {
        sc.sheet_texts.push(text_at(0.0, 0.0, p.state.clone().unwrap_or_else(|| "reading the source …".into()), Color32::GRAY));
        return;
    };
    let head = format!("{}  {}:{}{}", fit_tail(&card.path, 48), src.start_line, src.end_line, if p.pinned { "  (pinned)" } else { "" });
    sc.sheet_texts.push(text_at(0.0, 0.0, head, Color32::from_rgb(0x90, 0xca, 0xf9)));
    let calls: HashMap<u32, (usize, &SiteRow)> = card.sites.iter().enumerate().flat_map(|(k, s)| s.lines.iter().map(move |l| (*l, (k, s)))).collect();
    let first = p.scroll.max(0.0) as usize;
    for (row, (i, line)) in src.lines.iter().enumerate().skip(first).take(PANEL_LINES).enumerate() {
        let n = src.start_line + i as u32;
        let r = row as f32 + 1.2;
        sc.sheet_texts.push(text_at(0.0, r, format!("{n:>5}"), Color32::from_rgb(0x60, 0x68, 0x78)));
        for (col, piece, color) in tint_line(&fit_head(line, PANEL_COLS - 7)) {
            sc.sheet_texts.push(text_at(6.0 + col as f32, r, piece, color));
        }
        if let Some((k, s)) = calls.get(&n) {
            // the call line: a bar in its spline's tint, tied to its marker on the card
            let tint = rel_color(&s.rel, Color32::from_rgb(0xff, 0xd5, 0x4f));
            let bar = add(at(5.6, r), mul(down, -PANEL_EM * 0.35));
            sc.quads.push(Quad { origin: add(bar, mul(down, -PANEL_EM * 0.5)), u: mul(right, PANEL_EM * 0.3), v: mul(down, PANEL_EM * 1.1), color: tint });
            sc.lines.push(Line { a: bar, b: marker_at(card, g, *k), width_px: 1.2, color: tint.gamma_multiply(0.8) });
        }
    }
}

// ------------------------------------------------------------------ the diff

/// One consumed commit of the timeline (lod `commits`).
#[derive(Debug, Clone, PartialEq)]
pub struct CommitRow {
    pub seq: i64,
    pub sha: String,
    pub subject: String,
    pub status: String,
}

/// One head of the diff (lod `diff`): what changed and where it is.
#[derive(Debug, Clone, PartialEq)]
pub struct DiffHead {
    /// `added` | `removed` | `changed`.
    pub class: String,
    pub moniker: String,
    pub name: String,
    pub crate_key: String,
    /// The current map's card, when the head is on it.
    pub card: Option<u64>,
    /// Where to draw it (the card's head, or the server's ghost spot).
    pub at: Option<[f32; 2]>,
    pub ghost: bool,
    pub file: Option<String>,
    pub line: Option<u32>,
    pub end_line: Option<u32>,
    /// Where it stood at `from` (removed and changed heads): file, lines.
    pub before: Option<(String, u32, u32)>,
    /// The MEANING JUMP: 1 − cosine(old body vector, new body vector); 0 =
    /// the body (or its meaning) stayed; `None` = a side has no vector yet.
    pub jump: Option<f32>,
    /// Which side misses a vector (`old` / `new` / `both`), said plainly.
    pub jump_missing: Option<String>,
}

/// One CALLS spline of the diff, endpoints placed by the server.
#[derive(Debug, Clone, PartialEq)]
pub struct DiffSpline {
    pub class: String,
    pub from_at: Option<[f32; 2]>,
    pub to_at: Option<[f32; 2]>,
}

/// The landed diff of one (repo, from, to): the UI only places what it says.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct DiffView {
    pub repo: String,
    pub from: i64,
    pub to: i64,
    pub from_sha: Option<String>,
    pub to_sha: Option<String>,
    pub heads: Vec<DiffHead>,
    pub splines: Vec<DiffSpline>,
    /// (current plate, its key, added, removed, changed, summed jump).
    pub plates: Vec<(Option<usize>, String, u64, u64, u64, f32)>,
    /// How much of the changed set the jump measured, in the server's words.
    pub jump_note: Option<String>,
}

pub fn commits_of(a: &RoomAnswer) -> Result<Vec<CommitRow>, String> {
    if !a.is_measured() {
        return Err(format!("[{}] {}", a.state, a.evidence_text()));
    }
    Ok(a.body["commits"]
        .as_array()
        .into_iter()
        .flatten()
        .filter_map(|c| {
            Some(CommitRow {
                seq: c["seq"].as_i64()?,
                sha: c["sha"].as_str()?.to_string(),
                subject: c["subject"].as_str().unwrap_or("").to_string(),
                status: c["status"].as_str().unwrap_or("").to_string(),
            })
        })
        .collect())
}

pub fn diff_of(a: &RoomAnswer) -> Result<DiffView, String> {
    if !a.is_measured() {
        return Err(format!("[{}] {}", a.state, a.evidence_text()));
    }
    let b = &a.body;
    let at2 = |v: &Value| -> Option<[f32; 2]> {
        let a = v.as_array()?;
        Some([f(&a[0]), f(&a[1])])
    };
    Ok(DiffView {
        repo: b["repo"].as_str().unwrap_or("").to_string(),
        from: b["from"].as_i64().unwrap_or(0),
        to: b["to"].as_i64().unwrap_or(0),
        from_sha: b["from_sha"].as_str().map(str::to_string),
        to_sha: b["to_sha"].as_str().map(str::to_string),
        heads: b["nodes"]
            .as_array()
            .into_iter()
            .flatten()
            .map(|n| DiffHead {
                class: n["class"].as_str().unwrap_or("").to_string(),
                moniker: n["moniker"].as_str().unwrap_or("").to_string(),
                name: n["name"].as_str().unwrap_or("").to_string(),
                crate_key: n["crate"].as_str().unwrap_or("").to_string(),
                card: n["card"].as_u64(),
                at: at2(&n["at"]),
                ghost: n["ghost"].as_bool().unwrap_or(false),
                file: n["file"].as_str().map(str::to_string),
                line: n["line"].as_u64().map(|x| x as u32),
                end_line: n["end_line"].as_u64().map(|x| x as u32),
                before: n["before"]["file"].as_str().map(|f| {
                    let l = n["before"]["line"].as_u64().unwrap_or(1) as u32;
                    (f.to_string(), l, n["before"]["end_line"].as_u64().map_or(l, |x| x as u32))
                }),
                jump: n["jump"].as_f64().map(|x| x as f32),
                jump_missing: n["jump_missing"].as_str().map(str::to_string),
            })
            .collect(),
        splines: b["edges"]
            .as_array()
            .into_iter()
            .flatten()
            .map(|e| DiffSpline { class: e["class"].as_str().unwrap_or("").to_string(), from_at: at2(&e["from_at"]), to_at: at2(&e["to_at"]) })
            .collect(),
        plates: b["plates"]
            .as_array()
            .into_iter()
            .flatten()
            .map(|p| {
                (
                    p["crate"].as_u64().map(|x| x as usize),
                    p["key"].as_str().unwrap_or("").to_string(),
                    p["added"].as_u64().unwrap_or(0),
                    p["removed"].as_u64().unwrap_or(0),
                    p["changed"].as_u64().unwrap_or(0),
                    p["jump"].as_f64().unwrap_or(0.0) as f32,
                )
            })
            .collect(),
        jump_note: b["jump_coverage"]["note"].as_str().map(str::to_string),
    })
}

/// A diff class's tint: added green, removed red (its splines ghosted),
/// changed amber.
pub fn class_color(class: &str) -> Color32 {
    match class {
        "added" => Color32::from_rgb(0x66, 0xbb, 0x6a),
        "removed" => Color32::from_rgb(0xef, 0x53, 0x50),
        _ => Color32::from_rgb(0xff, 0xca, 0x28),
    }
}

/// **The diff on the scene** (pure, tested): tint bands over the changed
/// heads, ghost heads where a removed one stood (the server's spot), the
/// diff's CALLS splines (removed ones ghosted), and far out a `+a −r ~c`
/// badge on every touched plate.
pub fn diff_scene(sc: &mut Scene, world: &World, d: &DiffView, near: bool) {
    let g = world.geometry;
    for h in &d.heads {
        let Some(at) = h.at else { continue };
        let col = class_color(&h.class);
        let band = [at[0] - g.card_w * 0.5, at[1], at[0] + g.card_w * 0.5, at[1] + g.head_h];
        // the MEANING JUMP sets the glow: a reformat stays cool, a real
        // behaviour change burns — an unmeasured one keeps the plain tint
        let glow = h.jump.map(|j| j.clamp(0.0, 1.0));
        if h.ghost {
            sc.quads.push(flat_quad(band, 0.05, col.gamma_multiply(0.30)));
            let cols = ((g.card_w - 0.4) / (HEAD_EM * ADV)).floor() as usize;
            sc.texts.push(flat_text(band[0] + 0.2, band[1] + g.head_h * 0.5 + HEAD_EM * 0.35, HEAD_EM, fit_head(&h.name, cols), col.gamma_multiply(0.8)));
        } else {
            let a = match glow {
                Some(j) => 0.25 + 0.6 * j,
                None => 0.55,
            };
            sc.quads.push(flat_quad(band, 0.05, col.gamma_multiply(a)));
        }
        // a pin above the head: the class from afar, its height the jump
        let pin = g.head_h * (3.0 + glow.unwrap_or(0.0) * 9.0);
        sc.lines.push(Line { a: [at[0], 0.0, at[1]], b: [at[0], pin, at[1]], width_px: 2.0 + glow.unwrap_or(0.0) * 2.0, color: col });
        if let Some(j) = glow.filter(|j| *j >= 0.05) {
            sc.texts.push(flat_text(band[0] + 0.2, band[1] - 0.3, HEAD_EM * 0.8, format!("Δ{j:.2}"), col));
        }
    }
    for s in &d.splines {
        let (Some(a), Some(b)) = (s.from_at, s.to_at) else { continue };
        let (col, w) = match s.class.as_str() {
            "added" => (class_color("added"), 2.0),
            "removed" => (Color32::from_rgba_unmultiplied(0xef, 0x53, 0x50, 70), 1.4),
            _ => (class_color("changed"), 2.0),
        };
        let pts = arc_points([a[0], 0.08, a[1]], [b[0], 0.08, b[1]], 16, 0.25);
        for seg in pts.windows(2) {
            sc.lines.push(Line { a: seg[0], b: seg[1], width_px: w, color: col });
        }
    }
    if !near {
        for (plate, _, a, r, c, j) in &d.plates {
            let Some(p) = plate.and_then(|i| world.crates.get(i)) else { continue };
            let label = if *j >= 0.005 { format!("+{a} −{r} ~{c} Δ{j:.2}") } else { format!("+{a} −{r} ~{c}") };
            let em = (p.title * 0.5).min(3.0);
            let x = p.rect[2] - (label.chars().count() as f32 * ADV * em) - 1.0;
            sc.texts.push(flat_text(x, p.rect[1] + (p.title + em * 0.7) * 0.5 + 0.5, em, label, Color32::from_rgb(0xff, 0xca, 0x28)));
        }
    }
}

// ------------------------------------------------------------------ the tab

/// A read in flight, and what it was for.
struct Ask {
    key: String,
    pending: Pending<RoomAnswer>,
}

/// The last frame's facts, for `state_json` (and a robot to point at).
#[derive(Debug, Clone, Default)]
struct LastFrame {
    rect: Option<Rect>,
    near: bool,
    cards: usize,
    splines: usize,
    labels: usize,
    gpu: bool,
    heads_screen: Vec<(u64, Pos2)>,
    /// The diff's tinted heads on screen: (class, moniker, where).
    diff_heads_screen: Vec<(String, String, Pos2)>,
    plates_screen: Vec<(usize, Pos2)>,
    panels_screen: Vec<(u64, Pos2)>,
    wheel_events: u64,
    wanted_tiles: Vec<(i32, i32)>,
    wanted_plates: Vec<usize>,
}

/// The diff picker and its landed view.
#[derive(Default)]
struct DiffUi {
    /// The timeline strip is shown (the 🕑 diff toggle).
    open: bool,
    /// The repo whose commits the timeline lists.
    repo: String,
    commits: Vec<CommitRow>,
    commits_err: Option<String>,
    from: Option<i64>,
    to: Option<i64>,
    view: Option<DiffView>,
    err: Option<String>,
    /// The before/after window of one head: (head, before lines or the ask's
    /// state, after lines likewise).
    hover: Option<BeforeAfter>,
}

/// The before/after of one changed or removed head.
struct BeforeAfter {
    head: DiffHead,
    before: Result<Vec<String>, String>,
    after: Result<Vec<String>, String>,
}

/// **The 🏛 Code room tab.**
pub struct CodeRoom3DTab {
    source: Arc<dyn RoomSource>,
    forge: Option<(String, String)>,
    workspace: String,
    world_answer: RoomAnswer,
    world: Option<World>,
    /// Card tiles held (of the world version they were asked for).
    tiles: HashMap<(i32, i32), Result<Vec<CardView>, String>>,
    /// Module blocks held, by plate.
    modules: HashMap<usize, Result<Vec<ModuleView>, String>>,
    asks: Vec<Ask>,
    /// A queued answer is asked again at this UI time.
    retry: HashMap<String, (f64, Value)>,
    /// When a request was first answered `queued` (UI clock), by key: it is given up
    /// past [`GIVE_UP_S`], said, and not asked again.
    waiting: HashMap<String, f64>,
    cam: Camera,
    yaw_pitch_set: bool,
    flight: Option<Flight>,
    nav: NavMode,
    nav_set: bool,
    selected: Option<u64>,
    hovered: Option<u64>,
    hover_since: Option<(u64, f64)>,
    /// The unpinned (at most one) and the pinned source panels.
    panels: Vec<Panel>,
    search: String,
    hits: Vec<Value>,
    expanded: Option<Expanded>,
    diff: DiffUi,
    /// The map by role (the layout toggle): cards placed by what they do, crates a tint.
    by_role: bool,
    /// The 3D replay of a traced run (the 🎬 toggle; [`crate::trace_replay`]).
    trace: crate::trace_replay::TraceUi,
    last: LastFrame,
    now: f64,
}

impl CodeRoom3DTab {
    pub fn new(endpoint: impl Into<String>, token: impl Into<String>, workspace: impl Into<String>) -> Self {
        let (endpoint, token, workspace) = (endpoint.into(), token.into(), workspace.into());
        let source = ForgeRoom::shared(&endpoint, &token, &workspace);
        let mut t = Self::with_source(source, &workspace);
        t.forge = Some((endpoint, token));
        t
    }

    /// A tab over any room source (a test's canned room).
    pub fn with_source(source: Arc<dyn RoomSource>, workspace: &str) -> Self {
        let world_answer = RoomAnswer::pending(S_CODE_MAP, &source.describe());
        let mut t = Self {
            source,
            forge: None,
            workspace: workspace.to_string(),
            world_answer,
            world: None,
            tiles: HashMap::new(),
            modules: HashMap::new(),
            asks: Vec::new(),
            retry: HashMap::new(),
            waiting: HashMap::new(),
            cam: Camera { target: [0.0; 3], dist: 1000.0, yaw: 0.0, pitch: 1.0 },
            yaw_pitch_set: false,
            flight: None,
            nav: NavMode::Stay,
            nav_set: false,
            selected: None,
            hovered: None,
            hover_since: None,
            panels: Vec::new(),
            search: String::new(),
            hits: Vec::new(),
            expanded: None,
            diff: DiffUi::default(),
            by_role: false,
            trace: Default::default(),
            last: LastFrame::default(),
            now: 0.0,
        };
        t.ask(serde_json::json!({ "lod": "world" }));
        t
    }

    fn ask(&mut self, mut params: Value) {
        // by role every question about places carries the layout (a source page does not)
        if self.by_role && !matches!(params["lod"].as_str(), Some("source" | "source_at" | "commits")) {
            params["layout"] = serde_json::json!("role");
        }
        let key = params.to_string();
        if self.asks.iter().any(|a| a.key == key) {
            return;
        }
        let source = self.source.clone();
        self.asks.push(Ask { key, pending: Pending::spawn("code-map", move || source.ask(S_CODE_MAP, &params)) });
    }

    fn land(&mut self, key: &str, a: RoomAnswer) {
        let params: Value = serde_json::from_str(key).unwrap_or(Value::Null);
        if a.kind == RoomState::Queued {
            // queued (or running) on jera: said, asked again — never a bare spinner,
            // and given up (said so) when it does not come
            let since = *self.waiting.entry(key.to_string()).or_insert(self.now);
            let waited = self.now - since;
            let job = a.job_id().unwrap_or("?").to_string();
            let give_up = waited >= GIVE_UP_S;
            if params["lod"] == "source" {
                let m = params["moniker"].as_str().unwrap_or("");
                for p in self.panels.iter_mut().filter(|p| p.moniker == m && p.source.is_none()) {
                    p.state = Some(if give_up {
                        format!("no answer after {waited:.0} s — job {job} is still {} on jera; click the head to ask again", a.state)
                    } else {
                        format!("{} on jera — job {job} · {waited:.0} s", a.state)
                    });
                }
            }
            if params["lod"] == "world" {
                self.world_answer = a;
            }
            if give_up {
                self.waiting.remove(key);
            } else {
                self.retry.insert(key.to_string(), (self.now + RETRY_S, params));
            }
            return;
        }
        self.waiting.remove(key);
        match params["lod"].as_str().unwrap_or("") {
            "world" => {
                if let Ok(w) = world_of(&a) {
                    if self.world.as_ref().is_none_or(|old| old.version != w.version) {
                        self.tiles.clear();
                        self.modules.clear();
                        self.expanded = None;
                    }
                    if self.world.is_none() {
                        let mut c = Camera::fit(w.bounds);
                        if self.yaw_pitch_set {
                            (c.yaw, c.pitch) = (self.cam.yaw, self.cam.pitch);
                        }
                        self.cam = c;
                    }
                    self.world = Some(w);
                }
                self.world_answer = a;
            }
            "cards" => {
                let t = &params["tile"];
                if let (Some(x), Some(z)) = (t[0].as_i64(), t[1].as_i64()) {
                    self.tiles.insert((x as i32, z as i32), cards_of(&a));
                }
            }
            "modules" => {
                if let Some(k) = params["crate"].as_u64() {
                    self.modules.insert(k as usize, modules_of(&a));
                }
            }
            "find" => {
                self.hits = if a.is_measured() { a.body["hits"].as_array().cloned().unwrap_or_default() } else { Vec::new() };
            }
            "source" => self.land_source(&params, &a),
            "calls" => self.expanded = expanded_of(&a),
            "commits" => match commits_of(&a) {
                Ok(c) => {
                    self.diff.commits = c;
                    self.diff.commits_err = None;
                }
                Err(e) => self.diff.commits_err = Some(e),
            },
            "diff" => match diff_of(&a) {
                Ok(d) => {
                    if self.diff.repo == d.repo && self.diff.from == Some(d.from) && (self.diff.to.is_none() || self.diff.to == Some(d.to)) {
                        self.diff.to = Some(d.to);
                        self.diff.view = Some(d);
                        self.diff.err = None;
                    }
                }
                Err(e) => self.diff.err = Some(e),
            },
            "source_at" => self.land_source_at(&params, &a),
            "trace_runs" => match crate::trace_replay::runs_of(&a) {
                Ok(r) => {
                    self.trace.runs = r;
                    self.trace.runs_err = None;
                }
                Err(e) => self.trace.runs_err = Some(e),
            },
            "trace" => match crate::trace_replay::trace_of(&a) {
                Ok(v) => {
                    if self.trace.picked.as_ref().is_none_or(|r| r.run_id != v.run_id) {
                        return; // a run no longer picked
                    }
                    if params.get("after_seq").is_some() {
                        // the next page of the held window, its heads re-pointed
                        if let Some(held) = self.trace.view.as_mut().filter(|h| h.from_ns == v.from_ns) {
                            crate::trace_replay::append_page(held, v);
                        }
                    } else if v.from_ns == self.trace.t_ns as u64 {
                        self.trace.view = Some(v);
                        crate::trace_replay::land_stacks(&mut self.trace);
                    }
                    self.trace.err = None;
                }
                Err(e) => self.trace.err = Some(e),
            },
            _ => {}
        }
    }

    /// Open the replay (and fetch the repo's traced runs once).
    fn trace_open(&mut self, repo: &str) {
        self.trace.open = true;
        if self.trace.repo != repo {
            self.trace = crate::trace_replay::TraceUi { open: true, repo: repo.to_string(), ..Default::default() };
        }
        if self.trace.runs.is_empty() {
            self.ask(serde_json::json!({ "lod": "trace_runs", "repo": repo }));
        }
    }

    /// Pick a run: the replay starts at its head, paused.
    fn trace_pick(&mut self, run_id: &str) {
        let Some(r) = self.trace.runs.iter().find(|r| r.run_id == run_id).cloned() else { return };
        let speed = if self.trace.speed > 0.0 { self.trace.speed } else { (r.duration_ns.max(1) as f32) / 10.0 };
        self.trace.picked = Some(r);
        self.trace.speed = speed;
        crate::trace_replay::scrub(&mut self.trace, 0);
        self.ask_trace_window(0, false);
    }

    /// Scrub the replay to a time: a fresh window from there, its stacks seeded.
    fn trace_scrub(&mut self, to_ns: u64) {
        if self.trace.picked.is_none() {
            return;
        }
        crate::trace_replay::scrub(&mut self.trace, to_ns);
        self.ask_trace_window(to_ns, to_ns > 0);
    }

    fn ask_trace_window(&mut self, from_ns: u64, stacks: bool) {
        let Some(r) = &self.trace.picked else { return };
        let mut p = serde_json::json!({ "lod": "trace", "run_id": r.run_id, "from_ns": from_ns, "limit": crate::trace_replay::PAGE });
        if stacks {
            p["stacks"] = serde_json::json!(true);
        }
        self.ask(p);
    }

    fn land_source(&mut self, params: &Value, a: &RoomAnswer) {
        let m = params["moniker"].as_str().unwrap_or("").to_string();
        for p in self.panels.iter_mut().filter(|p| p.moniker == m) {
            let b = &a.body;
            if !(a.is_measured() && b["lines"].is_array()) {
                p.state = Some(format!("[{}] {}", a.state, a.evidence_text()));
                continue;
            }
            let lines: Vec<String> = b["lines"].as_array().into_iter().flatten().map(|l| l.as_str().unwrap_or("").to_string()).collect();
            let start = b["start_line"].as_u64().unwrap_or(0) as u32;
            let from = b["from"].as_u64().map_or(start, |x| x as u32);
            let src = p.source.get_or_insert_with(|| Source {
                moniker: m.clone(),
                start_line: start,
                end_line: b["end_line"].as_u64().unwrap_or(0) as u32,
                sha: b["sha"].as_str().map(str::to_string),
                lines: Vec::new(),
                truncated: false,
            });
            // a page lands at its own offset
            let at = from.saturating_sub(src.start_line) as usize;
            if at <= src.lines.len() {
                src.lines.truncate(at);
                src.lines.extend(lines);
            }
            src.truncated = b["truncated"].as_bool().unwrap_or(false);
        }
    }

    /// One side of the before/after window lands.
    fn land_source_at(&mut self, params: &Value, a: &RoomAnswer) {
        let Some(h) = &mut self.diff.hover else { return };
        let lines = || -> Result<Vec<String>, String> {
            if !a.is_measured() {
                return Err(format!("[{}] {}", a.state, a.evidence_text()));
            }
            Ok(a.body["lines"].as_array().into_iter().flatten().map(|l| l.as_str().unwrap_or("").to_string()).collect())
        };
        let (sha, view_from, view_to) = (params["sha"].as_str().unwrap_or(""), self.diff.view.as_ref().and_then(|v| v.from_sha.clone()), self.diff.view.as_ref().and_then(|v| v.to_sha.clone()));
        if view_from.as_deref() == Some(sha) {
            h.before = lines();
        }
        if view_to.as_deref() == Some(sha) {
            h.after = lines();
        }
    }

    /// Open the diff picker (and fetch the repo's timeline once).
    fn diff_open(&mut self, repo: &str) {
        self.diff.open = true;
        if self.diff.repo != repo {
            self.diff = DiffUi { open: true, repo: repo.to_string(), ..DiffUi::default() };
        }
        if self.diff.commits.is_empty() {
            self.ask(serde_json::json!({ "lod": "commits", "repo": repo }));
        }
    }

    /// Pick a window: `to = None` means the tip.
    fn diff_pick(&mut self, from: i64, to: Option<i64>) {
        self.diff.from = Some(from);
        self.diff.to = to;
        self.diff.view = None;
        self.diff.err = None;
        self.diff.hover = None;
        let mut p = serde_json::json!({ "lod": "diff", "repo": self.diff.repo, "from": from });
        if let Some(t) = to {
            p["to"] = serde_json::json!(t);
        }
        self.ask(p);
    }

    /// Open the before/after window of one tinted head.
    fn diff_hover(&mut self, moniker: &str) {
        let Some(v) = &self.diff.view else { return };
        let Some(h) = v.heads.iter().find(|h| h.moniker == moniker).cloned() else { return };
        let pend = "…".to_string();
        let mut ba = BeforeAfter { head: h, before: Err(pend.clone()), after: Err(pend) };
        let page = |sha: &Option<String>, file: &Option<String>, line: Option<u32>, end: Option<u32>| -> Option<Value> {
            let (sha, file) = (sha.clone()?, file.clone()?);
            let from = line.unwrap_or(1);
            let max = end.map_or(80, |e| (e + 1).saturating_sub(from).clamp(1, 200));
            Some(serde_json::json!({ "lod": "source_at", "repo": self.diff.repo, "sha": sha, "file": file, "from": from, "max_lines": max }))
        };
        let before = ba.head.before.clone();
        let before_ask = match &before {
            Some((f, l, e)) => page(&v.from_sha, &Some(f.clone()), Some(*l), Some(*e)),
            None => None,
        };
        let after_ask = if ba.head.class == "removed" { None } else { page(&v.to_sha, &ba.head.file, ba.head.line, ba.head.end_line) };
        if before_ask.is_none() {
            ba.before = Err("nothing stood here at the from-commit".into());
        }
        if after_ask.is_none() {
            ba.after = Err(if ba.head.class == "removed" { "removed: nothing stands here at the to-commit".into() } else { "no place known".into() });
        }
        self.diff.hover = Some(ba);
        for ask in [before_ask, after_ask].into_iter().flatten() {
            self.ask(ask);
        }
    }

    fn poll(&mut self) {
        let mut done = Vec::new();
        self.asks.retain(|a| match a.pending.try_take() {
            Some(r) => {
                done.push((a.key.clone(), r));
                false
            }
            None => true,
        });
        for (k, r) in done {
            let a = r.unwrap_or_else(|()| room::worker_died(S_CODE_MAP, self.source.as_ref()));
            self.land(&k, a);
        }
        let due: Vec<String> = self.retry.iter().filter(|(_, (t, _))| *t <= self.now).map(|(k, _)| k.clone()).collect();
        for k in due {
            let Some((_, p)) = self.retry.remove(&k) else { continue };
            // only what is still wanted: a tile the camera left, a plate out of view, a
            // source whose sheet was closed is not asked again (it is asked anew when wanted)
            let still = match p["lod"].as_str().unwrap_or("") {
                "cards" => self.last.wanted_tiles.iter().any(|t| p["tile"] == serde_json::json!([t.0, t.1])),
                "modules" => self.last.wanted_plates.iter().any(|c| p["crate"] == serde_json::json!(c)),
                "source" => self.panels.iter().any(|x| p["moniker"] == x.moniker.as_str()),
                _ => true,
            };
            if still {
                self.ask(p);
            } else {
                self.waiting.remove(&k);
            }
        }
    }

    /// Block on every read in flight (a robot / CLI drive).
    fn wait(&mut self) {
        for _ in 0..8 {
            let asks = std::mem::take(&mut self.asks);
            if asks.is_empty() {
                break;
            }
            for a in asks {
                let r = a.pending.wait().unwrap_or_else(|()| room::worker_died(S_CODE_MAP, self.source.as_ref()));
                self.land(&a.key, r);
            }
        }
    }

    fn near(&self) -> bool {
        self.cam.dist <= CARDS_DIST
    }

    /// The tiles the view covers, nearest the target first, capped.
    fn tiles_in_view(&self, rect: Rect) -> Vec<(i32, i32)> {
        let Some(w) = &self.world else { return Vec::new() };
        let t = w.tile.max(1.0);
        let mut pts: Vec<[f32; 3]> = [rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom(), rect.center()].iter().filter_map(|p| self.cam.ground(*p, rect)).collect();
        if pts.len() < 5 {
            // the horizon is in view: as far as a few distances out
            let r = self.cam.dist * 2.0;
            pts.push(add(self.cam.target, [-r, 0.0, -r]));
            pts.push(add(self.cam.target, [r, 0.0, r]));
        }
        let (x0, x1) = pts.iter().fold((f32::MAX, f32::MIN), |m, p| (m.0.min(p[0]), m.1.max(p[0])));
        let (z0, z1) = pts.iter().fold((f32::MAX, f32::MIN), |m, p| (m.0.min(p[2]), m.1.max(p[2])));
        let (tx, tz) = ((self.cam.target[0] / t).floor() as i32, (self.cam.target[2] / t).floor() as i32);
        let mut want: Vec<(i32, i32)> = w.tiles.iter().copied().filter(|(ix, iz)| (*ix as f32 + 1.0) * t >= x0 && (*ix as f32) * t <= x1 && (*iz as f32 + 1.0) * t >= z0 && (*iz as f32) * t <= z1).collect();
        want.sort_by_key(|(x, z)| ((x - tx).abs().max((z - tz).abs()), *x, *z));
        want.truncate(TILES_MAX);
        want
    }

    /// The plates the view covers, nearest the target first, capped.
    fn plates_in_view(&self, rect: Rect) -> Vec<usize> {
        let Some(w) = &self.world else { return Vec::new() };
        let pts: Vec<[f32; 3]> = [rect.left_top(), rect.right_top(), rect.right_bottom(), rect.left_bottom(), rect.center()].iter().filter_map(|p| self.cam.ground(*p, rect)).collect();
        let r = self.cam.dist * 2.0;
        let (x0, x1, z0, z1) = if pts.len() == 5 {
            (pts.iter().map(|p| p[0]).fold(f32::MAX, f32::min), pts.iter().map(|p| p[0]).fold(f32::MIN, f32::max), pts.iter().map(|p| p[2]).fold(f32::MAX, f32::min), pts.iter().map(|p| p[2]).fold(f32::MIN, f32::max))
        } else {
            (self.cam.target[0] - r, self.cam.target[0] + r, self.cam.target[2] - r, self.cam.target[2] + r)
        };
        let t = self.cam.target;
        let mut want: Vec<(f32, usize)> = w
            .crates
            .iter()
            .enumerate()
            .filter(|(_, p)| p.cards > 0 && p.rect[2] >= x0 && p.rect[0] <= x1 && p.rect[3] >= z0 && p.rect[1] <= z1)
            .map(|(i, p)| (((p.rect[0] + p.rect[2]) * 0.5 - t[0]).hypot((p.rect[1] + p.rect[3]) * 0.5 - t[2]), i))
            .collect();
        want.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
        want.into_iter().take(PLATES_MAX).map(|x| x.1).collect()
    }

    fn held_modules(&self, plates: &[usize]) -> Vec<&ModuleView> {
        plates.iter().filter_map(|p| self.modules.get(p)).filter_map(|r| r.as_ref().ok()).flatten().collect()
    }

    fn held_cards(&self, tiles: &[(i32, i32)]) -> Vec<&CardView> {
        tiles.iter().filter_map(|t| self.tiles.get(t)).filter_map(|r| r.as_ref().ok()).flatten().collect()
    }

    fn card(&self, id: u64) -> Option<&CardView> {
        self.tiles.values().filter_map(|r| r.as_ref().ok()).flatten().find(|c| c.card == id)
    }

    fn fly_to(&mut self, target: [f32; 3], dist: f32) {
        let to = ([f64::from(target[0]), f64::from(target[1]), f64::from(target[2])], f64::from(dist));
        let from = ([f64::from(self.cam.target[0]), f64::from(self.cam.target[1]), f64::from(self.cam.target[2])], f64::from(self.cam.dist));
        self.flight = Some(match &self.flight {
            Some(f) if !f.done(self.now) => f.retarget(self.now, to.0, to.1),
            _ => Flight::new(self.now, from.0, from.1, to.0, to.1),
        });
    }

    fn fly_to_rect(&mut self, r: [f32; 4]) {
        let size = (r[2] - r[0]).max(r[3] - r[1]);
        self.fly_to([(r[0] + r[2]) * 0.5, 0.0, (r[1] + r[3]) * 0.5], (size * 1.4 + 6.0).max(12.0));
    }

    fn open_panel(&mut self, card: u64, pinned: bool) {
        let Some(moniker) = self.card(card).map(|c| c.id.clone()) else { return };
        if let Some(p) = self.panels.iter_mut().find(|p| p.card == card) {
            p.pinned |= pinned;
            // a sheet with no source (given up, refused) asks again on a click
            if p.source.is_none() {
                p.state = None;
                self.ask(serde_json::json!({ "lod": "source", "moniker": moniker }));
            }
            return;
        }
        // one unpinned panel at a time
        self.panels.retain(|p| p.pinned);
        self.panels.push(Panel { card, moniker: moniker.clone(), pinned, scroll: 0.0, source: None, state: None });
        self.ask(serde_json::json!({ "lod": "source", "moniker": moniker }));
    }

    /// Scroll a panel by `lines`; ask the next page when it runs past what is held.
    fn scroll_panel(&mut self, i: usize, lines: f32) {
        let Some(p) = self.panels.get_mut(i) else { return };
        let held = p.source.as_ref().map_or(0, |s| s.lines.len());
        let max = held.saturating_sub(PANEL_LINES / 2) as f32;
        p.scroll = (p.scroll + lines).clamp(0.0, max.max(0.0));
        let next = p.source.as_ref().filter(|s| s.truncated && p.scroll as usize + PANEL_LINES >= s.lines.len()).map(|s| (s.moniker.clone(), s.start_line + s.lines.len() as u32));
        if let Some((m, from)) = next {
            self.ask(serde_json::json!({ "lod": "source", "moniker": m, "from": from }));
        }
    }

    /// **Switch the layout** (crate ↔ role): the same server, the same cards, other
    /// places — what is held of the other layout is dropped, the camera fits the new map.
    fn set_layout(&mut self, role: bool) {
        if role == self.by_role {
            return;
        }
        self.by_role = role;
        self.asks.clear();
        self.retry.clear();
        self.waiting.clear();
        self.world = None;
        self.tiles.clear();
        self.modules.clear();
        self.expanded = None;
        if self.trace.picked.is_some() {
            // the heads stand on the OTHER map now: the window is asked anew
            let at = self.trace.t_ns as u64;
            crate::trace_replay::scrub(&mut self.trace, at);
            self.ask_trace_window(at, at > 0);
        }
        self.hits.clear();
        self.flight = None;
        self.ask(serde_json::json!({ "lod": "world" }));
    }

    /// Expand a head: ask every caller and callee (double-click again: fold it).
    fn expand(&mut self, card: u64) {
        if self.expanded.as_ref().is_some_and(|x| x.card == card) {
            self.expanded = None;
            return;
        }
        self.ask(serde_json::json!({ "lod": "calls", "card": card }));
    }

    fn select(&mut self, card: u64) {
        self.selected = Some(card);
        if self.nav == NavMode::Focus {
            if let Some(r) = self.card(card).map(|c| c.rect) {
                self.fly_to([(r[0] + r[2]) * 0.5, 0.0, (r[1] + r[3]) * 0.5], 40.0);
            }
        }
    }

    fn toolbar(&mut self, ui: &mut egui::Ui) {
        ui.horizontal_wrapped(|ui| {
            ui.label(format!("Code room of: {}", if self.workspace.is_empty() { "(no workspace selected)" } else { &self.workspace }));
            ui.separator();
            if let Some(m) = NavMode::toggle(ui, self.nav) {
                self.nav = m;
            }
            ui.separator();
            let mut role = self.by_role;
            ui.selectable_value(&mut role, false, "▦ by crate").on_hover_text("functions on their crate's plate, in source order");
            ui.selectable_value(&mut role, true, "✦ by role").on_hover_text("functions placed by what they do — similar roles stand together, across crates (the crate stays a tint)");
            if role != self.by_role {
                self.set_layout(role);
            }
            if ui.button("⊙ fit").on_hover_text("fly back to the whole map").clicked() {
                if let Some(b) = self.world.as_ref().map(|w| w.bounds) {
                    let c = Camera::fit(b);
                    self.fly_to(c.target, c.dist);
                }
            }
            let lit = self.diff.view.is_some();
            if ui.selectable_label(self.diff.open || lit, "🕑 diff").on_hover_text("what changed between two commits, tinted on the map").clicked() {
                if self.diff.open {
                    self.diff = DiffUi::default();
                } else {
                    let repo = if self.diff.repo.is_empty() {
                        self.world.as_ref().and_then(|w| w.repos.first().map(|(n, ..)| n.clone())).unwrap_or_default()
                    } else {
                        self.diff.repo.clone()
                    };
                    self.diff_open(&repo);
                }
            }
            if ui.selectable_label(self.trace.open, "🎬 replay").on_hover_text("a traced run played back on the map: glows, pulses, a timeline").clicked() {
                if self.trace.open {
                    self.trace = Default::default();
                } else {
                    let repo = if self.trace.repo.is_empty() {
                        self.world.as_ref().and_then(|w| w.repos.first().map(|(n, ..)| n.clone())).unwrap_or_default()
                    } else {
                        self.trace.repo.clone()
                    };
                    self.trace_open(&repo);
                }
            }
            ui.separator();
            let edit = ui.add(egui::TextEdit::singleline(&mut self.search).hint_text("🔎 find a crate or fn").desired_width(180.0));
            if edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !self.search.trim().is_empty() {
                let q = self.search.trim().to_string();
                self.ask(serde_json::json!({ "lod": "find", "q": q }));
            }
            if let Some(t) = self.asks.iter().map(|a| a.pending.started).min() {
                ui.separator();
                ui.spinner();
                ui.label(egui::RichText::new(room::asking_line(t)).small());
            }
            if let Some(w) = &self.world {
                ui.separator();
                ui.label(
                    egui::RichText::new(format!(
                        "{} regions · {} crates · {} functions · {} · {} cards · {} splines",
                        w.repos.len(),
                        w.crates.len(),
                        w.total_cards,
                        if self.last.near { "near: cards" } else if self.cam.dist <= MODULES_DIST { "closer: modules" } else { "far: crates" },
                        self.last.cards,
                        self.last.splines
                    ))
                    .small(),
                );
            }
        });
        let mut go: Option<[f32; 4]> = None;
        if !self.hits.is_empty() {
            ui.horizontal_wrapped(|ui| {
                for h in self.hits.iter().take(12) {
                    let label = format!("{} {}", if h["what"] == "crate" { "📦" } else { "ƒ" }, h["path"].as_str().unwrap_or(""));
                    if ui.small_button(fit_tail(&label, 48)).clicked() {
                        go = Some(rect4(&h["rect"]));
                    }
                }
                if ui.small_button("✖").clicked() {
                    self.hits.clear();
                }
            });
        }
        if let Some(r) = go {
            self.fly_to_rect(r);
            self.hits.clear();
        }
        if self.diff.open {
            self.timeline(ui);
        }
        if self.trace.open {
            self.replay_strip(ui);
        }
    }

    /// The replay strip: the repo's traced runs; a picked run gets ▶/⏸, a
    /// speed, and the timeline scrubber (a drag asks the stacks of that moment).
    fn replay_strip(&mut self, ui: &mut egui::Ui) {
        let mut scrubbed: Option<u64> = None;
        let mut picked: Option<String> = None;
        crate::trace_replay::strip(ui, &mut self.trace, &mut picked, &mut scrubbed);
        if let Some(r) = picked {
            self.trace_pick(&r);
        }
        if let Some(ns) = scrubbed {
            self.trace_scrub(ns);
        }
    }

    /// The timeline strip: the repo's consumed commits, oldest first; click
    /// sets `to`, shift-click sets `from`, ↶ diffs a commit against its
    /// parent. The picked window is asked as lod `diff` and tinted in place.
    fn timeline(&mut self, ui: &mut egui::Ui) {
        ui.horizontal_wrapped(|ui| {
            // the repo picker: one button per consumed repo
            let repos: Vec<String> = self.world.as_ref().map(|w| w.repos.iter().map(|(n, ..)| n.clone()).collect()).unwrap_or_default();
            for r in repos {
                if ui.selectable_label(self.diff.repo == r, &r).clicked() && self.diff.repo != r {
                    self.diff = DiffUi::default();
                    self.diff_open(&r);
                }
            }
            if let Some(e) = &self.diff.commits_err {
                ui.label(egui::RichText::new(e.clone()).small().color(Color32::LIGHT_RED));
            }
        });
        let mut pick: Option<(i64, Option<i64>)> = None;
        egui::ScrollArea::horizontal().id_salt("diff-timeline").show(ui, |ui| {
            ui.horizontal(|ui| {
                for c in &self.diff.commits {
                    let short: String = c.sha.chars().take(8).collect();
                    let label = format!("{} {} {}", c.seq, short, fit_head(&c.subject, 32));
                    let sel = self.diff.from == Some(c.seq) || self.diff.to == Some(c.seq);
                    let b = ui.selectable_label(sel, label).on_hover_text(format!(
                        "{}\nclick: diff … → this · shift-click: diff this → …\n↶ beside it: this commit vs its parent",
                        c.subject
                    ));
                    if b.clicked() {
                        if ui.input(|i| i.modifiers.shift) {
                            pick = Some((c.seq, self.diff.to.filter(|t| *t > c.seq)));
                        } else if let Some(f) = self.diff.from.filter(|f| *f < c.seq) {
                            pick = Some((f, Some(c.seq)));
                        } else {
                            pick = Some((c.seq.saturating_sub(1).max(0), Some(c.seq)));
                        }
                    }
                    if c.seq > 0 && ui.small_button("↶").on_hover_text("this commit vs its parent").clicked() {
                        pick = Some((c.seq - 1, Some(c.seq)));
                    }
                }
            });
        });
        if let Some((f, t)) = pick {
            self.diff_pick(f, t);
        }
        let mut open_head: Option<(String, Option<[f32; 2]>)> = None;
        let mut clear = false;
        ui.horizontal_wrapped(|ui| {
            match (&self.diff.view, &self.diff.err) {
                (Some(v), _) => {
                    let (mut a, mut r, mut c) = (0, 0, 0);
                    for h in &v.heads {
                        match h.class.as_str() {
                            "added" => a += 1,
                            "removed" => r += 1,
                            _ => c += 1,
                        }
                    }
                    ui.label(
                        egui::RichText::new(format!("{} → {}: +{a} −{r} ~{c} heads · {} splines", v.from, v.to, v.splines.len())).small(),
                    );
                    for h in &v.heads {
                        let col = class_color(&h.class);
                        let label = match (h.jump, &h.jump_missing) {
                            (Some(j), _) if j >= 0.005 => format!("{} Δ{j:.2}{}", h.name, if h.ghost { " 👻" } else { "" }),
                            (Some(_), _) => format!("{}{}", h.name, if h.ghost { " 👻" } else { "" }),
                            (None, Some(side)) if h.class == "changed" => format!("{} Δ? ({side} vector missing)", h.name),
                            _ => format!("{}{}", h.name, if h.ghost { " 👻" } else { "" }),
                        };
                        if ui.small_button(egui::RichText::new(label).color(col)).clicked() {
                            open_head = Some((h.moniker.clone(), h.at));
                        }
                    }
                    clear = ui.small_button("✖ clear").clicked();
                }
                (None, Some(e)) => {
                    ui.label(egui::RichText::new(e.clone()).small().color(Color32::LIGHT_RED));
                }
                (None, None) if self.diff.from.is_some() => {
                    ui.spinner();
                    ui.label(egui::RichText::new("diffing …").small());
                }
                _ => {
                    ui.label(egui::RichText::new("pick two commits (or ↶ one against its parent)").small());
                }
            }
        });
        // the biggest meaning changes of the window, hottest first
        let mut hot: Vec<(String, Option<[f32; 2]>, String, f32)> = self
            .diff
            .view
            .iter()
            .flat_map(|v| v.heads.iter())
            .filter_map(|h| h.jump.filter(|j| *j >= 0.05).map(|j| (h.moniker.clone(), h.at, h.name.clone(), j)))
            .collect();
        hot.sort_by(|x, y| y.3.partial_cmp(&x.3).unwrap_or(std::cmp::Ordering::Equal));
        if !hot.is_empty() || self.diff.view.as_ref().is_some_and(|v| v.jump_note.is_some()) {
            ui.horizontal_wrapped(|ui| {
                if !hot.is_empty() {
                    ui.label(egui::RichText::new("🔥 biggest meaning changes:").small());
                    for (m, at, name, j) in hot.iter().take(8) {
                        if ui.small_button(egui::RichText::new(format!("{name} Δ{j:.2}")).color(class_color("changed"))).clicked() {
                            open_head = Some((m.clone(), *at));
                        }
                    }
                }
                if let Some(note) = self.diff.view.as_ref().and_then(|v| v.jump_note.clone()) {
                    ui.label(egui::RichText::new(note).small().color(Color32::GRAY));
                }
            });
        }
        if let Some((m, at)) = open_head {
            if let Some(at) = at {
                self.fly_to([at[0], 0.0, at[1]], 60.0);
            }
            self.diff_hover(&m);
        }
        if clear {
            self.diff.view = None;
            self.diff.from = None;
            self.diff.to = None;
            self.diff.hover = None;
        }
        self.before_after_window(ui);
    }

    /// The before/after window of one tinted head: the source at both shas.
    fn before_after_window(&mut self, ui: &mut egui::Ui) {
        let Some(ba) = &self.diff.hover else { return };
        let title = match ba.head.jump {
            Some(j) => format!("{} {} — before ⇄ after · meaning jump Δ{j:.2}", ba.head.class, ba.head.name),
            None => format!("{} {} — before ⇄ after", ba.head.class, ba.head.name),
        };
        let mut open = true;
        let before_place = ba.head.before.as_ref().map(|(f, l, _)| (f.clone(), *l));
        let after_place = ba.head.file.clone().map(|f| (f, ba.head.line.unwrap_or(1)));
        let side = |ui: &mut egui::Ui, name: &str, place: &Option<(String, u32)>, got: &Result<Vec<String>, String>| {
            ui.label(
                egui::RichText::new(match place {
                    Some((f, l)) => format!("{name} · {f}:{l}"),
                    None => format!("{name} · —"),
                })
                .small(),
            );
            match got {
                Ok(lines) => {
                    for l in lines.iter().take(PANEL_LINES) {
                        ui.label(egui::RichText::new(l.clone()).monospace().small());
                    }
                }
                Err(e) => {
                    ui.label(egui::RichText::new(e.clone()).small().color(Color32::GRAY));
                }
            }
        };
        egui::Window::new(title).id(egui::Id::new("diff-before-after")).open(&mut open).max_width(900.0).show(ui.ctx(), |ui| {
            ui.columns(2, |cols| {
                side(&mut cols[0], "before", &before_place, &ba.before);
                side(&mut cols[1], "after", &after_place, &ba.after);
            });
        });
        if !open {
            self.diff.hover = None;
        }
    }

    /// The canvas: input (wheel / pinch zoom to the cursor, drag orbit, right- or
    /// shift-drag pan, click, hover), then the scene.
    fn canvas(&mut self, ui: &mut egui::Ui) {
        // the shared canvas size: never laid out past the visible clip (facett_core::nav)
        let size = facett_core::nav::canvas_size(ui).max(Vec2::new(200.0, 200.0));
        let (rect, resp) = ui.allocate_exact_size(size, egui::Sense::click_and_drag());
        self.last.rect = Some(rect);
        ui.painter().rect_filled(rect, 0.0, Color32::from_rgb(0x0b, 0x0e, 0x14));
        let Some(world) = self.world.clone() else { return };
        // ── input ────────────────────────────────────────────────────────────────
        let pointer = resp.hover_pos();
        let mut zoom = 1.0f32;
        let mut wheel_lines = 0.0f32;
        if resp.hovered() {
            let mut events = 0;
            ui.ctx().input_mut(|i| {
                for e in &i.events {
                    match e {
                        egui::Event::MouseWheel { unit, delta, .. } => {
                            wheel_lines += match unit {
                                egui::MouseWheelUnit::Point => delta.y / 40.0,
                                egui::MouseWheelUnit::Line => delta.y,
                                egui::MouseWheelUnit::Page => delta.y * 10.0,
                            };
                            events += 1;
                        }
                        egui::Event::Zoom(z) => zoom *= *z,
                        _ => {}
                    }
                }
                // the canvas takes the wheel and the pinch: no scroll area around it moves
                i.smooth_scroll_delta = Vec2::ZERO;
            });
            self.last.wheel_events += events;
        }
        let over_panel = pointer.and_then(|p| self.panels.iter().position(|pl| self.card(pl.card).is_some_and(|c| in_quad(&self.cam, rect, panel_corners(c.rect, &self.cam), p))));
        if let Some(pt) = pointer {
            match over_panel {
                Some(i) if wheel_lines != 0.0 => self.scroll_panel(i, -wheel_lines * 3.0),
                _ if wheel_lines != 0.0 => zoom *= 1.18f32.powf(wheel_lines),
                _ => {}
            }
            if (zoom - 1.0).abs() > 1e-4 {
                self.flight = None;
                self.cam.zoom_at(1.0 / zoom, pt, rect);
            }
        }
        if resp.dragged() {
            self.flight = None;
            let d = resp.drag_delta();
            let pan = resp.dragged_by(egui::PointerButton::Secondary) || resp.dragged_by(egui::PointerButton::Middle) || ui.input(|i| i.modifiers.shift);
            if pan {
                if let Some(p) = pointer {
                    if let (Some(a), Some(b)) = (self.cam.ground(p - d, rect), self.cam.ground(p, rect)) {
                        self.cam.target = add(self.cam.target, sub(a, b));
                    }
                }
            } else {
                self.cam.yaw -= d.x * 0.006;
                self.cam.pitch = (self.cam.pitch + d.y * 0.005).clamp(0.25, 1.55);
            }
        }
        if let Some(f) = &self.flight {
            let c = cam_from(f.at(self.now), self.cam.yaw, self.cam.pitch);
            (self.cam.target, self.cam.dist) = (c.target, c.dist);
            if f.done(self.now) {
                self.flight = None;
            } else {
                ui.ctx().request_repaint();
            }
        }
        // ── what the camera sees ─────────────────────────────────────────────────
        let near = self.near();
        let want = if near { self.tiles_in_view(rect) } else { Vec::new() };
        for t in &want {
            if !self.tiles.contains_key(t) {
                self.ask(serde_json::json!({ "lod": "cards", "tile": [t.0, t.1] }));
            }
        }
        self.last.wanted_tiles = want.clone();
        let plates = if self.cam.dist <= MODULES_DIST && !self.by_role { self.plates_in_view(rect) } else { Vec::new() };
        for p in &plates {
            if !self.modules.contains_key(p) {
                self.ask(serde_json::json!({ "lod": "modules", "crate": p }));
            }
        }
        self.last.wanted_plates = plates.clone();
        let lit = Lit { card: self.hovered.or(self.selected) };
        let mut sc = build_scene(&world, &self.held_cards(&want), &self.held_modules(&plates), near, lit);
        if let (Some(x), true) = (&self.expanded, near) {
            expand_scene(&mut sc, &world, x, &self.held_cards(&want));
        }
        for p in &self.panels {
            if let Some(c) = self.card(p.card) {
                panel_scene(&mut sc, p, c, &self.cam, world.geometry);
            }
        }
        if let Some(d) = &self.diff.view {
            diff_scene(&mut sc, &world, d, near);
        }
        if self.trace.picked.is_some() {
            crate::trace_replay::trace_scene(&mut sc, &self.trace, self.now);
        }
        // ── pick ─────────────────────────────────────────────────────────────────
        let mut hovered = None;
        if let (Some(p), None) = (pointer, over_panel) {
            let mut best: Option<(f32, u64)> = None;
            for h in &sc.heads {
                if in_quad(&self.cam, rect, h.corners, p) {
                    let d = self.cam.project(h.corners[0], rect).map_or(f32::MAX, |x| x.1);
                    if best.is_none_or(|b| d < b.0) {
                        best = Some((d, h.card));
                    }
                }
            }
            hovered = best.map(|b| b.1);
        }
        self.hovered = hovered;
        match (hovered, self.hover_since) {
            (Some(h), Some((was, t))) if h == was => {
                if self.now - t >= HOVER_OPEN_S && !self.panels.iter().any(|p| p.card == h) {
                    self.open_panel(h, false);
                }
            }
            (Some(h), _) => self.hover_since = Some((h, self.now)),
            (None, _) => {
                self.hover_since = None;
                if over_panel.is_none() {
                    self.panels.retain(|p| p.pinned);
                }
            }
        }
        if hovered.is_some() {
            ui.ctx().request_repaint_after(std::time::Duration::from_millis(50));
        }
        if resp.double_clicked() {
            if let Some(h) = hovered {
                self.expand(h);
            }
        }
        if resp.clicked() {
            if let Some(i) = over_panel {
                self.panels[i].pinned = true;
            } else if let Some(h) = hovered {
                self.select(h);
                self.open_panel(h, true);
            } else if !near {
                let p = pointer.unwrap_or(rect.center());
                if let Some(i) = sc.plates.iter().rev().find(|(_, c)| in_quad(&self.cam, rect, *c, p)).map(|x| x.0) {
                    self.fly_to_rect(world.crates[i].rect);
                }
            } else {
                self.selected = None;
            }
        }
        if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
            self.panels.clear();
        }
        // ── draw ─────────────────────────────────────────────────────────────────
        self.last.near = near;
        self.last.cards = sc.heads.len();
        self.last.splines = sc.splines.len();
        self.last.heads_screen = sc.heads.iter().filter_map(|h| self.cam.project(centre(h.corners), rect).map(|p| (h.card, p.0))).filter(|(_, p)| rect.contains(*p)).collect();
        self.last.diff_heads_screen = self
            .diff
            .view
            .iter()
            .flat_map(|d| d.heads.iter())
            .filter_map(|h| {
                let at = h.at?;
                let p = self.cam.project([at[0], 0.0, at[1]], rect)?;
                rect.contains(p.0).then(|| (h.class.clone(), h.moniker.clone(), p.0))
            })
            .collect();
        self.last.plates_screen = sc.plates.iter().filter_map(|(i, c)| self.cam.project(centre(*c), rect).map(|p| (*i, p.0))).filter(|(_, p)| rect.contains(*p)).collect();
        self.last.panels_screen = self.panels.iter().filter_map(|p| self.card(p.card).and_then(|c| self.cam.project(centre(panel_corners(c.rect, &self.cam)), rect)).map(|x| (p.card, x.0))).collect();
        self.last.gpu = draw_gpu(ui, rect, &self.cam, &sc);
        if !self.last.gpu {
            self.last.labels = paint_cpu(ui, rect, &self.cam, &sc);
        }
    }
}

fn centre(c: [[f32; 3]; 4]) -> [f32; 3] {
    mul(add(add(c[0], c[1]), add(c[2], c[3])), 0.25)
}

/// Is screen point `p` inside the projected quad?
fn in_quad(cam: &Camera, rect: Rect, c: [[f32; 3]; 4], p: Pos2) -> bool {
    let s: Option<Vec<Pos2>> = c.iter().map(|x| cam.project(*x, rect).map(|y| y.0)).collect();
    let Some(s) = s else { return false };
    let mut sign = 0.0f32;
    for i in 0..4 {
        let (a, b) = (s[i], s[(i + 1) % 4]);
        let cr = (b - a).x * (p - a).y - (b - a).y * (p - a).x;
        if cr.abs() < 1e-6 {
            continue;
        }
        if sign == 0.0 {
            sign = cr.signum();
        } else if cr.signum() != sign {
            return false;
        }
    }
    sign != 0.0
}

/// The scene as facett's one 3-D path takes it (straight-alpha colours).
pub fn scene3d_of(sc: &Scene) -> facett_core::render::scene3d::Scene3D {
    use facett_core::render::scene3d::{Line3, Quad3, Scene3D, Text3};
    let c = |c: Color32| c.to_srgba_unmultiplied();
    Scene3D {
        quads: sc.quads.iter().map(|q| Quad3 { origin: q.origin, u: q.u, v: q.v, color: c(q.color) }).collect(),
        lines: sc.lines.iter().map(|l| Line3 { a: l.a, b: l.b, width_px: l.width_px, color: c(l.color) }).collect(),
        texts: sc.texts.iter().map(|t| Text3 { origin: t.origin, right: t.right, up: t.up, text: t.text.clone(), color: c(t.color) }).collect(),
        sheets: sc.sheets.iter().map(|q| Quad3 { origin: q.origin, u: q.u, v: q.v, color: c(q.color) }).collect(),
        sheet_texts: sc.sheet_texts.iter().map(|t| Text3 { origin: t.origin, right: t.right, up: t.up, text: t.text.clone(), color: c(t.color) }).collect(),
        clear: [0.043, 0.055, 0.078, 1.0],
        min_text_px: MIN_TEXT_PX,
    }
}

/// The camera as facett's 3-D path takes it.
pub fn camera3_of(cam: &Camera) -> facett_core::render::scene3d::Camera3 {
    facett_core::render::scene3d::Camera3 { eye: cam.eye(), target: cam.target, up: [0.0, 1.0, 0.0], fovy: FOVY, near: (cam.dist * 0.01).max(0.05), far: cam.dist * 60.0 + 1000.0 }
}

/// The GPU path: facett's 3-D scene (world-space SDF text planes with depth) when the
/// host installed it; `false` and the CPU painter draws otherwise.
#[cfg(feature = "gpu")]
fn draw_gpu(ui: &egui::Ui, rect: Rect, cam: &Camera, sc: &Scene) -> bool {
    use facett_core::render::gpu::scene3d as s3;
    if !s3::scene3d_renderer_installed() {
        return false;
    }
    s3::draw_scene3d(ui, rect, &camera3_of(cam), Arc::new(scene3d_of(sc)))
}

#[cfg(not(feature = "gpu"))]
fn draw_gpu(_ui: &egui::Ui, _rect: Rect, _cam: &Camera, _sc: &Scene) -> bool {
    false
}

/// **The CPU painter** (no GPU lane): far to near, quads as polygons, lines as
/// segments, text flat at its projected size and angle. Returns the labels drawn.
fn paint_cpu(ui: &egui::Ui, rect: Rect, cam: &Camera, sc: &Scene) -> usize {
    enum Item<'a> {
        Quad(&'a Quad),
        Line(&'a Line),
        Text(&'a Text),
    }
    let painter = ui.painter_at(rect);
    // Things lying flat on the map (regions, plates, cards, their words) are painted
    // first, lowest layer first — a plate's centre may be nearer than a card on it, so
    // depth would paint the plate over its cards; the rest (the splines rising out of
    // the plane, a source sheet facing the eye) after, far to near.
    let depth = |p: [f32; 3]| cam.project(p, rect).map(|x| x.1);
    let key = |flat: bool, y: f32, d: f32| if flat { (0u8, y, 0.0) } else { (1u8, 0.0, -d) };
    let mut items: Vec<((u8, f32, f32), u8, Item<'_>)> = Vec::with_capacity(sc.quads.len() + sc.sheets.len() + sc.lines.len() + sc.texts.len() + sc.sheet_texts.len());
    for q in sc.quads.iter().chain(&sc.sheets) {
        if let Some(d) = depth(add(q.origin, mul(add(q.u, q.v), 0.5))) {
            items.push((key(q.u[1] == 0.0 && q.v[1] == 0.0, q.origin[1], d), 0, Item::Quad(q)));
        }
    }
    for l in &sc.lines {
        if let Some(d) = depth(mul(add(l.a, l.b), 0.5)) {
            items.push((key(l.a[1] == l.b[1] && l.a[1] <= 0.0, l.a[1], d), 1, Item::Line(l)));
        }
    }
    for t in sc.texts.iter().chain(&sc.sheet_texts) {
        if let Some(d) = depth(t.origin) {
            items.push((key(t.right[1] == 0.0 && t.up[1] == 0.0, t.origin[1], d - 0.01), 2, Item::Text(t)));
        }
    }
    // on a tie quads, then lines, then text
    items.sort_by(|a, b| a.0.0.cmp(&b.0.0).then(a.0.1.total_cmp(&b.0.1)).then(a.0.2.total_cmp(&b.0.2)).then(a.1.cmp(&b.1)));
    let mut labels = 0;
    for (_, _, it) in items {
        match it {
            Item::Quad(q) => {
                let c = [q.origin, add(q.origin, q.u), add(add(q.origin, q.u), q.v), add(q.origin, q.v)];
                let pts: Option<Vec<Pos2>> = c.iter().map(|p| cam.project(*p, rect).map(|x| x.0)).collect();
                if let Some(pts) = pts {
                    painter.add(egui::Shape::convex_polygon(pts, q.color, egui::Stroke::NONE));
                }
            }
            Item::Line(l) => {
                if let (Some(a), Some(b)) = (cam.project(l.a, rect), cam.project(l.b, rect)) {
                    painter.line_segment([a.0, b.0], egui::Stroke::new(l.width_px, l.color));
                }
            }
            Item::Text(t) => {
                let (Some(o), Some(u), Some(r)) = (cam.project(t.origin, rect), cam.project(add(t.origin, t.up), rect), cam.project(add(t.origin, t.right), rect)) else { continue };
                let em = (u.0 - o.0).length();
                if em < MIN_TEXT_PX || !rect.expand(200.0).contains(o.0) {
                    continue;
                }
                let alpha = ((em - MIN_TEXT_PX) / (MIN_TEXT_PX * 0.5)).clamp(0.0, 1.0);
                let base = r.0 - o.0;
                let angle = base.y.atan2(base.x);
                let color = t.color.gamma_multiply(alpha);
                let galley = painter.layout_no_wrap(t.text.clone(), egui::FontId::monospace(em), color);
                // the galley's top-left, so its baseline sits on `origin`
                let up = (u.0 - o.0).normalized();
                painter.add(egui::epaint::TextShape::new(o.0 + up * (em * 0.95), galley, color).with_angle(angle));
                labels += 1;
            }
        }
    }
    labels
}

impl Facet for CodeRoom3DTab {
    fn title(&self) -> &str {
        "Code room"
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        let dt = (ui.input(|i| i.time) - self.now).clamp(0.0, 0.25);
        self.now = ui.input(|i| i.time);
        if self.trace.picked.is_some() {
            if crate::trace_replay::advance(&mut self.trace, dt, self.now) {
                if let (Some(r), Some(after)) = (&self.trace.picked, self.trace.view.as_ref().and_then(|v| v.next_after_seq)) {
                    let from = self.trace.view.as_ref().map_or(0, |v| v.from_ns);
                    self.ask(serde_json::json!({ "lod": "trace", "run_id": r.run_id, "from_ns": from, "after_seq": after, "limit": crate::trace_replay::PAGE }));
                }
            }
            if self.trace.playing {
                ui.ctx().request_repaint();
            }
        }
        if !self.nav_set {
            self.nav = NavMode::remembered(ui.ctx());
            self.nav_set = true;
        }
        self.poll();
        if !self.asks.is_empty() || !self.retry.is_empty() {
            ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
        }
        self.toolbar(ui);
        if self.world.is_none() {
            room::callout(ui, "code map (repos → crates → function cards with their params, locals and calls; zoom in for the cards)", &self.world_answer);
        }
        self.canvas(ui);
    }

    fn state_json(&self) -> serde_json::Value {
        let w = self.world.as_ref();
        let panels: Vec<Value> = self
            .panels
            .iter()
            .map(|p| {
                serde_json::json!({
                    "card": p.card, "moniker": p.moniker, "pinned": p.pinned, "scroll": p.scroll,
                    "first_line": p.source.as_ref().and_then(|s| s.lines.get(p.scroll as usize).cloned()),
                    "lines": p.source.as_ref().map(|s| s.lines.len()),
                    "start_line": p.source.as_ref().map(|s| s.start_line),
                    "sha": p.source.as_ref().and_then(|s| s.sha.clone()),
                    "state": p.state,
                })
            })
            .collect();
        let mut tiles: Vec<Value> = self.tiles.iter().map(|((x, z), r)| serde_json::json!({ "tile": [x, z], "cards": r.as_ref().map(Vec::len).ok(), "refusal": r.as_ref().err() })).collect();
        tiles.sort_by_key(|t| t["tile"].to_string());
        let xy = |p: &Pos2| [p.x, p.y];
        serde_json::json!({
            "kind": "code_map",
            "workspace": self.workspace,
            "pending": !self.asks.is_empty(),
            "asks": self.asks.iter().map(|a| a.key.clone()).collect::<Vec<_>>(),
            "queued": self.retry.keys().cloned().collect::<Vec<_>>(),
            "from_room": w.is_some(),
            "answer": self.world_answer.state_json(),
            "version": w.map(|w| w.version),
            "repos": w.map(|w| w.repos.len()),
            "crates": w.map(|w| w.crates.len()),
            "world_edges": w.map(|w| w.edges.len()),
            "total_cards": w.map(|w| w.total_cards),
            "camera": { "target": self.cam.target, "dist": self.cam.dist, "yaw": self.cam.yaw, "pitch": self.cam.pitch, "flying": self.flight.is_some() },
            "nav": self.nav.label(),
            "layout": if self.by_role { "role" } else { "crate" },
            "trace": self.trace.picked.as_ref().map(|r| {
                let v = self.trace.view.as_ref();
                serde_json::json!({
                    "run_id": r.run_id, "duration_ns": r.duration_ns,
                    "t_ns": self.trace.t_ns as u64, "playing": self.trace.playing, "speed": self.trace.speed,
                    "applied": self.trace.applied, "events": v.map(|v| v.events.len()), "unplaced": v.map(|v| v.unplaced),
                    "glows": self.trace.glows.iter().map(|g| serde_json::json!({
                        "thread": g.thread,
                        "path": v.and_then(|v| v.heads.get(g.head)).map(|h| h.path.clone()),
                    })).collect::<Vec<_>>(),
                    "pulses": self.trace.pulses.len(),
                    "err": self.trace.err,
                })
            }),
            "lod": if self.last.near { "cards" } else if self.cam.dist <= MODULES_DIST { "modules" } else { "world" },
            "plates_wanted": self.last.wanted_plates,
            "modules_held": self.modules.iter().map(|(k, r)| (k.to_string(), serde_json::json!(r.as_ref().map(Vec::len).ok()))).collect::<serde_json::Map<_, _>>(),
            "tiles_wanted": self.last.wanted_tiles.iter().map(|t| [t.0, t.1]).collect::<Vec<_>>(),
            "tiles_held": tiles,
            "cards_drawn": self.last.cards,
            "splines_drawn": self.last.splines,
            "labels_drawn": self.last.labels,
            "gpu": self.last.gpu,
            "wheel_events": self.last.wheel_events,
            "heads_screen": self.last.heads_screen.iter().map(|(c, p)| serde_json::json!({ "card": c, "at": xy(p) })).collect::<Vec<_>>(),
            "diff": self.diff.view.as_ref().map(|v| serde_json::json!({
                "repo": v.repo, "from": v.from, "to": v.to,
                "from_sha": v.from_sha, "to_sha": v.to_sha,
                "heads": v.heads.iter().map(|h| serde_json::json!({
                    "class": h.class, "name": h.name, "moniker": h.moniker, "ghost": h.ghost, "card": h.card, "at": h.at,
                    "jump": h.jump, "jump_missing": h.jump_missing,
                })).collect::<Vec<_>>(),
                "jump_note": v.jump_note,
                "splines": v.splines.len(),
                "heads_screen": self.last.diff_heads_screen.iter().map(|(c, m, p)| serde_json::json!({ "class": c, "moniker": m, "at": xy(p) })).collect::<Vec<_>>(),
                "before_after": self.diff.hover.as_ref().map(|b| serde_json::json!({
                    "head": b.head.name,
                    "before_lines": b.before.as_ref().map(Vec::len).ok(),
                    "after_lines": b.after.as_ref().map(Vec::len).ok(),
                })),
            })),
            "diff_commits": self.diff.commits.len(),
            "plates_screen": self.last.plates_screen.iter().map(|(c, p)| serde_json::json!({ "crate": c, "at": xy(p) })).collect::<Vec<_>>(),
            "panels_screen": self.last.panels_screen.iter().map(|(c, p)| serde_json::json!({ "card": c, "at": xy(p) })).collect::<Vec<_>>(),
            "canvas": self.last.rect.map(|r| [r.min.x, r.min.y, r.max.x, r.max.y]),
            "hovered": self.hovered,
            "selected": self.selected,
            "panels": panels,
            "hits": self.hits.len(),
            "hits_list": self.hits.iter().take(12).cloned().collect::<Vec<_>>(),
            "expanded": self.expanded.as_ref().map(|x| serde_json::json!({
                "card": x.card,
                "callers": x.callers.iter().map(|l| l.card).collect::<Vec<_>>(),
                "callees": x.callees.iter().map(|l| l.card).collect::<Vec<_>>(),
                "total_callers": x.total_callers, "total_callees": x.total_callees,
            })),
        })
    }

    fn update_json(&mut self, msg_json: &str) {
        let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) else { return };
        if let Some(ws) = v.get("set_workspace").and_then(|t| t.as_str()) {
            if let Some((endpoint, token)) = &self.forge {
                self.source = ForgeRoom::shared(endpoint, token, ws);
            }
            self.workspace = ws.to_string();
            self.asks.clear();
            self.retry.clear();
            self.world = None;
            self.tiles.clear();
            self.panels.clear();
            self.ask(serde_json::json!({ "lod": "world" }));
            return;
        }
        if let Some(l) = v.get("layout").and_then(Value::as_str) {
            self.set_layout(l == "role");
        }
        if let Some(c) = v.get("camera") {
            let g = |k: &str, d: f32| c.get(k).and_then(Value::as_f64).map_or(d, |x| x as f32);
            if let Some(t) = c.get("target").and_then(Value::as_array) {
                self.cam.target = [f(&t[0]), f(&t[1]), f(&t[2])];
            }
            self.cam.dist = g("dist", self.cam.dist);
            self.cam.yaw = g("yaw", self.cam.yaw);
            self.cam.pitch = g("pitch", self.cam.pitch);
            self.yaw_pitch_set = true;
            self.flight = None;
        }
        if let Some(n) = v.get("nav").and_then(Value::as_str) {
            self.nav = if n.to_lowercase().contains("focus") { NavMode::Focus } else { NavMode::Stay };
            self.nav_set = true;
        }
        if let Some(q) = v.get("find").and_then(Value::as_str) {
            self.search = q.to_string();
            self.ask(serde_json::json!({ "lod": "find", "q": q }));
        }
        if let Some(c) = v.get("open_source").and_then(Value::as_u64) {
            self.open_panel(c, true);
        }
        if let Some(c) = v.get("expand").and_then(Value::as_u64) {
            self.expand(c);
        }
        if let Some(r) = v.get("fly_to_rect") {
            self.fly_to_rect(rect4(r));
        }
        if let Some(r) = v.get("diff_open").and_then(Value::as_str) {
            self.diff_open(r);
        }
        if let Some(d) = v.get("diff_pick") {
            if let Some(from) = d.get("from").and_then(Value::as_i64) {
                if let Some(r) = d.get("repo").and_then(Value::as_str) {
                    self.diff_open(r);
                }
                self.diff_pick(from, d.get("to").and_then(Value::as_i64));
            }
        }
        if let Some(r) = v.get("trace_open").and_then(Value::as_str) {
            self.trace_open(r);
        }
        if let Some(r) = v.get("trace_pick").and_then(Value::as_str) {
            self.trace_pick(r);
        }
        if let Some(p) = v.get("trace_play") {
            if let Some(s) = p.get("speed").and_then(Value::as_f64) {
                self.trace.speed = s as f32;
            }
            self.trace.playing = p.get("pause").and_then(Value::as_bool) != Some(true);
        }
        if let Some(ns) = v.get("trace_scrub").and_then(Value::as_u64) {
            self.trace_scrub(ns);
        }
        if let Some(dt) = v.get("trace_step").and_then(Value::as_f64) {
            // a robot's frame: the replay advanced by `dt` UI seconds
            self.now += dt;
            if crate::trace_replay::advance(&mut self.trace, dt, self.now) {
                if let (Some(r), Some(after)) = (&self.trace.picked, self.trace.view.as_ref().and_then(|x| x.next_after_seq)) {
                    let from = self.trace.view.as_ref().map_or(0, |x| x.from_ns);
                    self.ask(serde_json::json!({ "lod": "trace", "run_id": r.run_id, "from_ns": from, "after_seq": after, "limit": crate::trace_replay::PAGE }));
                }
            }
        }
        if let Some(m) = v.get("diff_hover").and_then(Value::as_str) {
            self.diff_hover(m);
        }
        if v.get("reread").and_then(|t| t.as_bool()) == Some(true) {
            self.ask(serde_json::json!({ "lod": "world" }));
            self.wait();
            return;
        }
        if v.get("wait").and_then(|t| t.as_bool()) == Some(true) {
            self.wait();
        }
    }
}

/// Canned `Viz.CodeMap` answers (a robot's room, and this module's tests) — never in a
/// shipped build (`fixtures`).
#[cfg(any(test, feature = "fixtures"))]
pub mod testing {
    use super::*;
    use serde_json::json;
    use std::sync::Mutex;

    /// A refusal body, as the door sends one.
    pub fn refusal(state: &str, note: &str) -> Value {
        json!({ "state": state, "refused": true, "note": note })
    }

    fn room(workspace: &str, repo: &str, seq: i64, nodes: u64, edges: u64) -> Value {
        json!({
            "workspace": workspace, "universe": false,
            "repos": [{ "repo": repo, "seq": seq, "sha": "0123456789abcdef0123" }],
            "nodes": nodes, "edges_total": edges,
        })
    }

    /// The `shop` map: one repo and the dependencies; `checkout` calls `total` twice
    /// and `send` in the dependency `stripe`.
    pub fn world_answer() -> Value {
        let mut a = json!({
            "state": "complete", "lod": "world", "version": 7,
            "bounds": [0, 0, 120, 80], "tile": 128,
            "geometry": { "card_w": 8.0, "head_h": 1.2, "row_h": 0.6 },
            "repos": [{ "name": "shop", "rect": [0, 0, 70, 80], "title": 12 }, { "name": "dependencies", "rect": [90, 0, 120, 40], "title": 12 }],
            "crates": [
                { "id": "shop@0.1.0", "name": "shop", "version": "0.1.0", "repo": "shop", "local": true, "rect": [8, 20, 60, 70], "title": 5, "cards": 3 },
                { "id": "stripe@2.0.0", "name": "stripe", "version": "2.0.0", "repo": null, "local": false, "rect": [98, 20, 116, 34], "title": 5, "cards": 1 },
            ],
            "edges": [[0, 1, 1, 1, 1]],
            "tiles": [[0, 0, 4]],
            "total_cards": 4,
            "evidence": { "notes": [] },
        });
        a["room"] = room("w", "shop", 1, 40, 60);
        a
    }

    /// The diff 1 → 2 of the canned map: `total` changed (card 1), `dead`
    /// removed (a ghost spot south of the shop plate), `born` added (card 2),
    /// one added CALLS spline born → total and one removed, ghosted.
    pub fn diff_answer() -> Value {
        json!({
            "state": "complete", "lod": "diff", "repo": "shop", "from": 1, "to": 2,
            "from_sha": "aaaa111122223333aaaa", "to_sha": "bbbb111122223333bbbb",
            "nodes": [
                { "class": "changed", "moniker": "m total().", "name": "total", "kind": "fn", "crate": "shop@0.1.0",
                  "card": 1, "at": [26.5, 28.0], "ghost": false, "file": "src/lib.rs", "line": 62, "end_line": 66,
                  "before": { "file": "src/lib.rs", "line": 61, "end_line": 65 },
                  "jump": 0.91, "payload_changed": true, "body_changed": true },
                { "class": "removed", "moniker": "m dead().", "name": "dead", "kind": "fn", "crate": "shop@0.1.0",
                  "card": null, "at": [12.0, 71.5], "ghost": true, "file": "src/lib.rs", "line": 70, "end_line": 72,
                  "before": { "file": "src/lib.rs", "line": 70, "end_line": 72 } },
                { "class": "added", "moniker": "m born().", "name": "born", "kind": "fn", "crate": "shop@0.1.0",
                  "card": 2, "at": [15.0, 35.0], "ghost": false, "file": "src/pay.rs", "line": 5, "end_line": 8, "before": null },
            ],
            "edges": [
                { "class": "added", "from": "m born().", "to": "m total().", "from_at": [15.0, 35.0], "to_at": [26.5, 28.0] },
                { "class": "removed", "from": "m dead().", "to": "m total().", "from_at": [12.0, 71.5], "to_at": [26.5, 28.0] },
            ],
            "plates": [ { "key": "shop@0.1.0", "crate": 0, "rect": [8, 20, 60, 70], "added": 1, "removed": 1, "changed": 1, "jump": 0.91 } ],
            "top_jumps": [ { "moniker": "m total().", "name": "total", "jump": 0.91, "card": 1, "at": [26.5, 28.0] } ],
            "jump_coverage": { "changed": 1, "measured": 1, "missing": 0, "note": null },
            "events_read": 0,
            "evidence": { "notes": [] },
        })
    }

    pub fn tile_answer() -> Value {
        json!({
            "state": "complete", "lod": "cards", "tile": [0, 0],
            "geometry": { "card_w": 8.0, "head_h": 1.2, "row_h": 0.6 },
            "cards": [
                { "card": 0, "id": "m checkout().", "path": "shop::checkout", "name": "checkout", "kind": "fn", "crate": 0, "home": true,
                  "rect": [11, 28, 19, 33.5], "file": "src/lib.rs", "start_line": 10, "end_line": 60,
                  "rows": [ { "k": "param", "name": "cart", "ty": "&Cart", "line": 10 }, { "k": "ret", "name": "->", "ty": "u64", "line": 10 }, { "k": "local", "name": "sum", "ty": "u64", "line": 12 } ],
                  "more_rows": 0,
                  "sites": [
                    { "to": "m total().", "to_name": "total", "to_card": 1, "to_at": [26.5, 28], "to_crate": 0, "rel": "same", "lines": [12, 14], "n": 2 },
                    { "to": "s send().", "to_name": "send", "to_card": 3, "to_at": [107, 28], "to_crate": 1, "rel": "dep", "lines": [18], "n": 1 }
                  ],
                  "more_sites": 0 },
                { "card": 1, "id": "m total().", "path": "shop::total", "name": "total", "kind": "fn", "crate": 0, "home": true,
                  "rect": [22.5, 28, 30.5, 31.3], "file": "src/lib.rs", "start_line": 62, "end_line": 66,
                  "rows": [ { "k": "param", "name": "cart", "ty": "&Cart", "line": 62 } ], "more_rows": 0, "sites": [], "more_sites": 0 },
                { "card": 2, "id": "m charge().", "path": "shop::pay::charge", "name": "charge", "kind": "fn", "crate": 0, "home": true,
                  "rect": [11, 35, 19, 37], "file": "src/pay.rs", "start_line": 5, "end_line": 20, "rows": [], "more_rows": 0, "sites": [], "more_sites": 0 },
            ],
            "evidence": { "notes": [] },
        })
    }

    /// `checkout`'s source (lines 10–60), 20 lines a page.
    pub fn source_answer(from: u64) -> Value {
        let all: Vec<String> = (10..=60)
            .map(|n| if n == 10 { "pub fn checkout(cart: &Cart) -> u64 {".to_string() } else { format!("    let x{n} = total(cart); // line {n}") })
            .collect();
        let lines: Vec<String> = all.iter().skip((from - 10) as usize).take(20).cloned().collect();
        json!({ "state": "complete", "lod": "source", "start_line": 10, "end_line": 60, "from": from, "sha": "abc", "lines": lines, "truncated": from + 20 <= 60, "evidence": { "notes": [] } })
    }

    /// A room source answering `Viz.CodeMap` by `lod` (and `from` for a source page).
    pub struct MapRoom {
        pub asked: Mutex<Vec<Value>>,
        pub world: Value,
        /// The source pages stay queued on jera (a lane held by long work).
        pub source_queued: std::sync::atomic::AtomicBool,
    }

    impl RoomSource for MapRoom {
        fn ask(&self, surface: &str, p: &Value) -> RoomAnswer {
            self.asked.lock().unwrap().push(p.clone());
            let body = if surface != S_CODE_MAP {
                refusal("invalid_argument", "not the code map")
            } else {
                match p["lod"].as_str().unwrap_or("") {
                    "world" => self.world.clone(),
                    "cards" if p["tile"] == json!([0, 0]) => tile_answer(),
                    "cards" => json!({ "state": "complete", "cards": [], "evidence": { "notes": [] } }),
                    "source" if self.source_queued.load(std::sync::atomic::Ordering::SeqCst) => json!({ "surface": S_CODE_MAP, "state": "queued", "job_id": "j-7", "ms": 3000, "note": "queued on jera — job j-7 (queued 3s); ask again for the answer" }),
                    "source" => source_answer(p["from"].as_u64().unwrap_or(10)),
                    "find" => json!({ "state": "complete", "hits": [{ "what": "fn", "card": 1, "path": "shop::total", "rect": [22.5, 28, 30.5, 31.3] }], "evidence": { "notes": [] } }),
                    "modules" => json!({
                        "state": "complete", "lod": "modules", "crate": p["crate"],
                        "modules": [
                            { "name": "shop", "rect": [10, 26, 34, 40], "cards": 2, "types": [["Cart", "struct"]], "total_types": 1 },
                            { "name": "shop::pay", "rect": [10, 41, 34, 48], "cards": 1, "types": [["Charge", "struct"], ["Gateway", "trait"]], "total_types": 3 }
                        ],
                        "evidence": { "notes": [] } }),
                    "calls" => json!({
                        "state": "complete", "lod": "calls", "card": p["card"],
                        "callers": [
                            { "card": 0, "name": "checkout", "path": "shop::checkout", "at": [15, 28], "rect": [11, 28, 19, 33.5], "rel": "same", "lines": [12, 14] },
                            { "card": 9, "name": "render", "path": "web::render", "at": [40, 60], "rect": [36, 60, 44, 62], "rel": "repo", "lines": [5] }
                        ],
                        "callees": [], "total_callers": 2, "total_callees": 0, "evidence": { "notes": [] } }),
                    "commits" => json!({
                        "state": "complete", "lod": "commits", "repo": p["repo"], "tip": 2,
                        "commits": [
                            { "seq": 1, "sha": "aaaa111122223333aaaa", "subject": "one", "status": "ok", "at_tip": false },
                            { "seq": 2, "sha": "bbbb111122223333bbbb", "subject": "dead goes, total documented, born arrives", "status": "ok", "at_tip": true }
                        ],
                        "evidence": { "notes": [] } }),
                    "diff" => diff_answer(),
                    "trace_runs" => json!({
                        "state": "complete", "lod": "trace_runs", "repo": p["repo"],
                        "runs": [{
                            "run_id": "r-1", "repo": "shop", "commit": "bbbb1111", "target": "shop-bin", "source": "xray",
                            "ts_micros": 1, "chunks": 1, "chunks_filed": 1,
                            "events": 4, "total_events": 4, "truncated": false,
                            "threads": 2, "duration_ns": 1_000_000u64,
                            "functions": 3, "mapped_functions": 3, "local_functions": 3, "mapped_local_functions": 3,
                            "mapped_events": 4
                        }],
                        "evidence": { "notes": [] } }),
                    "trace" => json!({
                        "state": "complete", "lod": "trace", "run_id": p["run_id"],
                        "from_ns": p["from_ns"], "to_ns": null,
                        "events": [
                            [1u64, 100_000u64, 0u64, 0u8, 0usize, null],
                            [2u64, 300_000u64, 0u64, 0u8, 1usize, 0usize],
                            [3u64, 500_000u64, 0u64, 1u8, 1usize, null],
                            [4u64, 600_000u64, 1u64, 0u8, 2usize, null]
                        ],
                        "unplaced": 0, "more": false, "next_after_seq": 4,
                        "stacks": if p["stacks"] == json!(true) { json!([{ "thread": 0, "heads": [0] }]) } else { json!([]) },
                        "heads": [
                            { "card": 0, "at": [15.0, 28.0], "stable_id": "f shop::checkout().", "path": "shop::checkout", "symbol": "checkout" },
                            { "card": 1, "at": [26.5, 28.0], "stable_id": "f shop::total().", "path": "shop::total", "symbol": "total" },
                            { "card": 9, "at": [40.0, 60.0], "stable_id": "f web::render().", "path": "web::render", "symbol": "render" }
                        ],
                        "evidence": { "notes": [] } }),
                    "source_at" => json!({
                        "state": "complete", "lod": "source_at", "repo": p["repo"], "sha": p["sha"], "file": p["file"], "from": p["from"],
                        "file_lines": 3,
                        "lines": [format!("// at {}", p["sha"].as_str().unwrap_or("")), "fn shown() {}".to_string()],
                        "evidence": { "notes": [] } }),
                    _ => refusal("invalid_argument", "no such lod"),
                }
            };
            RoomAnswer::from_body(S_CODE_MAP, "canned map", body)
        }
        fn describe(&self) -> String {
            "canned map".into()
        }
    }

    pub fn map_room() -> Arc<MapRoom> {
        Arc::new(MapRoom { asked: Mutex::new(Vec::new()), world: world_answer(), source_queued: Default::default() })
    }

    pub fn refusing_room() -> Arc<MapRoom> {
        Arc::new(MapRoom { asked: Mutex::new(Vec::new()), world: refusal("not_consumed", "nothing of `shop` is consumed yet"), source_queued: Default::default() })
    }
}

#[cfg(test)]
mod tests {
    use super::testing::*;
    use super::*;
    use serde_json::json;

    fn model() -> (World, Vec<CardView>) {
        let src = map_room();
        let w = world_of(&src.ask(S_CODE_MAP, &json!({ "lod": "world" }))).unwrap();
        let c = cards_of(&src.ask(S_CODE_MAP, &json!({ "lod": "cards", "tile": [0, 0] }))).unwrap();
        (w, c)
    }

    fn view() -> Rect {
        Rect::from_min_size(Pos2::ZERO, Vec2::new(1200.0, 800.0))
    }

    /// The near view the cards open at: over them, where the heads read.
    fn near_cam() -> Camera {
        Camera { target: [20.0, 0.0, 32.0], dist: 40.0, yaw: 0.0, pitch: 1.0 }
    }

    fn screen_box(cam: &Camera, pts: &[[f32; 3]]) -> Rect {
        let s: Vec<Pos2> = pts.iter().map(|p| cam.project(*p, view()).unwrap().0).collect();
        Rect::from_points(&s)
    }

    /// A text's screen box (its baseline, its em up, its length along).
    fn text_box(cam: &Camera, t: &Text) -> Rect {
        let len = t.text.chars().count() as f32 * ADV;
        let o = t.origin;
        screen_box(cam, &[o, add(o, mul(t.right, len)), add(o, t.up), add(add(o, t.up), mul(t.right, len))])
    }

    /// **A spline behind the unfolded source stays visible through the sheet**, rendered
    /// on facett's 3-D GPU path: the sheet is half transparent and writes no depth, so a
    /// line standing behind it shows (dimmed) where a depth-writing quad cut it away.
    /// Software rasterisers count with `FACETT_ALLOW_SOFTWARE_GPU=1`.
    #[cfg(feature = "gpu")]
    #[test]
    fn a_spline_behind_the_source_sheet_shows_through_it() {
        let (_, cards) = model();
        let card = &cards[0];
        let cam = near_cam();
        let mut sc = Scene::default();
        let panel = Panel { card: card.card, moniker: card.id.clone(), pinned: true, scroll: 0.0, source: None, state: Some("…".into()) };
        let g = Geometry::default();
        panel_scene(&mut sc, &panel, card, &cam, g);
        // a white spline segment straight behind the sheet's centre (further along the eye's ray)
        let mid = centre(panel_corners(card.rect, &cam));
        let behind = add(mid, mul(sub(mid, cam.eye()), 0.5));
        let (_, right, _) = cam.basis();
        sc.lines.push(Line { a: add(behind, mul(right, -3.0)), b: add(behind, mul(right, 3.0)), width_px: 8.0, color: Color32::WHITE });
        let (w, h) = (1200u32, 800u32);
        let (s3, c3) = (scene3d_of(&sc), camera3_of(&cam));
        let Some(px) = facett_core::render::gpu::scene3d::render_scene3d_offscreen(w, h, &c3, &s3) else {
            eprintln!("SKIP: no GPU adapter (set FACETT_ALLOW_SOFTWARE_GPU=1 for a software one)");
            return;
        };
        let at = c3.project(behind, w as f32, h as f32).expect("in front of the eye").0;
        let luma = |x: u32, y: u32| {
            let i = ((y * w + x) * 4) as usize;
            (0.2126 * f32::from(px[i]) + 0.7152 * f32::from(px[i + 1]) + 0.0722 * f32::from(px[i + 2])) / 255.0
        };
        let (x, y) = (at[0] as u32, at[1] as u32);
        let peak = (y.saturating_sub(6)..y + 6).map(|yy| luma(x, yy)).fold(0.0f32, f32::max);
        assert!(peak > 0.3, "the spline behind the sheet shows through it (peak luma {peak} at {at:?})");
    }

    /// **Heads never overlap at the default zoom, and neither do their labels**; each
    /// head label stays inside its card and is big enough to read.
    #[test]
    fn heads_and_their_labels_do_not_overlap_at_the_default_zoom() {
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let sc = build_scene(&w, &refs, &[], true, Lit::default());
        let cam = near_cam();
        let heads: Vec<Rect> = sc.heads.iter().map(|h| screen_box(&cam, &h.corners)).collect();
        assert_eq!(heads.len(), 3);
        for (i, a) in heads.iter().enumerate() {
            for b in &heads[i + 1..] {
                assert!(!a.intersects(*b), "heads overlap on screen: {a:?} {b:?}");
            }
        }
        let head_labels: Vec<&Text> = sc.texts.iter().filter(|t| (t.right[0] - HEAD_EM).abs() < 1e-6 && cards.iter().any(|c| t.text.ends_with(&c.name))).collect();
        assert_eq!(head_labels.len(), 3, "one label per head: {:?}", head_labels.iter().map(|t| &t.text).collect::<Vec<_>>());
        for t in &head_labels {
            let c = cards.iter().find(|c| t.text.ends_with(&c.name)).unwrap();
            let len = t.text.chars().count() as f32 * ADV * HEAD_EM;
            assert!(t.origin[0] >= c.rect[0] && t.origin[0] + len <= c.rect[2], "the label {} fits its card", t.text);
        }
        let boxes: Vec<Rect> = head_labels.iter().map(|t| text_box(&cam, t)).collect();
        for (i, a) in boxes.iter().enumerate() {
            for b in &boxes[i + 1..] {
                assert!(!a.intersects(*b), "labels overlap: {a:?} {b:?}");
            }
            assert!(a.height() >= MIN_TEXT_PX, "a head reads at the default near zoom ({} px)", a.height());
        }
    }

    /// **Attributes hang under their head**: each row's text lies inside its card, below
    /// the head band, in the server's order; the call rows come after them.
    #[test]
    fn attributes_sit_under_their_head() {
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let sc = build_scene(&w, &refs, &[], true, Lit::default());
        let c = &cards[0];
        let rows: Vec<&Text> =
            sc.texts.iter().filter(|t| (t.right[0] - ROW_EM).abs() < 1e-6 && t.origin[0] > c.rect[0] && t.origin[0] < c.rect[2] && t.origin[2] < c.rect[3] + 0.5 && t.origin[2] > c.rect[1]).collect();
        let words: Vec<&str> = rows.iter().map(|t| t.text.as_str()).collect();
        assert_eq!(words, vec!["cart: &Cart", "-> u64", "let sum: u64", "→ total  :12,14", "→ send  :18"], "{words:?}");
        let head_bottom = c.rect[1] + w.geometry.head_h;
        let mut last = head_bottom;
        for t in &rows {
            assert!(t.origin[2] > head_bottom && t.origin[2] <= c.rect[3], "{} lies under the head, inside the card", t.text);
            assert!(t.origin[2] > last - 1e-4, "{} follows the row above it", t.text);
            last = t.origin[2];
        }
    }

    /// **Each spline starts on its call-site marker and ends on its callee's head**: the
    /// marker on the caller's right edge at its row, the end on the top edge of the
    /// callee's card (or where the server says it stands); it leaves sideways and rises.
    #[test]
    fn a_spline_runs_from_its_call_site_marker_to_the_callee_head() {
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let sc = build_scene(&w, &refs, &[], true, Lit::default());
        assert_eq!(sc.splines.len(), 2);
        let c = &cards[0];
        for (k, s) in c.sites.iter().enumerate() {
            let sp = sc.splines.iter().find(|x| x.to_card == s.to_card).unwrap();
            let m = marker_at(c, w.geometry, k);
            assert_eq!(sp.points[0], m, "starts on the marker");
            assert_eq!(m[0], c.rect[2], "the marker is on the card's right edge");
            let row_z = c.rect[1] + w.geometry.head_h + (site_row_index(c, k) as f32 + 0.5) * w.geometry.row_h;
            assert!((m[2] - row_z).abs() < 1e-5, "at its row");
            let end = *sp.points.last().unwrap();
            let want = match cards.iter().find(|x| x.card == s.to_card) {
                Some(t) => head_top(t.rect),
                None => [s.to_at[0], 0.06, s.to_at[1]],
            };
            for j in 0..3 {
                assert!((end[j] - want[j]).abs() < 1e-4, "ends on the callee's head: {end:?} {want:?}");
            }
            assert!(sp.points[1][0] > m[0], "leaves sideways");
            assert!(sp.points[sp.points.len() / 2][1] > 1.0, "rises out of the plane");
        }
        let dep = sc.splines.iter().find(|x| x.to_card == 3).unwrap();
        assert_eq!(dep.color, rel_color("dep", Color32::WHITE), "a dependency's spline is tinted as one");
        // a lit card lights its own splines and those into it, nothing else
        let lit = build_scene(&w, &refs, &[], true, Lit { card: Some(1) });
        assert!(lit.splines.iter().find(|x| x.to_card == 1).unwrap().lit);
        assert!(!lit.splines.iter().find(|x| x.to_card == 3).unwrap().lit);
    }

    /// Far away the map is crates and their bundled edges, no card.
    #[test]
    fn far_is_crates_and_bundled_edges() {
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let sc = build_scene(&w, &refs, &[], false, Lit::default());
        assert!(sc.heads.is_empty() && sc.splines.is_empty());
        assert_eq!(sc.plates.len(), 2);
        assert!(sc.lines.is_empty(), "far, an edge into a dependency (shop → stripe) is not drawn: every crate calls std");
        assert!(sc.texts.iter().any(|t| t.text.starts_with("shop · 3")));
        // two crates of the workspace: their bundled edge is a low arc between the plates
        let mut w2 = w.clone();
        w2.crates.push(Plate { id: "shop-core@0.1.0".into(), name: "shop-core".into(), repo: Some("shop".into()), local: true, rect: [8.0, 72.0, 60.0, 79.0], title: 5.0, cards: 2 });
        w2.edges = vec![(0, 2, 1, 7, 19)];
        let sc = build_scene(&w2, &refs, &[], false, Lit::default());
        assert_eq!(sc.lines.len(), 16, "one arc of 16 segments");
        assert_eq!(sc.lines[0].a, [34.0, 0.0, 45.0], "from the middle of shop");
        assert_eq!(sc.lines[15].b, [34.0, 0.0, 75.5], "to the middle of shop-core");
        let top = sc.lines[7].b[1];
        assert!(top > 0.5 && top < 0.2 * 30.5, "a low arc: {top}");
    }

    /// **Closer, a plate shows its modules**: each block named with its function count,
    /// the types it defines listed while the cards are not drawn; up close only the names.
    #[test]
    fn closer_a_plate_is_its_modules_and_their_types() {
        let (w, _) = model();
        let src = map_room();
        let mods = modules_of(&src.ask(S_CODE_MAP, &json!({ "lod": "modules", "crate": 0 }))).unwrap();
        let refs: Vec<&ModuleView> = mods.iter().collect();
        let mid = build_scene(&w, &[], &refs, false, Lit::default());
        let words: Vec<&str> = mid.texts.iter().map(|t| t.text.as_str()).collect();
        assert!(words.contains(&"shop · 2 fns") && words.contains(&"pay · 1 fn"), "{words:?}");
        assert!(words.contains(&"struct Charge · trait Gateway · +1"), "{words:?}");
        for t in mid.texts.iter().filter(|t| t.text.contains("Charge") || t.text.starts_with("pay")) {
            let r = mods[1].rect;
            assert!(t.origin[0] >= r[0] && t.origin[0] <= r[2] && t.origin[2] >= r[1] && t.origin[2] <= r[3], "{} lies on its block", t.text);
        }
        let near = build_scene(&w, &[], &refs, true, Lit::default());
        assert!(!near.texts.iter().any(|t| t.text.contains("Gateway")), "up close the cards speak, not the type list");
    }

    /// **By role** a card stands at its height — its head, its rows and its call
    /// markers lifted with it — and the splines run from a lifted marker to a lifted
    /// head; no plate is drawn, the tiles are tinted by their main crate.
    #[test]
    fn by_role_cards_stand_at_their_height_and_crates_are_a_tint() {
        let (mut w, mut cards) = model();
        for p in &mut w.crates {
            p.rect = [0.0; 4];
        }
        w.layout = "role".into();
        w.tints = vec![(0, 0, 0, 30)];
        cards[0].y = 6.0;
        cards[1].y = -3.0;
        for s in &mut cards[0].sites {
            if s.to_card == 1 {
                s.to_y = -3.0;
            }
        }
        let refs: Vec<&CardView> = cards.iter().collect();
        let sc = build_scene(&w, &refs, &[], true, Lit::default());
        assert!(sc.plates.is_empty(), "no plates by role");
        assert!(sc.quads.iter().any(|q| q.origin == [0.0, -0.1, 0.0] && q.u[0] == w.tile), "the tile's tint");
        let head0 = sc.heads.iter().find(|h| h.card == 0).unwrap();
        assert!(head0.corners.iter().all(|c| (c[1] - 6.0).abs() < 1e-5), "the head at its height");
        let label = sc.texts.iter().find(|t| t.text.ends_with("checkout")).unwrap();
        assert!((label.origin[1] - 6.08).abs() < 1e-4, "its name lifted with it");
        let to_total = sc.splines.iter().find(|x| x.to_card == 1).unwrap();
        assert!((to_total.points[0][1] - 6.06).abs() < 1e-4, "from the lifted marker");
        assert!((to_total.points.last().unwrap()[1] - (-2.94)).abs() < 1e-4, "onto the lifted head of total");
    }

    /// The wheel zooms to the cursor: the map point under it stays under it.
    #[test]
    fn zoom_keeps_the_point_under_the_cursor() {
        let mut cam = Camera { target: [50.0, 0.0, 40.0], dist: 300.0, yaw: 0.3, pitch: 0.9 };
        let at = Pos2::new(900.0, 300.0);
        let before = cam.ground(at, view()).unwrap();
        cam.zoom_at(0.5, at, view());
        assert!((cam.dist - 150.0).abs() < 1e-3);
        let after = cam.ground(at, view()).unwrap();
        assert!((before[0] - after[0]).abs() < 1e-2 && (before[2] - after[2]).abs() < 1e-2, "{before:?} {after:?}");
        let p = cam.project(after, view()).unwrap().0;
        assert!((p - at).length() < 0.1, "the projection is the ray's inverse");
    }

    #[test]
    fn a_line_is_tinted_by_what_it_is() {
        let t = tint_line("    let s = d.greet(); // static \"x\"");
        assert!(t.iter().any(|(_, w, _)| w == "let"));
        assert!(t.last().unwrap().1.starts_with("// static"));
        let s = tint_line(r#"format!("woof {}", 42)"#);
        assert!(s.iter().any(|(_, w, _)| w == "\"woof {}\""));
        assert!(s.iter().any(|(_, w, c)| w == "42" && *c != PLAIN));
        let line = "pub fn run(p: &Point) -> i32 {";
        let joined: String = tint_line(line).into_iter().map(|x| x.1).collect();
        assert_eq!(joined, line, "the pieces cover the line, in order");
    }

    #[test]
    fn the_tab_asks_the_world_then_the_tiles_it_sees_then_a_source_page_by_page() {
        let src = map_room();
        let mut t = CodeRoom3DTab::with_source(src.clone(), "w");
        t.wait();
        let s = t.state_json();
        assert_eq!((s["from_room"].as_bool(), s["crates"].as_u64(), s["repos"].as_u64()), (Some(true), Some(2), Some(2)), "{s}");
        assert_eq!(src.asked.lock().unwrap()[0], json!({ "lod": "world" }));
        assert!(s["camera"]["dist"].as_f64().unwrap() > CARDS_DIST as f64, "it opens on the whole map: {s}");
        t.update_json(r#"{"open_source": 0}"#);
        assert!(t.panels.is_empty(), "a card not held has no source to open");
        t.update_json(&json!({ "camera": { "target": [20.0, 0.0, 32.0], "dist": 40.0 } }).to_string());
        assert_eq!(t.tiles_in_view(view()), vec![(0, 0)]);
        t.ask(json!({ "lod": "cards", "tile": [0, 0] }));
        t.wait();
        t.update_json(r#"{"open_source": 0}"#);
        t.wait();
        let p = &t.state_json()["panels"][0];
        assert_eq!(p["first_line"], "pub fn checkout(cart: &Cart) -> u64 {", "{p}");
        assert_eq!((p["start_line"].as_u64(), p["pinned"].as_bool()), (Some(10), Some(true)));
        // scrolling towards the end of what is held asks the next page
        t.scroll_panel(0, 12.0);
        t.wait();
        assert!(src.asked.lock().unwrap().iter().any(|a| a["lod"] == "source" && a["from"] == 30), "{:?}", src.asked.lock().unwrap());
        assert_eq!(t.state_json()["panels"][0]["lines"], 40);
    }

    /// **Expanding a head** draws a spline from EVERY caller's head onto it — one held
    /// on a tile, one from another repo where the server says it stands (a ghost head).
    #[test]
    fn an_expanded_head_draws_every_caller_onto_it() {
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let src = map_room();
        let x = expanded_of(&src.ask(S_CODE_MAP, &json!({ "lod": "calls", "card": 1 }))).unwrap();
        let mut sc = build_scene(&w, &refs, &[], true, Lit::default());
        let before = sc.splines.len();
        expand_scene(&mut sc, &w, &x, &refs);
        let new: Vec<&Spline> = sc.splines[before..].iter().collect();
        assert_eq!(new.len(), 2);
        let total = head_top(cards[1].rect);
        for s in &new {
            assert_eq!(*s.points.last().unwrap(), total, "every caller's spline lands on total's head");
            assert!(s.lit);
        }
        assert_eq!(new[1].points[0], [44.0, 0.06, 60.0 + w.geometry.head_h * 0.5], "from the caller's head, where the server says it stands");
        assert!(sc.texts.iter().any(|t| t.text == "render"), "a caller not held is a ghost head");
    }

    /// **The robot picks two commits and sees the changed heads tinted.**
    /// The timeline lands, the pick lands the diff, the scene gains one tint
    /// band per head (the removed one a named ghost), the diff splines, and
    /// **The robot drives the replay**: open lists the runs, pick asks the
    /// window (from 0), play + a step glows the entered heads in `state_json`
    /// (their paths and threads said), a scrub asks the stacks of that moment,
    /// and by role the window asks carry the layout.
    #[test]
    fn the_robot_replays_a_traced_run() {
        let src = map_room();
        let mut t = CodeRoom3DTab::with_source(src.clone(), "w");
        t.wait();
        t.update_json(&json!({ "trace_open": "shop" }).to_string());
        t.wait();
        assert_eq!(t.trace.runs.len(), 1, "{:?}", t.trace.runs_err);
        t.update_json(&json!({ "trace_pick": "r-1" }).to_string());
        t.wait();
        assert!(t.trace.view.is_some(), "{:?}", t.trace.err);
        t.update_json(&json!({ "trace_play": { "speed": 1_000_000.0 } }).to_string());
        t.update_json(&json!({ "trace_step": 0.4 }).to_string());
        let s = t.state_json();
        let tr = &s["trace"];
        assert_eq!(tr["playing"], json!(true));
        assert_eq!(tr["t_ns"], json!(400_000));
        let glows: Vec<&str> = tr["glows"].as_array().unwrap().iter().map(|g| g["path"].as_str().unwrap()).collect();
        assert_eq!(glows, ["shop::checkout", "shop::total"], "{tr}");
        assert_eq!(tr["pulses"], json!(1));
        // the scrub asks a fresh window with the stacks of that moment
        t.update_json(&json!({ "trace_scrub": 200_000 }).to_string());
        t.wait();
        let asked = src.asked.lock().unwrap();
        let scrub_ask = asked.iter().rev().find(|p| p["lod"] == "trace" && p["stacks"] == json!(true)).expect("the scrub ask carries stacks: true");
        assert_eq!(scrub_ask["from_ns"], json!(200_000));
        drop(asked);
        assert_eq!(t.state_json()["trace"]["glows"].as_array().unwrap().len(), 1, "checkout alone is open at 200 µs");
        // by role, the window ask carries the layout (heads must match the shown map)
        t.update_json(&json!({ "layout": "role" }).to_string());
        t.update_json(&json!({ "trace_scrub": 0 }).to_string());
        t.wait();
        let asked = src.asked.lock().unwrap();
        let role_ask = asked.iter().rev().find(|p| p["lod"] == "trace").unwrap();
        assert_eq!(role_ask["layout"], json!("role"), "{role_ask}");
    }

    /// far out the plate badge; the before/after window reads both shas.
    #[test]
    fn the_robot_picks_two_commits_and_sees_the_changed_heads_tinted() {
        let src = map_room();
        let mut t = CodeRoom3DTab::with_source(src.clone(), "w");
        t.wait();
        t.update_json(&json!({ "diff_open": "shop" }).to_string());
        t.wait();
        assert_eq!(t.diff.commits.len(), 2, "the timeline landed");
        t.update_json(&json!({ "diff_pick": { "repo": "shop", "from": 1, "to": 2 } }).to_string());
        t.wait();
        let v = t.diff.view.clone().expect("the diff landed");
        assert_eq!(v.heads.len(), 3);

        // near: one tint band per head, the ghost named, the splines drawn
        let (w, cards) = model();
        let refs: Vec<&CardView> = cards.iter().collect();
        let mut sc = build_scene(&w, &refs, &[], true, Lit::default());
        let (q0, l0, t0) = (sc.quads.len(), sc.lines.len(), sc.texts.len());
        diff_scene(&mut sc, &w, &v, true);
        assert_eq!(sc.quads.len() - q0, 3, "one tint band per head");
        // (the changed head's band is asserted below: its alpha is the JUMP's)
        assert!(sc.quads[q0..].iter().any(|q| q.color == class_color("added").gamma_multiply(0.55)), "the added head is tinted green (no jump: the plain tint)");
        assert!(sc.texts[t0..].iter().any(|x| x.text.contains("dead")), "the removed head is a named ghost");
        assert!(sc.lines.len() - l0 > 3, "class pins and the diff splines");

        // far: the plate badge counts
        let mut far = build_scene(&w, &[], &[], false, Lit::default());
        let tf = far.texts.len();
        diff_scene(&mut far, &w, &v, false);
        assert!(far.texts[tf..].iter().any(|x| x.text == "+1 −1 ~1 Δ0.91"), "the plate badge reads from far out, the jump with it");

        // the MEANING JUMP glows: the changed head's band burns hotter than
        // the plain tint, its pin is taller, and the Δ number is drawn
        let changed_band = class_color("changed").gamma_multiply(0.25 + 0.6 * 0.91);
        assert!(sc.quads[q0..].iter().any(|q| q.color == changed_band), "the changed head glows by its jump");
        assert!(sc.texts[t0..].iter().any(|x| x.text == "Δ0.91"), "the jump number stands at the head");
        // the robot's view: the classes and jumps in state_json
        let s = t.state_json();
        assert_eq!(s["diff"]["heads"].as_array().map(Vec::len), Some(3), "{s}");
        assert_eq!(s["diff"]["from_sha"], "aaaa111122223333aaaa", "{s}");
        let total_head = s["diff"]["heads"].as_array().unwrap().iter().find(|h| h["name"] == "total").unwrap();
        assert!((total_head["jump"].as_f64().unwrap() - 0.91).abs() < 1e-4, "{total_head}");

        // the before/after of the changed head reads BOTH shas
        t.update_json(&json!({ "diff_hover": "m total()." }).to_string());
        t.wait();
        let s = t.state_json();
        assert_eq!(s["diff"]["before_after"]["head"], "total", "{s}");
        assert_eq!((s["diff"]["before_after"]["before_lines"].as_u64(), s["diff"]["before_after"]["after_lines"].as_u64()), (Some(2), Some(2)), "{s}");
        let asked = src.asked.lock().unwrap();
        assert!(asked.iter().any(|a| a["lod"] == "source_at" && a["sha"] == "aaaa111122223333aaaa"), "the before side was read at from_sha");
        assert!(asked.iter().any(|a| a["lod"] == "source_at" && a["sha"] == "bbbb111122223333bbbb"), "the after side was read at to_sha");
    }

    /// **The source never spins for ever** (owner 2026-10-17: "it says 'reading
    /// source' when I click a header"): a page that stays queued on jera says so on
    /// the sheet — the job and how long — and is asked again; past [`GIVE_UP_S`] the
    /// sheet says it got no answer and stops asking; a click on the head asks again.
    #[test]
    fn a_source_that_stays_queued_says_so_and_is_given_up_not_spun_for_ever() {
        let src = map_room();
        src.source_queued.store(true, std::sync::atomic::Ordering::SeqCst);
        let mut t = CodeRoom3DTab::with_source(src.clone(), "w");
        t.wait();
        t.update_json(&json!({ "camera": { "target": [20.0, 0.0, 32.0], "dist": 40.0 } }).to_string());
        t.ask(json!({ "lod": "cards", "tile": [0, 0] }));
        t.wait();
        t.now = 100.0;
        t.update_json(r#"{"open_source": 0}"#);
        t.wait();
        let state = |t: &CodeRoom3DTab| t.state_json()["panels"][0]["state"].as_str().unwrap_or("").to_string();
        assert!(state(&t).contains("queued on jera") && state(&t).contains("j-7"), "the sheet says the job it waits on: {}", t.state_json()["panels"]);
        let sources = |src: &MapRoom| src.asked.lock().unwrap().iter().filter(|a| a["lod"] == "source").count();
        let asked = sources(&src);
        // asked again after a short while
        t.now = 102.0;
        t.poll();
        t.wait();
        assert_eq!(sources(&src), asked + 1, "a queued page is asked again");
        // past the give-up time: said, and no longer asked
        t.now = 100.0 + GIVE_UP_S + 5.0;
        t.poll();
        t.wait();
        assert!(state(&t).contains("no answer after"), "{}", state(&t));
        let given_up = sources(&src);
        t.now += 10.0;
        t.poll();
        t.wait();
        assert_eq!(sources(&src), given_up, "given up: not asked again");
        // a click on the head asks again
        src.source_queued.store(false, std::sync::atomic::Ordering::SeqCst);
        t.update_json(r#"{"open_source": 0}"#);
        t.wait();
        assert_eq!(t.state_json()["panels"][0]["first_line"], "pub fn checkout(cart: &Cart) -> u64 {");
    }

    /// The toggle switches the layout: everything is asked again by role (a source
    /// page is the same in both), what was held of the crate layout is dropped.
    #[test]
    fn the_layout_toggle_asks_the_map_by_role() {
        let src = map_room();
        let mut t = CodeRoom3DTab::with_source(src.clone(), "w");
        t.wait();
        t.ask(json!({ "lod": "cards", "tile": [0, 0] }));
        t.wait();
        assert!(!t.tiles.is_empty());
        t.update_json(r#"{"layout":"role"}"#);
        assert!(t.tiles.is_empty(), "the crate layout's tiles are dropped");
        t.wait();
        t.ask(json!({ "lod": "cards", "tile": [0, 0] }));
        t.wait();
        t.ask(json!({ "lod": "source", "moniker": "m checkout()." }));
        t.wait();
        let asked = src.asked.lock().unwrap().clone();
        let n = asked.len();
        assert_eq!(asked[n - 3], json!({ "lod": "world", "layout": "role" }));
        assert_eq!(asked[n - 2], json!({ "lod": "cards", "tile": [0, 0], "layout": "role" }));
        assert_eq!(asked[n - 1], json!({ "lod": "source", "moniker": "m checkout()." }), "a source page is the same in both layouts");
        assert_eq!(t.state_json()["layout"], "role");
        t.update_json(r#"{"layout":"crate"}"#);
        t.wait();
        assert_eq!(src.asked.lock().unwrap().last().unwrap(), &json!({ "lod": "world" }));
    }

    #[test]
    fn a_map_that_does_not_answer_draws_its_state_and_no_map() {
        let mut t = CodeRoom3DTab::with_source(refusing_room(), "w");
        t.wait();
        let s = t.state_json();
        assert_eq!((s["from_room"].as_bool(), s["crates"].is_null()), (Some(false), true), "{s}");
        assert_eq!(s["answer"]["state"], "not_consumed", "{s}");
    }
}