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

nordisk/facett

facett-graphview/src/world_still.rs

//! **The world-graph STILL** — a converged, well-distributed layout rendered once,
//! headless, on the GPU into a single emissive image (the "mega tier": one nicely-spread
//! picture of the whole universe at 100k+). Reuses the shared instanced GPU cloud
//! ([`facett_core::render::gpu::render_graph_offscreen`], the ONE emissive node/edge
//! renderer with real bloom) — no second renderer — and the world graph's own galaxy
//! layout ([`crate::world::hier_placement`]/[`crate::world::layout_world`]) + categorical
//! colour ([`crate::world::node_color`]). Behind feature `gpu` (the wgpu path).
//!
//! The chip scene ([`crate::scene`]) is for labelled dep-graph boxes; a 100k node cloud
//! wants *points*, so the still lowers each node to a bloomed disc sized by its tier and
//! coloured by its repo/crate — a galaxy of coloured clusters, not grey turds.

use facett_core::render::gpu::{
    render_graph_offscreen, BloomParams, EdgeInstance, GraphRender, GraphScene, NodeInstance,
};

use crate::world::{crate_ordinals, node_color, tier_scale, WorldGraph};

/// Approx sRGB8 → linear, then a per-tier emissive gain so repos bloom brightest and the
/// hierarchy reads by brightness as well as hue/size. `gain` pushes radiance past 1.0 so
/// the bloom post-stack actually lights up.
fn emissive(c: crate::model::Color, gain: f32) -> [f32; 4] {
    let lin = |v: u8| {
        let s = v as f32 / 255.0;
        s.powf(2.2)
    };
    [lin(c.r) * gain, lin(c.g) * gain, lin(c.b) * gain, 1.0]
}

/// Fit `[-1,1]²` layout positions into a `w × h` canvas (physical px, y-down), preserving
/// aspect, with a fractional `margin`. Returns the projected centres.
fn project(pos: &[[f32; 2]], w: u32, h: u32, margin: f32) -> Vec<[f32; 2]> {
    let (fw, fh) = (w as f32, h as f32);
    let s = ((fw.min(fh)) * (1.0 - 2.0 * margin)) * 0.5; // [-1,1] → half-extent px
    let (cx, cy) = (fw * 0.5, fh * 0.5);
    pos.iter().map(|p| [cx + p[0] * s, cy - p[1] * s]).collect()
}

/// Build the emissive [`GraphScene`] for a laid-out world graph: one bloomed disc per
/// node (radius by tier, colour by repo/crate), plus optional dim edges. `dark` selects
/// the palette surface + background. `edge_cap` bounds the edge batch (0 = no edges) so a
/// 10M-edge world still renders a legible web instead of a muddy fill.
#[must_use]
pub fn world_scene(
    w: &WorldGraph,
    pos: &[[f32; 2]],
    width: u32,
    height: u32,
    dark: bool,
    edge_cap: usize,
) -> GraphScene {
    let proj = project(pos, width, height, 0.045);
    let ords = crate_ordinals(w);
    // Base disc radius scales gently down with node count so a dense world stays legible.
    let base_r = (2.6e6 / w.n.max(1) as f32).sqrt().clamp(0.7, 4.5);

    let nodes: Vec<NodeInstance> = (0..w.n)
        .map(|i| {
            let lvl = w.level[i];
            let col = node_color(lvl, w.repo_of[i] as usize, ords[i] as usize, dark);
            let gain = match lvl {
                crate::world::NodeLevel::Repo => 2.3,
                crate::world::NodeLevel::Crate => 1.7,
                crate::world::NodeLevel::Function => 1.25,
            };
            NodeInstance {
                center: proj[i],
                radius: (base_r * tier_scale(lvl)).max(0.6),
                _pad: 0.0,
                color: emissive(col, gain),
            }
        })
        .collect();

    let edges: Vec<EdgeInstance> = if edge_cap == 0 {
        Vec::new()
    } else {
        let stride = (w.edges.len() / edge_cap.max(1)).max(1);
        w.edges
            .iter()
            .step_by(stride)
            .filter_map(|&(a, b)| {
                let (a, b) = (a as usize, b as usize);
                if a >= w.n || b >= w.n {
                    return None;
                }
                // Edge hue: the source node's (dimmed) colour, thin, low radiance — a faint
                // structural web under the bloomed nodes.
                let col = node_color(w.level[a], w.repo_of[a] as usize, ords[a] as usize, dark);
                Some(EdgeInstance {
                    a: proj[a],
                    b: proj[b],
                    width: 0.55,
                    _pad: 0.0,
                    color: emissive(col, 0.28),
                })
            })
            .collect()
    };

    let background = if dark { [0.015, 0.017, 0.025, 1.0] } else { [0.96, 0.965, 0.975, 1.0] };
    GraphScene { background, nodes, edges }
}

