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, ¶ms)) });
}
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 = ¶ms["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(¶ms, &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(¶ms, &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}");
}
}