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

nordisk/facett

facet-wrapped/src/generated.rs

//! **The generator seam** — `{"generate": {"source": "sbm", "size": 100000, "seed": 1}}`
//! (lane F, 2026-10-02; owner: the demo's size + generator in the Streamlit facet demo).
//!
//! A host (the Streamlit gallery's picker, a `?gen=sbm:100000:1` link) names a generator;
//! the graph is generated HERE, in the wasm, by `facett-graphgen` — the same generators,
//! the same lowerings and the same per-component builders as facett-demo's graph tabs —
//! so `sbm:100000:1` is the same graph in both demos and 100 000 nodes never cross the
//! host boundary as JSON.
//!
//! Every generated component is wrapped in [`GenTab`]: the demo's size + generator row
//! sits above it (so the canvas can re-generate by itself), and above
//! [`facett_graphgen::COMPACT_ABOVE`] (5 000 nodes) `facet_state` publishes the
//! component's SUMMARY (counts + lane readouts) instead of 100 000 node objects — the
//! measured 0.6–12 s full-state cost at 100k would otherwise set the frame rate.
//!
//! Two entries are new here, both the demo's tabs:
//!
//! * `universe` — the whole graph as one star-field on the resident GPU cloud, 100k by
//!   default, up to 1M ([`facett_graphgen::universe::UniverseTab`]);
//! * `decograph` — the demo's `decograph` tab: the `DecoratedGraphView` (the component
//!   `graphview` mounts) WITH its size + generator row, opening on its own 4-node DAG.
//!   A plain graph payload goes through `graphview`'s adapter.

use facett_core::Facet;
use facett_graphgen::{BuildFn, GenControl, GenRequest, GenSummary, GenTab, GENERATED_SOURCES};
use facett_graphgen::{build, universe::UniverseTab};

use crate::registry::{Entry, GRAPH, Registry};

/// The ceiling of the components on the resident GPU lane / GPU layout.
pub const RESIDENT: usize = build::RESIDENT_SCALE;
/// The universe's ceiling (the resident lane's tested reach).
pub const UNIVERSE: usize = facett_graphgen::universe::MAX_NODES;

/// Register the two generator-first entries.
pub(crate) fn register(r: &mut Registry) {
    r.register(
        Entry::new(
            "universe",
            "The whole graph as ONE GPU star-field: 100k nodes by default, up to 1M, on the resident cloud (no CPU painter).",
            || Box::new(UniverseTab::new()),
        )
        .in_group(GRAPH)
        .with_generator(UNIVERSE, &UNIVERSE_SOURCES, universe),
    );
    r.register(
        Entry::new(
            "decograph",
            "The decorated pan/zoom graph (graphview's component) with the size + generator row: 100 → 100k on the resident GPU lane.",
            || {
                let mut v = facett_graphview::DecoratedGraphView::local().with_gpu(true);
                v.set_title("decograph");
                Box::new(GenTab::new("decograph", v, GenControl::new(RESIDENT), build::decograph))
            },
        )
        .in_group(GRAPH)
        .with_data(crate::graph::graphview)
        .with_generator(RESIDENT, &GENERATED_SOURCES, decograph),
    );
}

/// The universe's sources, in its picker order (crates — the galaxy — first).
static UNIVERSE_SOURCES: [facett_graphgen::Source; 8] = {
    use facett_graphgen::{GenKind, Source};
    [
        Source::Gen(GenKind::Crates),
        Source::Population,
        Source::Gen(GenKind::Community),
        Source::Gen(GenKind::Sbm),
        Source::Gen(GenKind::Random),
        Source::Gen(GenKind::ScaleFree),
        Source::Gen(GenKind::Grid),
        Source::Gen(GenKind::Tree),
    ]
};

fn tab<C: Facet + GenSummary + 'static>(name: &'static str, req: GenRequest, max: usize, b: BuildFn<C>) -> Option<Box<dyn Facet>> {
    Some(Box::new(GenTab::generated(name, req, max, &GENERATED_SOURCES, b)?))
}

pub(crate) fn universe(req: GenRequest) -> Option<Box<dyn Facet>> {
    Some(Box::new(UniverseTab::from_request(req)?))
}
pub(crate) fn decograph(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("decograph", req, RESIDENT, build::decograph)
}
pub(crate) fn graphview(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("graphview", req, RESIDENT, build::decograph)
}
pub(crate) fn graphpan(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("graphpan", req, RESIDENT, build::graphpan)
}
pub(crate) fn graphnav(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("graphnav", req, RESIDENT, build::graphnav)
}
pub(crate) fn graph3d(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("graph3d", req, RESIDENT, build::graph3d)
}
pub(crate) fn graph2d(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("graph2d", req, RESIDENT, build::graph)
}
pub(crate) fn lineage(req: GenRequest) -> Option<Box<dyn Facet>> {
    tab("lineage", req, RESIDENT, build::lineage)
}