/// **Render the world-graph still**: build the emissive scene and rasterise it headless
/// on the GPU with bloom. Returns the read-back frame (straight RGBA8 + draw counts), or
/// `None` when no usable GPU adapter is present (the caller then reports the honest
/// "needs a device" fallback). Reuses the shared cloud renderer — no new pipeline.
#[must_use]
pub fn render_world_still(
    w: &WorldGraph,
    pos: &[[f32; 2]],
    width: u32,
    height: u32,
    dark: bool,
    edge_cap: usize,
) -> Option<GraphRender> {
    let scene = world_scene(w, pos, width, height, dark, edge_cap);
    let bloom = BloomParams { intensity: 1.4, sigma: 2.6, ..BloomParams::default() };
    render_graph_offscreen(width, height, &scene, bloom)
}

// ── the 3D warehouse view ────────────────────────────────────────────────────
//
// The third view the owner asked for: the repo→crate→function hierarchy spread in **3D**
// and rendered on the GPU without the grey-blob look. It reuses the SAME instanced cloud
// renderer (facett-core graphcloud — the shared 3D cloud, per its own docs) and the world
// graph's categorical colour; the 3D is a nested-spherical galaxy + a CPU orthographic
// projection with depth cues (nearer = larger + brighter), exactly the approach
// `facett_graph3d` uses. No second renderer, no fabricated data.

const TAU: f32 = std::f32::consts::TAU;

// The camera + placement math (`project_azimuth`, `fib_sphere`, `galaxy_3d`)
// moved to the UNGATED `crate::camera3d` so CPU-side hosts share the one
// implementation instead of porting it past this module's `gpu` gate; re-exported
// here so every existing `world_still::*` consumer keeps compiling.
pub use crate::camera3d::{fib_sphere, galaxy_3d, project_azimuth};

/// Rotate a `[-1,1]³` point by `yaw` (about Y) then `pitch` (about X) and orthographically
/// project to a `w×h` canvas. Returns `(screen_xy, depth)` where depth in `[-1,1]` (+ =
/// nearer) drives the size/brightness cue. Camera math is [`project_azimuth`] (shared with
/// live host views); this only maps the world-space `[x, y]` into y-down canvas pixels.
fn project_3d(p: [f32; 3], yaw: f32, pitch: f32, w: u32, h: u32) -> ([f32; 2], f32) {
    let [x1, y2, z2] = project_azimuth(p, yaw, pitch);
    let s = (w.min(h) as f32) * 0.5 * 0.9;
    ([w as f32 * 0.5 + x1 * s, h as f32 * 0.5 - y2 * s], z2)
}

