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