// The core registry only: a group-only bundle (docs, model3d, sim) registers no graph entry.
#[allow(unexpected_cfgs)]
#[cfg(all(test, not(feature = "group-only")))]
mod tests {
    use crate::registry::{EmbedError, Registry};
    use serde_json::json;

    const GENERATED: [&str; 8] = ["universe", "decograph", "graphview", "graphpan", "graphnav", "graph3d", "graph2d", "lineage"];

    /// Every generated entry builds from a request, holds exactly the requested graph
    /// (counts from the generator itself), and advertises its ceiling in the manifest.
    #[test]
    fn every_generated_entry_builds_from_a_request_and_advertises_its_ceiling() {
        let r = Registry::with_builtins();
        let m = r.manifest_json();
        for name in GENERATED {
            let f = r
                .build_with(name, &json!({ "generate": { "source": "sbm", "size": 120, "seed": 3 } }))
                .unwrap_or_else(|e| panic!("{name}: {e}"));
            let s = f.state_json();
            let nodes = s["gen"]["nodes"].as_u64().unwrap_or_else(|| panic!("{name} publishes gen.nodes: {s}"));
            assert_eq!(nodes, 120, "{name}");
            let e = m.as_array().unwrap().iter().find(|e| e["name"] == name).unwrap();
            assert!(e["generate"]["max"].as_u64().unwrap() >= 1_000, "{name}: {e}");
            assert!(e["generate"]["sources"].as_array().unwrap().iter().any(|s| s["key"] == "sbm"), "{name}");
            assert!(!e["generate"]["sources"].as_array().unwrap().iter().any(|s| s["key"] == "demo"), "{name}: demo data is not a generator");
        }
        // ...and the deep-link string is the same request.
        let a = r.build_with("graphpan", &json!({ "generate": "sbm:500:3" })).unwrap().state_json();
        let b = r.build_with("graphpan", &json!({ "generate": { "source": "sbm", "size": 500, "seed": 3 } })).unwrap().state_json();
        // (`gen` carries the build's wall-clock ms, so the MODEL is compared.)
        let strip = |mut v: serde_json::Value| {
            v.as_object_mut().unwrap().remove("gen");
            v
        };
        assert_eq!(strip(a), strip(b));
        // Components without the seam say so in the manifest.
        let table = m.as_array().unwrap().iter().find(|e| e["name"] == "table").unwrap();
        assert!(table["generate"].is_null());
    }

    /// A request is refused BY NAME — wrong component, unknown source, a source the
    /// component does not offer — and a size above the ceiling is clamped, not refused.
    #[test]
    fn bad_requests_are_refused_by_name_and_sizes_clamp() {
        let r = Registry::with_builtins();
        let err = |n: &str, v: serde_json::Value| r.build_with(n, &v).err().expect("refused");
        assert_eq!(err("table", json!({ "generate": "sbm" })), EmbedError::NotGenerated("table"));
        let e = err("graphpan", json!({ "generate": { "source": "nope" } })).to_string();
        assert!(e.contains("`nope`") && e.contains("known:"), "{e}");
        let e = err("graphpan", json!({ "generate": { "source": "demo" } })).to_string();
        assert!(e.contains("not offered"), "{e}");
        let g = r.build_with("graph2d", &json!({ "generate": { "source": "grid", "size": 5_000_000 } })).unwrap();
        assert_eq!(g.state_json()["gen"]["size"], super::RESIDENT as u64, "clamped to graph2d's ceiling");
    }

    /// Above 5 000 nodes the published state is the SUMMARY (no per-node array), and the
    /// universe opens on the galaxy at the request's size.
    #[test]
    fn a_big_request_publishes_the_summary() {
        let r = Registry::with_builtins();
        let s = r.build_with("decograph", &json!({ "generate": { "source": "grid", "size": 10_000 } })).unwrap().state_json();
        assert_eq!(s["gen"]["compact"], true);
        assert_eq!(s["node_count"], 10_000);
        assert!(s.get("nodes").is_none());
        let u = r.build_with("universe", &json!({ "generate": { "source": "crates", "size": 10_000 } })).unwrap().state_json();
        assert_eq!(u["placement"], "galaxy");
        assert!(u["nodes"].as_u64().unwrap().abs_diff(10_000) <= 1_000, "{}", u["nodes"]);
        // The universe draws only generated data: a plain graph payload is not its input.
        assert!(r.build_with("universe", &json!({ "nodes": [{ "id": 1 }] })).is_err());
    }
}