/// **Render the 3D warehouse still**: a nested-spherical galaxy projected with depth cues
/// and rasterised headless on the GPU with bloom (the shared cloud renderer). `yaw`/`pitch`
/// orbit the camera. `None` when no GPU adapter is present (honest fallback).
#[must_use]
#[allow(clippy::too_many_arguments)] // public signature hosts call: kept as is
pub fn render_world_still_3d(
    w: &WorldGraph,
    pos3: &[[f32; 3]],
    width: u32,
    height: u32,
    dark: bool,
    yaw: f32,
    pitch: f32,
    edge_cap: usize,
) -> Option<GraphRender> {
    let ords = crate_ordinals(w);
    let base_r = (2.6e6 / w.n.max(1) as f32).sqrt().clamp(0.7, 4.5);
    let proj: Vec<([f32; 2], f32)> =
        pos3.iter().map(|&p| project_3d(p, yaw, pitch, width, height)).collect();

    let nodes: Vec<NodeInstance> = (0..w.n)
        .map(|i| {
            let lvl = w.level[i];
            let (xy, depth) = proj[i];
            // Depth cue: nearer (+depth) → larger + brighter.
            let dcue = 0.6 + 0.4 * (depth * 0.5 + 0.5);
            let col = node_color(lvl, w.repo_of[i] as usize, ords[i] as usize, dark);
            let gain = dcue * match lvl {
                crate::world::NodeLevel::Repo => 2.4,
                crate::world::NodeLevel::Crate => 1.7,
                crate::world::NodeLevel::Function => 1.25,
            };
            NodeInstance {
                center: xy,
                radius: (base_r * tier_scale(lvl) * dcue).max(0.6),
                _pad: 0.0,
                color: emissive(col, gain),
            }
        })
        .collect();

    let edges: Vec<EdgeInstance> = if edge_cap == 0 {
        Vec::new()
    } else {
        let stride = (w.edges.len() / edge_cap.max(1)).max(1);
        w.edges
            .iter()
            .step_by(stride)
            .filter_map(|&(a, b)| {
                let (a, b) = (a as usize, b as usize);
                if a >= w.n || b >= w.n {
                    return None;
                }
                let col = node_color(w.level[a], w.repo_of[a] as usize, ords[a] as usize, dark);
                Some(EdgeInstance { a: proj[a].0, b: proj[b].0, width: 0.5, _pad: 0.0, color: emissive(col, 0.24) })
            })
            .collect()
    };

    let background = if dark { [0.012, 0.014, 0.022, 1.0] } else { [0.95, 0.955, 0.965, 1.0] };
    let scene = GraphScene { background, nodes, edges };
    let bloom = BloomParams { intensity: 1.4, sigma: 2.6, ..BloomParams::default() };
    let _ = TAU;
    render_graph_offscreen(width, height, &scene, bloom)
}

// ── the MEGA composite still: dense RASTER + crisp SVG TEXT burned in ─────────
//
// The owner's "SVG + JPEG2000" mega tier: one gigantic composite of the whole code-universe
// graph — a dense high-res raster of the node/edge cloud with a crisp *vector* text layer
// (repo labels at cluster centroids, a title, a colour↔repo legend, node/edge counts, a
// timestamp) burned OVER it into ONE image.
//
// FORMAT — an honest, PURE-RUST note. The shared offscreen cloud renderer clamps to wgpu
// downlevel limits (max texture 2048²), so the mega canvas is rendered in overlapping
// tiles and stitched ([`render_world_still_mega`]). The text layer is a real SVG
// ([`world_text_svg`]) rasterised + composited by resvg/usvg/tiny_skia — the same pure-Rust
// stack facett-docview uses (no pdfium/inkscape/native libs). The composite is saved as
// **PNG**, NOT JPEG2000: there is **no pure-Rust JPEG2000 encoder** on crates.io — every JP2
// path (openjpeg-sys, jpeg2k, …) binds the C OpenJPEG library, which the pure-Rust law here
// forbids. PNG is the lossless, pure-Rust, offline-cacheable high-quality raster; the crisp
// text also survives as the separate scalable `.svg`. We do NOT shell out to any C imaging
// tool.

/// Escape a string for inclusion in XML text / attribute content.
fn xml_escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 8);
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&apos;"),
            _ => out.push(c),
        }
    }
    out
}

/// Tuning for the [`world_text_svg`] crisp vector text layer.
#[derive(Clone, Debug)]
pub struct MegaLabels {
    /// The big title, top-left of the canvas.
    pub title: String,
    /// Dark surface (light text) vs light surface (dark text) — match the raster's `dark`.
    pub dark: bool,
    /// Node count (rendered into the meta line).
    pub node_count: usize,
    /// Edge count (rendered into the meta line).
    pub edge_count: usize,
    /// Whether the world hierarchy is synthetic (honest provenance in the meta line).
    pub synthetic: bool,
    /// A free-form timestamp string (e.g. an ISO date) for the meta line.
    pub timestamp: String,
    /// Cap on how many repo cluster labels are drawn (the largest-by-node-count repos win),
    /// so a 1M-node world with thousands of repos stays legible instead of a wall of text.
    pub max_repo_labels: usize,
}

