The graph subsystem — design
Class:
design· Function axis:f0(the whole) ·f1GraphView/DepGraphView ·f2graphview engine ·f3graphpan ·f4graph3d ·f5syschart. Strict: this file states only the shape that is built. Every why, every alternative, lives ingraph-reasoning.md.
f0 — the whole: five node/edge surfaces, one shared shape
Every facett component that draws nodes joined by edges — the 2D scene viewer, the scalable render engine, the pan/zoom canvas, the 3D viewer, and the system map — is built from the same three-part shape: a domain-agnostic model (nodes + edges, laid out by the caller), a camera/transform (pan, zoom, or orbit), and a Facet that paints the model under the transform and returns Msgs (FC-2/FC-9, see elm-design.md). None of the five crates learns what a node means — a "release gate", a "kommun", a "PKI service" are all just an id, a label, a colour, and a position.
| crate | f |
shape | model type | camera |
facett-graph |
f1 |
2D scene / layered DAG | Scene (core) / DepGraphState |
none (static) / none |
facett-graphview |
f2 |
scalable render engine, CPU/GPU | GraphModel + Decorations |
Camera (pan+zoom) |
facett-graphpan |
f3 |
pan/zoom canvas (borrowed) | GraphModel + Decorations |
GraphView (pan+zoom+select) |
facett-graph3d |
f4 |
3D node cloud | Node3 cloud + edges |
Graph3DView (orbit yaw+spin) |
facett-syschart |
f5 |
free-positioned peer map | SysChartModel |
none (normalized 0..1 positions) |
Reuse graph (uses): f1/f3/f5 sit directly on facett_core::{Elm, Facet, theme, a11y} (facett-graph/src/lib.rs:28, facett-graphpan/src/lib.rs, facett-syschart/src/lib.rs:26-28). f2 additionally sits on facett_core::render (the shared L0 SDF kernel: Canvas, CpuRenderer, QuadInstance/LineInstance, facett-graphview/src/l0.rs:23-26) and on vello/vello_cpu for its own reference rasterizer (facett-graphview/src/cpu.rs:11-13). f4 sits on facett_core::render::decor (the neon/glow/particle kernel, facett-graph3d/src/lib.rs:35) and, under the wgpu feature, on facett_core::render::gpu::{OffscreenColorDepth, GaussianBlur} (facett-graph3d/src/graph_gpu.rs:16-27). DepGraphView and DepGraphLayout delegate layered placement to facett_core::layered, retaining their existing edge conventions and public APIs. Pipeline, plandag and Brokkr additionally share persistent facett_flow::FlowScene rendering and core navigation gestures.
The shared decorations contract. f2 and f3 both define an identical caller-supplied overlay type — Decorations { nodes: HashMap<id, NodeDecoration>, edges: Vec<GraphEdge> }, where NodeDecoration carries an optional ring colour, an optional badge, and an optional scale multiplier (facett-graphview/src/model.rs:119-144, mirrored in facett-graphpan/src/lib.rs). The engine never learns what a ring or badge means — the host draws meaning on top of opaque shapes. f4's DecorPolicy and f1/f5 have no decorations layer; they carry status via plain node colour/border/badge fields on the model itself.
The shared backend-select policy. f2's runtime GPU/CPU choice (facett-graphview/src/backend.rs) is a direct re-export of facett_core::render::backend::{decide, Backend, GpuProbe} — the same policy the map skins use (facett-graphview/src/backend.rs:19). f4's GPU lane (graph_gpu.rs) is feature-gated independently (wgpu) and does not go through decide; it is a headless-offscreen-only lane today (see f4).
f1 — GraphView / DepGraphView (facett-graph)
Two Facets in one crate, both on facett_core::draw/Scene/Layout (the original facett-core scene-graph primitives), not on facett-graphview's GraphModel:
- [
scene_from_labeled_edges] (facett-graph/src/lib.rs:41-53) builds aScenefrom(src_id, dst_id, src_label, dst_label)rows: distinct ids become nodes, coloured byhash_color(&label). GraphView(facett-graph/src/lib.rs:114-124) — aScene+ aGraphLayout(Circular|Force, a serde-mirror of core'sLayout,facett-graph/src/lib.rs:61-85). Its ElmModelisGraphState { layout }(:93-96) — theSceneitself is input, held on the struct outside the Model becauseColor32doesn't round-trip through serde.Msg::SetLayoutis the only mutation (:102-105);Effectis uninhabited (:110, no I/O).viewpaints via the coredrawfn and returns noMsgs — the 2D skin's layout is host-driven, not click-driven (:178-189).DepGraphView(facett-graph/src/depgraph.rs:83-89) — a layered DAG: nodes toposorted into columns (deps left, dependents right,DepGraphView::columns,:142-165, longest-path layering), drawn as boxes with a status-coloured border + arrows carryingvialabels. Its Model isDepGraphState { selected: Option<String> }, keyed on the node's stable label (FC-5,:60-63) so a reorder never re-points the selection.Msg::SelectNodetoggles (re-selecting the open node clears it,:131-134); clicking a node emits theMsgfrom the pureview(:250-256), which also paints a drill-down panel of the selected node's deps/dependents (:258-290).
Both facets are wired through impl_facet_via_elm! form 3 (custom_state_json, facett-graph/src/lib.rs:249-266, facett-graph/src/depgraph.rs:371-396): the state_json a driver reads (node/edge counts, DepGraphView's column depth) is derived from the input data, not a plain serde dump of the Model, so the macro's richer form supplies it verbatim.
f2 — the graphview render engine (facett-graphview)
The scalable engine: a domain-agnostic model + a runtime-selected raster backend, aimed at the eventual single home for every graph surface in the constellation. Nine sibling modules, one entry point:
GraphModel + Decorations + Camera → decide(GpuProbe) → Backend → render_to_rgba() → RGBA8
- The model (
facett-graphview/src/model.rs) —GraphNode { id, label, fill, stroke, pos }+GraphEdge { from, to, color, dashed, label }inGraphModel(:74-99);Coloris a POD sRGBA8 with aFrom<egui::Color32>conversion (:16-52) so a caller already speaking egui maps across with zero glue.Camera::fit(:165-174) computes pan+zoom to fitmodel.world_bounds()into aw×hviewport with a margin, in a single call. - Backend select (
facett-graphview/src/backend.rs:19) —decide/Backend{Wgpu, VelloCpu}/GpuProbeare re-exports offacett_core::render::backend;probe_from_env(:27-31) additionally honours the legacyFACETT_GRAPHVIEW_CPUenv var alongside core'sFACETT_RENDER_CPU, so old hosts keep forcing CPU. render_to_rgba(facett-graphview/src/lib.rs:87-119) dispatches onBackend. Both arms currently rasterize throughcpu::render(facett-graphview/src/cpu.rs:31): theWgpuarm additionally builds avello::Sceneviagpu::build_scene(facett-graphview/src/gpu.rs:47-64, gated on featuregpu) to prove the version-aligned seam (vello0.9 ↔wgpu29 ↔egui-wgpu0.34) compiles, then falls through to the CPU rasterizer for the actual pixels — one pixel contract either way.- CPU rasterizer (
facett-graphview/src/cpu.rs) —vello_cpu(multithreaded SIMD, ships transitively inside epaint 0.34): rounded-rect node chips with a ring/kind-stroke border (:66-87), PCB-style cubic edges with an arrowhead and a sampled-dash approximation fordashededges (draw_edge_styled,:132-198). DecoratedGraphView(facett-graphview/src/decorated_view.rs:103-107) — the interactive egui twin: an owningFacetoverGraphViewState { model, decorations, selected, pan_x, pan_y, zoom }(:74-87, the Elm Model).MsgcoversSelect/PanBy/ZoomBy/Fit(:50-59);downstream_of(:25-44, forward-edge BFS from a seed) computes the lit subtree a selection highlights, dimming everything else tolinear_multiply(0.22)(:331).DepGraphLayout(facett-graphview/src/depgraph_layout.rs:86-92) — a pure, egui-free re-home of nornir's dep-graph layout: takes genericDepEdge{from, to, via}, supports direct vs deep (full transitive-closure) display modes (EdgeClass::{Direct, Transitive},:42-58) and per-node collapse (hides a node's exclusive subtree,:103-209). It retains its separate model and traversal semantics, while layered placement delegates to the same core implementation asDepGraphView.- L0 kernel adoption (
facett-graphview/src/l0.rs) — an opt-in alternative render path:lower()(:46-108) turns aGraphModel/Decorationsinto the sharedfacett_core::renderSDF vocabulary (QuadInstancerounded-square markers,LineInstancestraight capsules) andrender_to_rgba_l0(:115-149) hands them to a coreRenderer. This is a different geometry from the reference path (SDF circles/straight lines vs. rounded-rects/cubics) — it proves the routing, not pixel parity withcpu::render. - GPU-compute layout (
facett-graphview/src/gpu_layout.rs) —GraphCsr(:44-108, compressed-sparse-row adjacency + per-node weight, streamable from Arrow viafrom_arrow) + a Fruchterman-Reingold force model (repulsion/attraction/gravity,step_cpu/relax_cpu,:218-279) that is a pure, deterministic function of seed+params+steps. A wgpu compute-shader twin (GpuLayout, featuregpu,:306-457) iterates positions in VRAM (ping-pong buffers, no per-step readback), mirroring the CPU path line-for-line. - Force-directed edge bundling (
facett-graphview/src/fdeb.rs) —bundle()(:92-160) subdivides each edge into a flexible polyline and lets angle/scale/position-compatible edges (compatibility,:70-85) attract each other's control points, collapsing parallel edges into shared trunks.occupancy()(:175-191) is the grid-cell ink-coverage metric that proves the de-hairballing. - Topological semantic zoom (
facett-graphview/src/community.rs) —louvain()(:36-106, local-moving modularity ascent, deterministic) partitions the graph into communities;meta_graph()(:166-196) collapses each into oneMetaNode(centroid + member list) with aggregated crossing edges;fracture_t()(:204-208) maps a zoom level through an injected easing function to a[0,1]open-ness, andfractured_positions()(:214-225) lerps each member between its meta-centroid and its real position. - Metro/subway view (
facett-graphview/src/metro.rs) —MetroMap/MetroLine/MetroStationrender a test-coverage chain as a transit line: a station is lit when its emitter fired, a line is green only when every gating station (StationKind::{Start,Emitter,Grpc,Terminus}, non-gatingPassThrough) is lit (facett-graphview/src/metro.rs:1-20,MetroLine::is_green:228-233).MetroViewis a canonicalFacetwrapping it. - Clip discipline (
facett-graphview/src/clip.rs) —render_graph_clippedrenders aGraphModelinto arect ⊆ canvasthrough the pipelinelayout → cull → nodes → edges → picking → composite, returning aGraphClipReportfor every observable stage and instrumenting only the pixel-onlycompositestage (ink_outside_rect == 0proves nothing painted outside the widget rect).
f3 — graphpan (facett-graphpan)
The minimal counterpart to f2: a free function, not an owning engine. Same GraphModel/GraphNode/GraphEdge/Decorations/NodeDecoration shapes (re-declared, not re-exported, facett-graphpan/src/lib.rs:65-125), but the transform (GraphView { pan: Vec2, zoom: f32, selected: Option<String> }, :141-147) is caller-owned and the paint is a borrowed call the host makes every frame:
draw_graph(:244-275) — paintsmodelontoui, drives pan/drag + scroll-zoom + click-to-select, mutates the caller'sGraphViewin place, and returns aGraphResponse { clicked_node, clicked_empty }(:232-237) so the host can run its own side effects (click-to-code, scope a release) without the crate knowing what those are.content_extent(:282-296) computes the model's TRUE painted world-space bounding box (chips + margin);draw_graph_scrolled(:308+) uses it to allocate a canvas at its real size inside anegui::ScrollArea, so the scrollbar handle scales to the actual overflow instead of the viewport — for a static wiring board rather than a draggable pan surface.GraphPanView(:614-628) — the owningFacetwrapper: holds its ownmodel+decorations(input, outside the Model) and aGraphView(the Elm Model).Msg(:584-600) coversPanBy/SetPan/ZoomBy/SetZoom/Select/ClearSelection/Fit;update(:688-702) is the sole mutation path.last_origin(:622-627) is an interior-mutable (Cell) paint cache the pureviewrecords through&self— explicitly outside the Model (FC-7).downstream_of(:200-219) is graphpan's own copy of the forward-edge BFSf2'sdecorated_viewalso defines — same algorithm, independent crates (no shared dependency betweenf2andf3).
f2's DecoratedGraphView and f3's GraphPanView are two independently-built Facets over near-identical data: f2 is part of the consolidated render-engine crate (CPU/GPU rasterizer + this egui pane); f3 is a standalone, dependency-light crate exposing the bare draw_graph function a host can call without pulling in f2's vello/wgpu machinery.
f4 — graph3d (facett-graph3d)
A 3D node cloud viewer, CPU-projected today (orthographic + Y-rotation, depth → size/brightness), themed via facett_core::look/render::decor:
-
Node3(facett-graph3d/src/lib.rs:99-103) —label+color+pos: [f32;3](normalised ~[-1,1]).Layout3D— five placements:Sphere(Fibonacci sphere),Helix(Y-axis spiral),Grid(row-major cube lattice) andForce(deterministic 3D Fruchterman-Reingold, O(n²)/iteration) all derive a position from the node index or the edge set;Givenis the one that does not — see below. -
Layout3D::Given— the HOST placed them. The four layouts above are the right answer for a graph that knows nothing about itself, and the wrong one for a graph whose nodes have a place in the world and a measurement that earns the vertical axis: a road junction is at a lon/lat, a node ranked by a measured metric belongs at that height. Neither is expressible as "the i-th point on a sphere". BeforeGivena consumer had no way to say it — writing through thepub nodesfield worked until the nextMsg::SetLayoutorGraph3D::absorbsilently overwrote the lot.- API:
Graph3D::with_positions(Vec<[f32;3]>)(builder) /set_positions(..)(in place, re-lays out immediately whenGivenis selected) /positions() -> &[[f32;3]]/placement_is_complete() -> bool. One[x, y, z]per node, in node order, in the crate's normalised ~[-1,1]cube. The host owns the projection — the crate has none, which is exactly the seam this variant opens (korp runs its road junctions through the samemapquery::merc_xythe 2-D map uses;facett-demo'sgraph3d-giventab projects a synthetic lon/lat itself). - RETENTION: the placement is held in a private
givenfield, not written straight intonodes, so it survives a round trip —Given → Sphere → Givenrestores exactly what the host handed over instead of the sphere it was overwritten with. - SHORT PLACEMENT ⇒ observable fallback (the
Gridlattice). Aposthat does not cover every node is not drawn:layout()falls through to theGridarm (one arm, so the fallback cannot drift from the layout it names) and the pane raisesfacet-graph3d-5— "graph3d host placement (layoutgiven) does not cover every node — the grid lattice drew it instead" — withseverity() == Severity::Warning(amber, not red: the picture is honest, it is simply not the one that was asked for). Longer than the node list is fine; the tail is ignored. WhyGridand notForce. Two reasons, both measured on the real caller. (1)Graph3D::new(nodes, edges, Layout3D::Given)runslayout()BEFOREwith_positionscan hand anything over, so the fallback runs on EVERYGivengraph at construction — a full Barnes-Hut relaxation of korp's 89 130-node road slice, thrown away one line later. The grid is O(n). (2) A regular lattice is unmistakably not data; a force blob looks exactly like a graph somebody meant to draw, which is the failure this code exists to make visible. The rule is the one korp's geographic layout already runs on, and for the same reason: a partly-placed cloud draws the remainder at the origin, and that reads as data. - Two new
state_jsonkeys, on EVERYGraph3D:given_positions(usize — how many host positions were supplied) andlayout_fallback(the label of the layout that actually drew it, ornullwhen nothing degraded).layoutis the CHOICE,layout_fallbackis the truth on screen; both read off the singleplacement_is_complete()writer, so the report can never describe a picture the canvas did not draw.error_codecarries"facet-graph3d-5"in the same state andnullwhen the placement is complete. - Proven in
facett-graph3d/tests/given_positions.rs(complete / short / retention / the fallback report) and shown infacett-demo'sgraph3d-giventab — a synthetic traffic-sensor road network with x/y from a projected lon/lat and z from the measured speed, so the cloud reads as terraces and dipping ribbons rather than a force blob.
- API:
-
Graph3DView(:140-162) is the Elm Model — the control surface only: orbityaw+ auto-spin/spin_rate, the activeLayout3D, ashow_labelstoggle, an injected clock (clock_s: Option<f64>, FC-7 — a pinned clock makes the spin a pure function of state, so two renders are bit-identical), theDecorPolicy, andselected: Option<usize>(FC-5, keyed on node index, which is invariant across camera spin and relayout). The rawnodes/edgesgeometry stays OUTSIDE the Model, as plain fields onGraph3D(:207-223). -
Msg(:169-197):DragYaw/SetYaw/AdvanceSpin/SetSpin/Fit(camera),SetLayout(recomputes positions viaGraph3D::layout),SetShowLabels,SetDecor,SelectNode/ClearSelection,SetClock/Tick(the injected clock).Effectis uninhabited (:204). -
DecorPolicy(:64-93) —Off(flat),FromTheme(default: follows the activeLookTheme'sEffectsPolicy),Reduced(glow + selection focus, no particles/strobe),Full(the neon mode: gradient-by-scalar palette, breathing pulse, glow halos + AO orbs, particle tracers on lit edges, click → shockwave + dim). Resolved throughfacett_core::render::decor(facett-graph3d/src/lib.rs:35) and painted via the pure CPU-painter helpers inneon.rs(theNEON6-stop ramp,neon.rs:20-30;DIM_ALPHA = 0.1for unrelated nodes/edges when a selection is focused,neon.rs:18). -
CallGraphLayout(facett-graph3d/src/callgraph_layouts.rs:24-53) — seven structure-aware depth-axis placements distinct fromLayout3D:Force,ClusterZ(Louvain communities → z-shells),Topo(longest-path call depth),Spectral(Laplacian eigenvectors),Mds(classical MDS on hop-distance),Centrality(degree → z),Helix(degree-ordered spiral). All pure and deterministic (fixed iteration counts,sin(index)-seeded, no RNG). -
GPU lane (
facett-graph3d/src/graph_gpu.rs, featurewgpu) — an instanced emissive node/edge pipeline with real bloom: nodes/edges draw with additive blend into an HDR offscreen target (OffscreenColorDepth,Rgba16Float), then bright-pass →GaussianBlur→ Reinhard-tonemap composite.render_graph_offscreen(:641) is the headless-only entry point today; a liveegui_wgpupaint-callback wiring (mirroringlogo3d::gpu) is not yet built. This lane does not go throughf2'sdecide/Backend— it is independently feature-gated. -
logo3d(facett-graph3d/src/logo3d.rs) — the extruded znippy-rabbit 3D mascot (facett-graph3d's built-in 3D test data), sharing the crate's injected-clock spin discipline but not a node/edge graph.
f5 — syschart (facett-syschart)
A monitoring map of a handful of free-positioned peer nodes — the shape none of f1–f4 cover: f1's GraphView/core Scene is non-interactive, f1's DepGraphView and f2's depgraph_layout are layered DAGs with a fixed deps/dependents drill-down, not a free triangle of peers.
SysNode(facett-syschart/src/lib.rs:35-50) —id(stable, selection key),label,color,badge: u64(a drawn count),pos: (f32,f32)normalized0.0..=1.0of the canvas (resolution-independent, deterministic — headless- and screenshot-friendly), and a free-formdetailstring the host fills.SysEdge(:67-71) is an undirected{a, b}link.SysChartModel(:110-120) is the complete Elm Model —nodes,edges,selected: Option<String>(FC-5, keyed onSysNode::id),canvas_h. Unlikef1/f2/f3, there is no separate "input held outside the Model" split: the whole graph (including node colours) IS the observable state, becauseegui::Color32here is treated as serializable node data, not a paint-only concern.Msg(:84-94):Select(toggle by id, unknown id is a no-op — parity with the click),ClearSelection,SetBadge,SetDetail.update(:167-191) is the sole mutation path;Effectis uninhabited (:101).SystemChart::center(:234-245) resolves a node's normalized position to an absolute pixel centre inside the canvas rect, padded byNODE_R + 6.0so no chip clips the edge.view(:277-380) paints edges under nodes, a node circle + badge text + label, an AccessKit hit node per element (FC-4/FC-5, keyed on the stable id), and — below a separator — an inline detail panel:ui.weakplaceholder when nothing is selected, else the selected node's label/badge/detail lines (:348-368). The host may leavedetailempty and draw its own richer widgets afterui(), keyed offSystemChart::selected().