impl Default for MegaLabels {
    fn default() -> Self {
        Self {
            title: "facett — the code-universe graph".into(),
            dark: true,
            node_count: 0,
            edge_count: 0,
            synthetic: true,
            timestamp: String::new(),
            max_repo_labels: 80,
        }
    }
}

/// One repo's aggregate for the label/legend layer: its name, colour slot, projected
/// cluster centroid (mega-canvas px) and node population.
struct RepoAgg {
    slot: usize,
    name: String,
    cx: f32,
    cy: f32,
    count: usize,
}

/// Aggregate per-repo centroid + population from the SAME projection the raster uses
/// (`project(.., 0.045)`), so labels land on their clusters.
fn repo_aggregates(w: &WorldGraph, pos: &[[f32; 2]], width: u32, height: u32) -> Vec<RepoAgg> {
    let proj = project(pos, width, height, 0.045);
    let rc = w.repo_count();
    let mut sx = vec![0.0f64; rc];
    let mut sy = vec![0.0f64; rc];
    let mut cnt = vec![0usize; rc];
    for (p, &ri) in proj.iter().zip(w.repo_of.iter()) {
        let r = ri as usize;
        if r < rc {
            sx[r] += p[0] as f64;
            sy[r] += p[1] as f64;
            cnt[r] += 1;
        }
    }
    (0..rc)
        .filter(|&r| cnt[r] > 0)
        .map(|r| RepoAgg {
            slot: r,
            name: w.repos.get(r).cloned().unwrap_or_else(|| format!("repo{r}")),
            cx: (sx[r] / cnt[r] as f64) as f32,
            cy: (sy[r] / cnt[r] as f64) as f32,
            count: cnt[r],
        })
        .collect()
}

/// **Build the crisp vector TEXT layer** as a standalone SVG string sized to the mega
/// canvas (`width × height` px, transparent background). Contains: a title + meta panel
/// (node/edge counts, timestamp, provenance, the honest raster-format note), a colour↔repo
/// legend (swatch + name for the largest repos), and a repo label anchored at each repo
/// cluster's centroid with a contrasting halo (`paint-order: stroke`) so it reads over the
/// bright node cloud. Pure string building — no font/GPU dependency — so it is always
/// available (the composite burn-in that rasterises it needs `gpu`). Also kept as the
/// separate `.svg` so the text stays scalable/searchable at any zoom.
#[must_use]
pub fn world_text_svg(
    w: &WorldGraph,
    pos: &[[f32; 2]],
    width: u32,
    height: u32,
    opts: &MegaLabels,
) -> String {
    let mut repos = repo_aggregates(w, pos, width, height);
    // Largest repos first — both the legend and (when capped) the drawn labels.
    repos.sort_by(|a, b| b.count.cmp(&a.count).then(a.slot.cmp(&b.slot)));

    let dark = opts.dark;
    let text_col = if dark { "#e9edf6" } else { "#12151c" };
    let halo_col = if dark { "#05060a" } else { "#f6f7fb" };
    let panel_fill = if dark { "rgba(8,10,16,0.72)" } else { "rgba(250,251,253,0.82)" };
    let panel_stroke = if dark { "rgba(150,165,200,0.35)" } else { "rgba(30,40,70,0.30)" };
    let muted = if dark { "#9fb0cc" } else { "#4a5568" };

    let fw = width as f32;
    // Font sizes scale with the canvas so text reads at any mega resolution.
    let title_fs = (fw / 30.0).round().max(14.0);
    let meta_fs = (fw / 78.0).round().max(9.0);
    let legend_fs = (fw / 82.0).round().max(9.0);
    let label_fs = (fw / 96.0).round().max(9.0);
    let pad = (fw / 90.0).round().max(8.0);

    let legend_n = repos.len().min(24);
    let sw = legend_fs * 1.15; // swatch size
    let line_h = legend_fs * 1.55;
    let title_h = title_fs * 1.2;
    let meta_h = meta_fs * 1.35 * 3.0; // three meta lines
    let panel_x = pad;
    let panel_y = pad;
    let panel_w = (fw * 0.34).max(title_fs * 12.0).min(fw - 2.0 * pad);
    let panel_h = title_h + meta_h + legend_fs * 1.4 + legend_n as f32 * line_h + pad * 2.5;

    let mut s = String::with_capacity(4096 + repos.len() * 160);
    s.push_str(&format!(
        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" \
         viewBox=\"0 0 {width} {height}\" font-family=\"DejaVu Sans, Liberation Sans, sans-serif\">\n"
    ));

    // Info panel backing.
    s.push_str(&format!(
        "<rect x=\"{:.1}\" y=\"{:.1}\" width=\"{:.1}\" height=\"{:.1}\" rx=\"{:.1}\" \
         fill=\"{panel_fill}\" stroke=\"{panel_stroke}\" stroke-width=\"1.5\"/>\n",
        panel_x, panel_y, panel_w, panel_h, meta_fs * 0.8
    ));
    let tx = panel_x + pad;
    let mut ty = panel_y + pad + title_fs;
    // Title.
    s.push_str(&format!(
        "<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{:.1}\" font-weight=\"700\" fill=\"{text_col}\">{}</text>\n",
        tx, ty, title_fs, xml_escape(&opts.title)
    ));
    // Meta lines.
    ty += title_fs * 0.35 + meta_fs * 1.5;
    let prov = if opts.synthetic { "synthetic hierarchy" } else { "live warehouse data" };
    for line in [
        format!("{} nodes · {} edges · {prov}", opts.node_count, opts.edge_count),
        format!("{} repos · raster: PNG (pure Rust — no pure-Rust JPEG2000 encoder)", w.repo_count()),
        if opts.timestamp.is_empty() {
            "text layer: crisp SVG (resvg/usvg/tiny_skia)".to_string()
        } else {
            format!("{} · text layer: crisp SVG", opts.timestamp)
        },
    ] {
        s.push_str(&format!(
            "<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{:.1}\" fill=\"{muted}\">{}</text>\n",
            tx, ty, meta_fs, xml_escape(&line)
        ));
        ty += meta_fs * 1.35;
    }
    // Legend heading + rows.
    ty += legend_fs * 0.8;
    s.push_str(&format!(
        "<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{:.1}\" font-weight=\"700\" fill=\"{text_col}\">legend — repo ↔ colour</text>\n",
        tx, ty, legend_fs
    ));
    ty += legend_fs * 0.8;
    for ra in repos.iter().take(legend_n) {
        let col = crate::world::repo_hue(ra.slot, dark);
        ty += line_h;
        s.push_str(&format!(
            "<rect x=\"{:.1}\" y=\"{:.1}\" width=\"{:.1}\" height=\"{:.1}\" rx=\"{:.1}\" fill=\"#{:02x}{:02x}{:02x}\"/>\n",
            tx, ty - sw * 0.85, sw, sw, sw * 0.2, col.r, col.g, col.b
        ));
        s.push_str(&format!(
            "<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{:.1}\" fill=\"{text_col}\">{} · {}</text>\n",
            tx + sw * 1.5, ty, legend_fs, xml_escape(&ra.name), ra.count
        ));
    }

    // Repo cluster labels at centroids (largest repos, capped), with a halo.
    for ra in repos.iter().take(opts.max_repo_labels) {
        s.push_str(&format!(
            "<text x=\"{:.1}\" y=\"{:.1}\" font-size=\"{:.1}\" font-weight=\"600\" \
             text-anchor=\"middle\" fill=\"{text_col}\" stroke=\"{halo_col}\" stroke-width=\"{:.2}\" \
             paint-order=\"stroke\" stroke-linejoin=\"round\">{}</text>\n",
            ra.cx, ra.cy, label_fs, label_fs * 0.18, xml_escape(&ra.name)
        ));
    }

    s.push_str("</svg>\n");
    s
}

/// A finished MEGA raster: the stitched straight-RGBA8 buffer at mega resolution plus the
/// tiling + draw counts (the render-proof asserts on these as DATA).
#[cfg(feature = "gpu")]
pub struct MegaRaster {
    pub width: u32,
    pub height: u32,
    /// Straight RGBA8, row-major (`width*height*4`).
    pub rgba: Vec<u8>,
    /// Scene node instances (the full projected cloud).
    pub nodes: usize,
    /// Scene edge instances.
    pub edges: usize,
    /// Number of GPU tiles stitched.
    pub tiles: u32,
}

/// Clone a scene, shift every instance by `(-ox, -oy)`, and CULL to the render region
/// `[0,rw]×[0,rh]` (plus a small margin) — so each tile only uploads the handful of
/// instances that actually touch it (a 1M cloud stays cheap per tile). The halo is what
/// keeps bloom continuous across tile seams: near-region instances outside the crop still
/// contribute their glow.
#[cfg(feature = "gpu")]
fn shift_and_cull(scene: &GraphScene, ox: f32, oy: f32, rw: f32, rh: f32) -> GraphScene {
    let m = 4.0f32; // margin beyond node radius already covered by the halo
    let inside = |x: f32, y: f32, r: f32| {
        x >= -(r + m) && x <= rw + r + m && y >= -(r + m) && y <= rh + r + m
    };
    let nodes: Vec<NodeInstance> = scene
        .nodes
        .iter()
        .filter_map(|n| {
            let c = [n.center[0] - ox, n.center[1] - oy];
            inside(c[0], c[1], n.radius).then_some(NodeInstance { center: c, ..*n })
        })
        .collect();
    let edges: Vec<EdgeInstance> = scene
        .edges
        .iter()
        .filter_map(|e| {
            let a = [e.a[0] - ox, e.a[1] - oy];
            let b = [e.b[0] - ox, e.b[1] - oy];
            // Keep if the segment's bbox overlaps the (margined) region.
            let (lo_x, hi_x) = (a[0].min(b[0]), a[0].max(b[0]));
            let (lo_y, hi_y) = (a[1].min(b[1]), a[1].max(b[1]));
            (hi_x >= -m && lo_x <= rw + m && hi_y >= -m && lo_y <= rh + m)
                .then_some(EdgeInstance { a, b, ..*e })
        })
        .collect();
    GraphScene { background: scene.background, nodes, edges }
}

/// **Render the MEGA world-graph raster** — the dense colored node/edge cloud at a
/// resolution far beyond the GPU's 2048² offscreen cap, by rendering overlapping tiles
/// and stitching them. `tile` is the interior tile edge and `halo` the per-side bleed
/// (both in px); `tile + 2*halo` MUST be ≤ 2048 (the downlevel max texture). Every mega
/// pixel is written by exactly one tile's cropped interior, so there are no gaps; the halo
/// keeps bloom seamless. Returns `None` when no GPU adapter is present (honest fallback).
#[cfg(feature = "gpu")]
#[must_use]
#[allow(clippy::too_many_arguments, reason = "explicit mega/tile/halo knobs read clearer than a struct here")]
pub fn render_world_still_mega(
    w: &WorldGraph,
    pos: &[[f32; 2]],
    mega_w: u32,
    mega_h: u32,
    dark: bool,
    edge_cap: usize,
    tile: u32,
    halo: u32,
) -> Option<MegaRaster> {
    assert!(tile + 2 * halo <= 2048, "tile+2*halo must fit the 2048 downlevel cap");
    let scene = world_scene(w, pos, mega_w, mega_h, dark, edge_cap);
    let bloom = BloomParams { intensity: 1.4, sigma: 2.6, ..BloomParams::default() };

    let mut out = vec![0u8; mega_w as usize * mega_h as usize * 4];
    let (nx, ny) = (mega_w.div_ceil(tile), mega_h.div_ceil(tile));
    let mut tiles = 0u32;
    for ty in 0..ny {
        for tx in 0..nx {
            let (x0, y0) = (tx * tile, ty * tile);
            let (tw, th) = ((mega_w - x0).min(tile), (mega_h - y0).min(tile));
            let (rw, rh) = (tw + 2 * halo, th + 2 * halo);
            let (ox, oy) = (x0 as f32 - halo as f32, y0 as f32 - halo as f32);
            let sub = shift_and_cull(&scene, ox, oy, rw as f32, rh as f32);
            let r = render_graph_offscreen(rw, rh, &sub, bloom)?;
            // Copy the tile's interior (offset by halo) into the mega buffer at (x0,y0).
            for row in 0..th {
                let src = (((row + halo) * rw + halo) * 4) as usize;
                let dst = (((y0 + row) * mega_w + x0) * 4) as usize;
                let n = (tw * 4) as usize;
                out[dst..dst + n].copy_from_slice(&r.rgba[src..src + n]);
            }
            tiles += 1;
        }
    }
    Some(MegaRaster {
        width: mega_w,
        height: mega_h,
        rgba: out,
        nodes: scene.nodes.len(),
        edges: scene.edges.len(),
        tiles,
    })
}

/// **Burn the crisp SVG text layer OVER an RGBA raster** (in place). Rasterises `svg` at
/// the raster's exact size with resvg/usvg/tiny_skia (pure Rust; a fontdb populated from
/// system fonts so `<text>` shapes) and alpha-composites the premultiplied result over the
/// opaque raster. Returns `false` if the SVG failed to parse or the pixmap could not be
/// allocated. `rgba` must be `width*height*4` straight RGBA8.
#[cfg(feature = "gpu")]
#[must_use]
pub fn burn_svg_over_rgba(rgba: &mut [u8], width: u32, height: u32, svg: &str) -> bool {
    use resvg::{tiny_skia, usvg};
    let mut db = usvg::fontdb::Database::new();
    db.load_system_fonts();
    if db.is_empty() {
        // Airgap safety: no system fonts → usvg would drop every <text>. Report honestly.
        eprintln!("burn_svg_over_rgba: no system fonts found — text layer would be empty");
    }
    let opts = usvg::Options { fontdb: std::sync::Arc::new(db), ..usvg::Options::default() };
    let tree = match usvg::Tree::from_str(svg, &opts) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("burn_svg_over_rgba: SVG parse failed: {e}");
            return false;
        }
    };
    let Some(mut pm) = tiny_skia::Pixmap::new(width, height) else { return false };
    resvg::render(&tree, tiny_skia::Transform::identity(), &mut pm.as_mut());
    // tiny_skia pixmaps are premultiplied sRGB; alpha-over onto the opaque raster:
    // out = src_premul + dst * (255 - a) / 255.
    for (d, sc) in rgba.as_chunks_mut::<4>().0.iter_mut().zip(pm.data().as_chunks::<4>().0) {
        let a = sc[3] as u32;
        if a == 0 {
            continue;
        }
        let inv = 255 - a;
        d[0] = (sc[0] as u32 + d[0] as u32 * inv / 255).min(255) as u8;
        d[1] = (sc[1] as u32 + d[1] as u32 * inv / 255).min(255) as u8;
        d[2] = (sc[2] as u32 + d[2] as u32 * inv / 255).min(255) as u8;
        d[3] = 255;
    }
    true
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::world::{layout_world, Lod, LayoutBudget};

    #[test]
    fn galaxy_3d_spreads_in_all_three_dims() {
        let w = WorldGraph::synthetic(4_000, Lod::Functions, false);
        let p = galaxy_3d(&w);
        assert_eq!(p.len(), w.n);
        // Real extent on every axis (a true 3D spread, not a flat sheet or a point).
        for d in 0..3 {
            let lo = p.iter().map(|q| q[d]).fold(f32::MAX, f32::min);
            let hi = p.iter().map(|q| q[d]).fold(f32::MIN, f32::max);
            assert!(hi - lo > 1.0, "axis {d} spreads (extent {:.3})", hi - lo);
        }
        // Distinct positions (no stacking).
        let mut seen = std::collections::BTreeSet::new();
        for q in &p {
            seen.insert((q[0].to_bits(), q[1].to_bits(), q[2].to_bits()));
        }
        assert!(seen.len() as f32 > 0.99 * w.n as f32, "3D positions are distinct");
    }

    #[test]
    fn project_azimuth_spins_about_the_vertical_axis_without_touching_the_layout() {
        // A galaxy laid out ONCE (the cached layout the live spin must never rebuild).
        let w = WorldGraph::synthetic(2_000, Lod::Functions, false);
        let pos3 = galaxy_3d(&w);
        let snapshot = pos3.clone();

        // Azimuth about Y leaves each point's HEIGHT (y) invariant — it is the vertical spin
        // axis — while the projected x/z sweep. Pitch = 0 so world-y == layout-y exactly.
        for &p in pos3.iter().take(64) {
            let a = project_azimuth(p, 0.0, 0.0);
            let b = project_azimuth(p, 0.9, 0.0);
            assert!((a[1] - p[1]).abs() < 1e-5, "yaw preserves the vertical axis");
            assert!((b[1] - p[1]).abs() < 1e-5, "yaw preserves the vertical axis");
            // The camera actually turned: the flattened x moved (unless the point is on-axis).
            if p[0].hypot(p[2]) > 1e-3 {
                assert!((a[0] - b[0]).abs() > 1e-4, "advancing yaw sweeps the projection");
            }
        }
        // Rotating the CAMERA never mutated the (cached) LAYOUT.
        assert_eq!(pos3, snapshot, "projection is read-only over the galaxy_3d layout");
    }

    #[test]
    fn text_svg_carries_repo_labels_legend_and_counts() {
        let w = WorldGraph::synthetic(4_000, Lod::Functions, true);
        let (pos, _) = layout_world(&w, &LayoutBudget::default());
        let opts = MegaLabels {
            title: "facett — code universe".into(),
            node_count: w.n,
            edge_count: w.edges.len(),
            synthetic: w.synthetic_flag(),
            timestamp: "2026-07-24".into(),
            ..MegaLabels::default()
        };
        let svg = world_text_svg(&w, &pos, 2000, 2000, &opts);
        // Well-formed, sized, transparent overlay.
        assert!(svg.starts_with("<svg"), "is an SVG");
        assert!(svg.contains("viewBox=\"0 0 2000 2000\""), "sized to the mega canvas");
        // The crisp TEXT: title, the synthetic repo names, legend + honest format note.
        assert!(svg.contains("facett — code universe"));
        assert!(svg.contains(">repo0<"), "repo0 label present");
        assert!(svg.contains(">repo1<") || svg.contains("repo1 ·"), "repo1 present (label/legend)");
        assert!(svg.contains("legend — repo ↔ colour"));
        assert!(svg.contains(&format!("{} nodes", w.n)));
        assert!(svg.contains("no pure-Rust JPEG2000 encoder"), "honest format note burned in");
        // A real label per repo cluster (capped) — at least a handful of <text> runs.
        assert!(svg.matches("<text").count() > 8, "many text runs: {}", svg.matches("<text").count());
    }

    #[test]
    fn scene_lowers_every_node_and_colours_by_tier() {
        let w = WorldGraph::synthetic(4_000, Lod::Functions, true);
        let (pos, _) = layout_world(&w, &LayoutBudget::default());
        let scene = world_scene(&w, &pos, 1024, 1024, true, 20_000);
        assert_eq!(scene.nodes.len(), w.n, "every node lowered to a disc");
        // Distinct fills across tiers/repos — NOT one grey (the grey-blob red-when-broken).
        let mut set = std::collections::BTreeSet::new();
        for nd in &scene.nodes {
            let key = (
                (nd.color[0] * 255.0) as u32,
                (nd.color[1] * 255.0) as u32,
                (nd.color[2] * 255.0) as u32,
            );
            set.insert(key);
        }
        assert!(set.len() > 8, "world is multi-coloured (>{} distinct fills), not grey blobs", set.len());
        // Nodes are spread — projected centres occupy a real 2-D extent, not one point.
        let xs: Vec<f32> = scene.nodes.iter().map(|n| n.center[0]).collect();
        let ys: Vec<f32> = scene.nodes.iter().map(|n| n.center[1]).collect();
        let sx = xs.iter().cloned().fold(f32::MAX, f32::min)..xs.iter().cloned().fold(f32::MIN, f32::max);
        let sy = ys.iter().cloned().fold(f32::MAX, f32::min)..ys.iter().cloned().fold(f32::MIN, f32::max);
        assert!((sx.end - sx.start) > 400.0 && (sy.end - sy.start) > 400.0, "nodes fill the canvas");
    }
}